Preliminar implementation of billing machinery

This commit is contained in:
Marc 2014-09-16 14:35:00 +00:00
parent 97253d2d10
commit 5fa54adf24
13 changed files with 397 additions and 336 deletions

View File

@ -101,3 +101,4 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* bill.bad_debt() -> transaction.ABORTED * bill.bad_debt() -> transaction.ABORTED
* transaction.ABORTED -> bill.bad_debt * transaction.ABORTED -> bill.bad_debt
- Issue new transaction when current transaction is ABORTED - Issue new transaction when current transaction is ABORTED
* underescore *every* private function

View File

@ -39,7 +39,6 @@ class BillsBackend(object):
description=self.get_line_description(line), description=self.get_line_description(line),
) )
self.create_sublines(billine, line.discounts) self.create_sublines(billine, line.discounts)
print bills
return bills return bills
def format_period(self, ini, end): def format_period(self, ini, end):

View File

@ -1,5 +1,6 @@
import calendar import calendar
import datetime import datetime
import decimal
from dateutil import relativedelta from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -71,7 +72,7 @@ class ServiceHandler(plugins.Plugin):
else: else:
raise NotImplementedError(msg) raise NotImplementedError(msg)
bp = datetime.datetime(year=date.year, month=date.month, day=day, bp = datetime.datetime(year=date.year, month=date.month, day=day,
tzinfo=timezone.get_current_timezone()) tzinfo=timezone.get_current_timezone()).date()
elif self.billing_period == self.ANUAL: elif self.billing_period == self.ANUAL:
if self.billing_point == self.ON_REGISTER: if self.billing_point == self.ON_REGISTER:
month = order.registered_on.month month = order.registered_on.month
@ -87,7 +88,7 @@ class ServiceHandler(plugins.Plugin):
if bp.month >= month: if bp.month >= month:
year = bp.year + 1 year = bp.year + 1
bp = datetime.datetime(year=year, month=month, day=day, bp = datetime.datetime(year=year, month=month, day=day,
tzinfo=timezone.get_current_timezone()) tzinfo=timezone.get_current_timezone()).date()
elif self.billing_period == self.NEVER: elif self.billing_period == self.NEVER:
bp = order.registered_on bp = order.registered_on
else: else:
@ -96,21 +97,23 @@ class ServiceHandler(plugins.Plugin):
return order.cancelled_on return order.cancelled_on
return bp return bp
def get_pricing_size(self, ini, end): def get_price_size(self, ini, end):
rdelta = relativedelta.relativedelta(end, ini) rdelta = relativedelta.relativedelta(end, ini)
if self.get_pricing_period() == self.MONTHLY: if self.billing_period == self.MONTHLY:
size = rdelta.months size = rdelta.years * 12
size += rdelta.months
days = calendar.monthrange(end.year, end.month)[1] days = calendar.monthrange(end.year, end.month)[1]
size += float(rdelta.days)/days size += decimal.Decimal(rdelta.days)/days
elif self.get_pricing_period() == self.ANUAL: elif self.billing_period == self.ANUAL:
size = rdelta.years size = rdelta.years
size += decimal.Decimal(rdelta.months)/12
days = 366 if calendar.isleap(end.year) else 365 days = 366 if calendar.isleap(end.year) else 365
size += float((end-ini).days)/days size += decimal.Decimal(rdelta.days)/days
elif self.get_pricing_period() == self.NEVER: elif self.billing_period == self.NEVER:
size = 1 size = 1
else: else:
raise NotImplementedError raise NotImplementedError
return size return decimal.Decimal(size)
def get_pricing_slots(self, ini, end): def get_pricing_slots(self, ini, end):
period = self.get_pricing_period() period = self.get_pricing_period()
@ -131,56 +134,139 @@ class ServiceHandler(plugins.Plugin):
yield ini, next yield ini, next
ini = next ini = next
def get_price_with_orders(self, order, size, ini, end): def generate_discount(self, line, dtype, price):
porders = self.orders.filter(account=order.account).filter( line.discounts.append(AttributeDict(**{
Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini) 'type': dtype,
).filter(registered_on__lt=end).order_by('registered_on') 'total': price,
price = 0
if self.orders_effect == self.REGISTER_OR_RENEW:
events = helpers.get_register_or_renew_events(porders, order, ini, end)
elif self.orders_effect == self.CONCURRENT:
events = helpers.get_register_or_cancel_events(porders, order, ini, end)
else:
raise NotImplementedError
for metric, position, ratio in events:
price += self.get_price(order, metric, position=position) * size * ratio
return price
def get_price_with_metric(self, order, size, ini, end):
metric = order.get_metric(ini, end)
price = self.get_price(order, metric) * size
return price
def generate_line(self, order, price, size, ini, end):
subtotal = float(self.nominal_price) * size
discounts = []
if subtotal > price:
discounts.append(AttributeDict(**{
'type': 'volume',
'total': price-subtotal
})) }))
elif subtotal < price:
raise ValueError("Something is wrong!") def generate_line(self, order, price, size, ini, end, discounts=[]):
return AttributeDict(**{ subtotal = self.nominal_price * size
line = AttributeDict(**{
'order': order, 'order': order,
'subtotal': subtotal, 'subtotal': subtotal,
'size': size, 'size': size,
'ini': ini, 'ini': ini,
'end': end, 'end': end,
'discounts': discounts, 'discounts': [],
}) })
discounted = 0
for dtype, dprice in discounts:
self.generate_discount(line, dtype, dprice)
discounted += dprice
subtotal -= discounted
if subtotal > price:
self.generate_discount(line, 'volume', price-subtotal)
return line
def _generate_bill_lines(self, orders, account, **options): def compensate(self, givers, receivers, commit=True):
compensations = []
for order in givers:
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
interval = helpers.Interval(order.cancelled_on, order.billed_until, order)
compensations.append[interval]
for order in receivers:
if not order.billed_until or order.billed_until < order.new_billed_until:
# receiver
ini = order.billed_until or order.registered_on
end = order.cancelled_on or datetime.date.max
order_interval = helpers.Interval(ini, order.new_billed_until)
compensations, used_compensations = helpers.compensate(order_interval, compensations)
order._compensations = used_compensations
for comp in used_compensations:
comp.order.new_billed_until = min(comp.order.billed_until, comp.end)
if commit:
for order in givers:
if hasattr(order, 'new_billed_until'):
order.billed_until = order.new_billed_until
order.save()
def get_register_or_renew_events(self, porders, ini, end):
# TODO count intermediat billing points too
counter = 0
for order in porders:
bu = getattr(order, 'new_billed_until', order.billed_until)
if bu:
if order.register >= ini and order.register < end:
counter += 1
if order.register != bu and bu >= ini and bu < end:
counter += 1
if order.billed_until and order.billed_until != bu:
if order.register != order.billed_until and order.billed_until >= ini and order.billed_until < end:
counter += 1
return counter
def bill_concurrent_orders(self, account, porders, rates, ini, end, commit=True):
# Concurrent
# Get pricing orders
priced = {}
for ini, end, orders in helpers.get_chunks(porders, ini, end):
size = self.get_price_size(ini, end)
metric = len(orders)
interval = helpers.Interval(ini=ini, end=end)
for position, order in enumerate(orders):
csize = 0
compensations = getattr(order, '_compensations', [])
for comp in compensations:
intersect = comp.intersect(interval)
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 * (size-csize)
if order in prices:
priced[order][0] += price
priced[order][1] += cprice
else:
priced[order] = (price, cprice)
lines = []
for order, prices in priced.iteritems():
# Generate lines and discounts from order.nominal_price
price, cprice = prices
if cprice:
discounts = (('compensation', cprice),)
line = self.generate_line(order, price, size, ini, end, discounts=discounts)
lines.append(line)
if commit:
order.billed_until = order.new_billed_until
order.save()
return lines
def bill_registered_or_renew_events(self, account, porders, rates, ini, end, commit=True):
# Before registration
lines = []
perido = self.get_pricing_period()
if period == self.MONTHLY:
rdelta = relativedelta.relativedelta(months=1)
elif period == self.ANUAL:
rdelta = relativedelta.relativedelta(years=1)
elif period == self.NEVER:
raise NotImplementedError("Rates with no pricing period?")
ini -= rdelta
for position, order in enumerate(porders):
if hasattr(order, 'new_billed_until'):
cend = order.billed_until or order.registered_on
cini = cend - rdelta
metric = self.get_register_or_renew_events(porders, cini, cend)
size = self.get_price_size(ini, end)
price = self.get_price(account, metric, position=position, rates=rates)
price = price * size
line = self.generate_line(order, price, size, ini, end)
lines.append(line)
if commit:
order.billed_until = order.new_billed_until
order.save()
def bill_with_orders(self, orders, account, **options):
# For the "boundary conditions" just think that: # For the "boundary conditions" just think that:
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0) # date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
# In most cases: # In most cases:
# ini >= registered_date, end < registered_date # ini >= registered_date, end < registered_date
bp = None bp = None
lines = [] lines = []
commit = options.get('commit', True) commit = options.get('commit', True)
ini = datetime.date.max ini = datetime.date.max
end = datetime.date.ini end = datetime.date.min
# boundary lookup # boundary lookup
for order in orders: for order in orders:
cini = order.registered_on cini = order.registered_on
@ -189,87 +275,42 @@ class ServiceHandler(plugins.Plugin):
bp = self.get_billing_point(order, bp=bp, **options) bp = self.get_billing_point(order, bp=bp, **options)
order.new_billed_until = bp order.new_billed_until = bp
ini = min(ini, cini) ini = min(ini, cini)
end = max(end, bp) # TODO if all bp are the same ... end = max(end, bp)
from .models import Order
related_orders = Order.objects.filter(service=self.service, account=account) related_orders = Order.objects.filter(service=self.service, account=account)
if self.on_cancel == self.COMPENSATE: if self.on_cancel == self.COMPENSATE:
# Get orders pending for compensation # Get orders pending for compensation
givers = related_orders.filter_givers(ini, end) givers = related_orders.filter_givers(ini, end)
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on) givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on) orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
self.compensate(givers, orders) self.compensate(givers, orders, commit=commit)
rates = 'TODO' rates = self.get_rates(account)
if rates: if rates:
# Get pricing orders
porders = related_orders.filter_pricing_orders(ini, end) porders = related_orders.filter_pricing_orders(ini, end)
porders = set(orders).union(set(porders)) porders = list(set(orders).union(set(porders)))
for ini, end, orders in self.get_chunks(porders, ini, end): porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
if self.pricing_period == self.ANUAL: if self.billing_period != self.NEVER and self.get_pricing_period != self.NEVER:
pass liens = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit)
elif self.pricing_period == self.MONTHLY:
pass
else: else:
raise NotImplementedError lines = self.bill_registered_or_renew_events(account, porders, rates, ini, end, commit=commit)
metric = len(orders)
for position, order in enumerate(orders):
# TODO position +1?
price = self.get_price(order, metric, position=position)
price *= size
else: else:
pass lines = []
price = self.nominal_price
def compensate(self, givers, receivers): # Calculate nominal price
compensations = [] for order in orders:
for order in givers:
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
compensations.append[Interval(order.cancelled_on, order.billed_until, order)]
for order in receivers:
if not order.billed_until or order.billed_until < order.new_billed_until:
# receiver
ini = order.billed_until or order.registered_on ini = order.billed_until or order.registered_on
end = order.cancelled_on or datetime.date.max end = order.new_billed_until
order_interval = helpers.Interval(ini, order.new_billed_until) # TODO beyond interval? size = self.get_price_size(ini, end)
compensations, used_compensations = helpers.compensate(order_interval, compensations) order.nominal_price = price * size
order._compensations = used_compensations line = self.generate_line(order, price*size, size, ini, end)
for comp in used_compensations: lines.append(line)
comp.order.billed_until = min(comp.order.billed_until, comp.end) if commit:
order.billed_until = order.new_billed_until
order.save()
return lines
def get_chunks(self, porders, ini, end, ix=0): def bill_with_metric(self, orders, account, **options):
if ix >= len(porders):
return [[ini, end, []]]
order = porders[ix]
ix += 1
bu = getattr(order, 'new_billed_until', order.billed_until)
if not bu or bu <= ini or order.registered_on >= end:
return self.get_chunks(porders, ini, end, ix=ix)
result = []
if order.registered_on < end and order.registered_on > ini:
ro = order.registered_on
result = self.get_chunks(porders, ini, ro, ix=ix)
ini = ro
if bu < end:
result += self.get_chunks(porders, bu, end, ix=ix)
end = bu
chunks = self.get_chunks(porders, ini, end, ix=ix)
for chunk in chunks:
chunk[2].insert(0, order)
result.append(chunk)
return result
def generate_bill_lines(self, orders, **options):
# For the "boundary conditions" just think that:
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
# In most cases:
# ini >= registered_date, end < registered_date
# TODO Perform compensations on cancelled services
if self.on_cancel in (self.COMPENSATE, self.REFOUND):
pass
# TODO compensations with commit=False, fuck commit or just fuck the transaction?
# compensate(orders, **options)
# TODO create discount per compensation
bp = None
lines = [] lines = []
commit = options.get('commit', True) commit = options.get('commit', True)
for order in orders: for order in orders:
@ -277,18 +318,32 @@ class ServiceHandler(plugins.Plugin):
ini = order.billed_until or order.registered_on ini = order.billed_until or order.registered_on
if bp <= ini: if bp <= ini:
continue continue
if not self.metric: order.new_billed_until = bp
# Number of orders metric; bill line per order
size = self.get_pricing_size(ini, bp)
price = self.get_price_with_orders(order, size, ini, bp)
lines.append(self.generate_line(order, price, size, ini, bp))
else:
# weighted metric; bill line per pricing period # weighted metric; bill line per pricing period
prev = None
lines_info = []
for ini, end in self.get_pricing_slots(ini, bp): for ini, end in self.get_pricing_slots(ini, bp):
size = self.get_pricing_size(ini, end) size = self.get_price_size(ini, end)
price = self.get_price_with_metric(order, size, ini, end) metric = order.get_metric(ini, end)
price = self.get_price(order, metric)
current = AttributeDict(price=price, size=size, ini=ini, end=end)
if prev and prev.metric == current.metric and prev.end == current.end:
prev.end = current.end
prev.size += current.size
prev.price += current.price
else:
lines_info.append(current)
prev = current
for line in lines_info:
lines.append(self.generate_line(order, price, size, ini, end)) lines.append(self.generate_line(order, price, size, ini, end))
order.billed_until = bp
if commit: if commit:
order.billed_until = order.new_billed_until
order.save() order.save()
return lines return lines
def generate_bill_lines(self, orders, account, **options):
if not self.metric:
lines = self.bill_with_orders(orders, account, **options)
else:
lines = self.bill_with_metric(orders, account, **options)
return lines

View File

@ -39,57 +39,27 @@ def get_related_objects(origin, max_depth=2):
queue.append(new_models) queue.append(new_models)
def get_register_or_cancel_events(porders, order, ini, end): def get_chunks(porders, ini, end, ix=0):
assert ini <= end, "ini > end" if ix >= len(porders):
CANCEL = 'cancel' return [[ini, end, []]]
REGISTER = 'register' order = porders[ix]
changes = {} ix += 1
counter = 0 bu = getattr(order, 'new_billed_until', order.billed_until)
for num, porder in enumerate(porders.order_by('registered_on'), start=1): if not bu or bu <= ini or order.registered_on >= end:
if porder == order: return get_chunks(porders, ini, end, ix=ix)
position = num result = []
if porder.cancelled_on: if order.registered_on < end and order.registered_on > ini:
cancel = porder.cancelled_on ro = order.registered_on
if porder.billed_until and porder.cancelled_on < porder.billed_until: result = get_chunks(porders, ini, ro, ix=ix)
cancel = porder.billed_until ini = ro
if cancel > ini and cancel < end: if bu < end:
changes.setdefault(cancel, []) result += get_chunks(porders, bu, end, ix=ix)
changes[cancel].append((CANCEL, num)) end = bu
if porder.registered_on <= ini: chunks = get_chunks(porders, ini, end, ix=ix)
counter += 1 for chunk in chunks:
elif porder.registered_on < end: chunk[2].insert(0, order)
changes.setdefault(porder.registered_on, []) result.append(chunk)
changes[porder.registered_on].append((REGISTER, num)) return result
pointer = ini
total = float((end-ini).days)
for date in sorted(changes.keys()):
yield counter, position, (date-pointer).days/total
for change, num in changes[date]:
if change is CANCEL:
counter -= 1
if num < position:
position -= 1
else:
counter += 1
pointer = date
yield counter, position, (end-pointer).days/total
def get_register_or_renew_events(handler, porders, order, ini, end):
total = float((end-ini).days)
for sini, send in handler.get_pricing_slots(ini, end):
counter = 0
position = -1
for porder in porders.order_by('registered_on'):
if porder == order:
position = abs(position)
elif position < 0:
position -= 1
if porder.registered_on >= sini and porder.registered_on < send:
counter += 1
elif porder.billed_until > send or porder.cancelled_on > send:
counter += 1
yield counter, position, (send-sini)/total
def cmp_billed_until_or_registered_on(a, b): def cmp_billed_until_or_registered_on(a, b):
@ -158,8 +128,9 @@ def get_intersections(order_intervals, compensations):
intersections.sort() intersections.sort()
return intersections return intersections
# Intervals should not overlap
def intersect(compensation, order_intervals): def intersect(compensation, order_intervals):
# Intervals should not overlap
compensated = [] compensated = []
not_compensated = [] not_compensated = []
unused_compensation = [] unused_compensation = []
@ -167,14 +138,16 @@ def intersect(compensation, order_intervals):
compensated.append(compensation.intersect(interval, unused_compensation, not_compensated)) compensated.append(compensation.intersect(interval, unused_compensation, not_compensated))
return (compensated, not_compensated, unused_compensation) return (compensated, not_compensated, unused_compensation)
def apply_compensation(order, compensation): def apply_compensation(order, compensation):
remaining_order = [] remaining_order = []
remaining_compensation = [] remaining_compensation = []
applied_compensation = compensation.intersect_set(order, remaining_compensation, remaining_order) applied_compensation = compensation.intersect_set(order, remaining_compensation, remaining_order)
return applied_compensation, remaining_order, remaining_compensation return applied_compensation, remaining_order, remaining_compensation
# TODO can be optimized
def update_intersections(not_compensated, compensations): def update_intersections(not_compensated, compensations):
# TODO can be optimized
compensation_intervals = [] compensation_intervals = []
for __, compensation in compensations: for __, compensation in compensations:
compensation_intervals.append(compensation) compensation_intervals.append(compensation)

View File

@ -49,7 +49,7 @@ class RateQuerySet(models.QuerySet):
return self.filter( return self.filter(
Q(plan__is_default=True) | Q(plan__is_default=True) |
Q(plan__contracts__account=account) Q(plan__contracts__account=account)
).order_by('plan', 'quantity').select_related('plan').distinct() ).order_by('plan', 'quantity').select_related('plan')
class Rate(models.Model): class Rate(models.Model):
@ -257,12 +257,13 @@ class Service(models.Model):
return self.billing_period return self.billing_period
return self.pricing_period return self.pricing_period
def get_price(self, order, metric, rates=None, position=None): def get_price(self, account, metric, rates=None, position=None):
""" """
if position is provided an specific price for that position is returned, if position is provided an specific price for that position is returned,
accumulated price is returned otherwise accumulated price is returned otherwise
""" """
rates = self.get_rates(order.account) if rates is None:
rates = self.get_rates(account)
if not rates: if not rates:
rates = [{ rates = [{
'quantity': metric, 'quantity': metric,
@ -288,7 +289,7 @@ class Service(models.Model):
if counter >= position: if counter >= position:
return float(rate['price']) return float(rate['price'])
def get_rates(self, account, cache=False): def get_rates(self, account, cache=True):
# rates are cached per account # rates are cached per account
if not cache: if not cache:
return self.rates.by_account(account) return self.rates.by_account(account)
@ -310,7 +311,7 @@ class OrderQuerySet(models.QuerySet):
bill_backend = Order.get_bill_backend() bill_backend = Order.get_bill_backend()
qs = self.select_related('account', 'service') qs = self.select_related('account', 'service')
commit = options.get('commit', True) commit = options.get('commit', True)
for account, services in qs.group_by('account', 'service'): for account, services in qs.group_by('account', 'service').iteritems():
bill_lines = [] bill_lines = []
for service, orders in services: for service, orders in services:
lines = service.handler.generate_bill_lines(orders, account, **options) lines = service.handler.generate_bill_lines(orders, account, **options)

View File

@ -7,14 +7,25 @@ def _compute(rates, metric):
value = 0 value = 0
num = len(rates) num = len(rates)
accumulated = 0 accumulated = 0
barrier = 1
next_barrier = None
end = False end = False
ix = 0 ix = 0
steps = [] steps = []
while ix < num and not end: while ix < num and not end:
fold = 1
# Multiple contractions
while ix < num-1 and rates[ix] == rates[ix+1]:
ix += 1
fold += 1
if ix+1 == num: if ix+1 == num:
quantity = metric - accumulated quantity = metric - accumulated
next_barrier = quantity
else: else:
quantity = rates[ix+1].quantity - rates[ix].quantity quantity = rates[ix+1].quantity - rates[ix].quantity
next_barrier = quantity
if rates[ix+1].price > rates[ix].price:
quantity *= fold
if accumulated+quantity > metric: if accumulated+quantity > metric:
quantity = metric - accumulated quantity = metric - accumulated
end = True end = True
@ -22,9 +33,10 @@ def _compute(rates, metric):
steps.append(AttributeDict(**{ steps.append(AttributeDict(**{
'quantity': quantity, 'quantity': quantity,
'price': price, 'price': price,
'barrier': accumulated+1, 'barrier': barrier,
})) }))
accumulated += quantity accumulated += quantity
barrier += next_barrier
value += quantity*price value += quantity*price
ix += 1 ix += 1
return value, steps return value, steps
@ -32,9 +44,10 @@ def _compute(rates, metric):
def step_price(rates, metric): def step_price(rates, metric):
# Step price # Step price
# TODO allow multiple plans
group = [] group = []
minimal = (sys.maxint, []) minimal = (sys.maxint, [])
for plan, rates in rates.group_by('plan'): for plan, rates in rates.group_by('plan').iteritems():
value, steps = _compute(rates, metric) value, steps = _compute(rates, metric)
if plan.is_combinable: if plan.is_combinable:
group.append(steps) group.append(steps)
@ -90,7 +103,7 @@ def match_price(rates, metric):
candidates = [] candidates = []
selected = False selected = False
prev = None prev = None
for rate in rates: for rate in rates.distinct():
if prev and prev.plan != rate.plan: if prev and prev.plan != rate.plan:
if not selected and prev.quantity <= metric: if not selected and prev.quantity <= metric:
candidates.append(prev) candidates.append(prev)

View File

@ -1,4 +1,6 @@
import datetime import datetime
import decimal
import sys
from dateutil import relativedelta from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -9,7 +11,7 @@ from orchestra.apps.users.models import User
from orchestra.utils.tests import BaseTestCase, random_ascii from orchestra.utils.tests import BaseTestCase, random_ascii
from ... import settings, helpers from ... import settings, helpers
from ...models import Service, Order from ...models import Plan, Service, Order
class OrderTests(BaseTestCase): class OrderTests(BaseTestCase):
@ -26,122 +28,114 @@ class OrderTests(BaseTestCase):
account.save() account.save()
return account return account
def create_service(self): def create_ftp_service(self):
service = Service.objects.create( service = Service.objects.create(
description="FTP Account", description="FTP Account",
content_type=ContentType.objects.get_for_model(User), content_type=ContentType.objects.get_for_model(User),
match='not user.is_main and user.has_posix()', match='not user.is_main and user.has_posix()',
billing_period=Service.ANUAL, billing_period=Service.ANUAL,
billing_point=Service.FIXED_DATE, billing_point=Service.FIXED_DATE,
# delayed_billing=Service.NEVER,
is_fee=False, is_fee=False,
metric='', metric='',
pricing_period=Service.BILLING_PERIOD, pricing_period=Service.BILLING_PERIOD,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.STEP_PRICE,
# orders_effect=Service.CONCURRENT,
on_cancel=Service.DISCOUNT, on_cancel=Service.DISCOUNT,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,
# trial_period=Service.NEVER, tax=0,
# refound_period=Service.NEVER,
tax=21,
nominal_price=10, nominal_price=10,
) )
return service return service
# def test_ftp_account_1_year_fiexed(self): def create_ftp(self, account=None):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(years=1)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(20, bills[0].get_total())
def create_ftp(self):
username = '%s_ftp' % random_ascii(10) username = '%s_ftp' % random_ascii(10)
if not account:
account = self.create_account() account = self.create_account()
user = User.objects.create_user(username=username, account=account) user = User.objects.create_user(username=username, account=account)
POSIX = user._meta.get_field_by_name('posix')[0].model POSIX = user._meta.get_field_by_name('posix')[0].model
POSIX.objects.create(user=user) POSIX.objects.create(user=user)
return user return user
def atest_get_chunks(self): def test_get_chunks(self):
service = self.create_service() service = self.create_ftp_service()
handler = service.handler handler = service.handler
porders = [] porders = []
now = timezone.now().date() now = timezone.now().date()
ct = ContentType.objects.get_for_model(User) ct = ContentType.objects.get_for_model(User)
account = self.create_account()
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order = Order.objects.get(content_type=ct, object_id=ftp.pk) order = Order.objects.get(content_type=ct, object_id=ftp.pk)
porders.append(order) porders.append(order)
end = handler.get_billing_point(order).date() end = handler.get_billing_point(order)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(1, len(chunks)) self.assertEqual(1, len(chunks))
self.assertIn([now, end, []], chunks) self.assertIn([now, end, []], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order1 = Order.objects.get(content_type=ct, object_id=ftp.pk) order1 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order1.billed_until = now+datetime.timedelta(days=2) order1.billed_until = now+datetime.timedelta(days=2)
porders.append(order1) porders.append(order1)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(2, len(chunks)) self.assertEqual(2, len(chunks))
self.assertIn([order1.registered_on, order1.billed_until, [order1]], chunks) self.assertIn([order1.registered_on, order1.billed_until, [order1]], chunks)
self.assertIn([order1.billed_until, end, []], chunks) self.assertIn([order1.billed_until, end, []], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order2 = Order.objects.get(content_type=ct, object_id=ftp.pk) order2 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order2.billed_until = now+datetime.timedelta(days=700) order2.billed_until = now+datetime.timedelta(days=700)
porders.append(order2) porders.append(order2)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(2, len(chunks)) self.assertEqual(2, len(chunks))
self.assertIn([order.registered_on, order1.billed_until, [order1, order2]], chunks) self.assertIn([order.registered_on, order1.billed_until, [order1, order2]], chunks)
self.assertIn([order1.billed_until, end, [order2]], chunks) self.assertIn([order1.billed_until, end, [order2]], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order3 = Order.objects.get(content_type=ct, object_id=ftp.pk) order3 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order3.billed_until = now+datetime.timedelta(days=700) order3.billed_until = now+datetime.timedelta(days=700)
porders.append(order3) porders.append(order3)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(2, len(chunks)) self.assertEqual(2, len(chunks))
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks) self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
self.assertIn([order1.billed_until, end, [order2, order3]], chunks) self.assertIn([order1.billed_until, end, [order2, order3]], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order4 = Order.objects.get(content_type=ct, object_id=ftp.pk) order4 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order4.registered_on = now+datetime.timedelta(days=5) order4.registered_on = now+datetime.timedelta(days=5)
order4.billed_until = now+datetime.timedelta(days=10) order4.billed_until = now+datetime.timedelta(days=10)
porders.append(order4) porders.append(order4)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(4, len(chunks)) self.assertEqual(4, len(chunks))
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks) self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks) self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks) self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
self.assertIn([order4.billed_until, end, [order2, order3]], chunks) self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order5 = Order.objects.get(content_type=ct, object_id=ftp.pk) order5 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order5.registered_on = now+datetime.timedelta(days=700) order5.registered_on = now+datetime.timedelta(days=700)
order5.billed_until = now+datetime.timedelta(days=780) order5.billed_until = now+datetime.timedelta(days=780)
porders.append(order5) porders.append(order5)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(4, len(chunks)) self.assertEqual(4, len(chunks))
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks) self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks) self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks) self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
self.assertIn([order4.billed_until, end, [order2, order3]], chunks) self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
ftp = self.create_ftp() ftp = self.create_ftp(account=account)
order6 = Order.objects.get(content_type=ct, object_id=ftp.pk) order6 = Order.objects.get(content_type=ct, object_id=ftp.pk)
order6.registered_on = now-datetime.timedelta(days=780) order6.registered_on = now-datetime.timedelta(days=780)
order6.billed_until = now-datetime.timedelta(days=700) order6.billed_until = now-datetime.timedelta(days=700)
porders.append(order6) porders.append(order6)
chunks = handler.get_chunks(porders, now, end) chunks = helpers.get_chunks(porders, now, end)
self.assertEqual(4, len(chunks)) self.assertEqual(4, len(chunks))
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks) self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks) self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks) self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
self.assertIn([order4.billed_until, end, [order2, order3]], chunks) self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
def atest_sort_billed_until_or_registered_on(self): def test_sort_billed_until_or_registered_on(self):
service = self.create_service() service = self.create_ftp_service()
now = timezone.now() now = timezone.now()
order = Order( order = Order(
service=service, service=service,
@ -171,7 +165,7 @@ class OrderTests(BaseTestCase):
orders = [order3, order, order1, order2, order4, order5, order6] orders = [order3, order, order1, order2, order4, order5, order6]
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on)) self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
def atest_compensation(self): def test_compensation(self):
now = timezone.now() now = timezone.now()
order = Order( order = Order(
description='0', description='0',
@ -219,7 +213,7 @@ class OrderTests(BaseTestCase):
]) ])
porders = [order3, order, order1, order2, order4, order5, order6] porders = [order3, order, order1, order2, order4, order5, order6]
porders = sorted(porders, cmp=helpers.cmp_billed_until_or_registered_on) porders = sorted(porders, cmp=helpers.cmp_billed_until_or_registered_on)
service = self.create_service() service = self.create_ftp_service()
compensations = [] compensations = []
receivers = [] receivers = []
for order in porders: for order in porders:
@ -237,29 +231,22 @@ class OrderTests(BaseTestCase):
self.assertEqual(test_line[1], compensation.end) self.assertEqual(test_line[1], compensation.end)
self.assertEqual(test_line[2], compensation.order) self.assertEqual(test_line[2], compensation.order)
def get_rates(self, account):
rates = self.rates.filter(Q(plan__is_default=True) | Q(plan__contracts__account=account)).order_by('plan', 'quantity').select_related('plan').disctinct()
def test_rates(self): def test_rates(self):
from ...models import Plan service = self.create_ftp_service()
import sys account = self.create_account()
from decimal import Decimal
service = self.create_service()
superplan = Plan.objects.create(name='SUPER', allow_multiple=False, is_combinable=True) superplan = Plan.objects.create(name='SUPER', allow_multiple=False, is_combinable=True)
service.rates.create(plan=superplan, quantity=1, price=0) service.rates.create(plan=superplan, quantity=1, price=0)
service.rates.create(plan=superplan, quantity=3, price=10) service.rates.create(plan=superplan, quantity=3, price=10)
service.rates.create(plan=superplan, quantity=4, price=9) service.rates.create(plan=superplan, quantity=4, price=9)
service.rates.create(plan=superplan, quantity=10, price=1) service.rates.create(plan=superplan, quantity=10, price=1)
account = self.create_account()
account.plans.create(plan=superplan) account.plans.create(plan=superplan)
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
rates = [ rates = [
{'price': Decimal('0.00'), 'quantity': 2}, {'price': decimal.Decimal('0.00'), 'quantity': 2},
{'price': Decimal('10.00'), 'quantity': 1}, {'price': decimal.Decimal('10.00'), 'quantity': 1},
{'price': Decimal('9.00'), 'quantity': 6}, {'price': decimal.Decimal('9.00'), 'quantity': 6},
{'price': Decimal('1.00'), 'quantity': 21} {'price': decimal.Decimal('1.00'), 'quantity': 21}
] ]
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
@ -268,44 +255,44 @@ class OrderTests(BaseTestCase):
dupeplan = Plan.objects.create(name='DUPE', allow_multiple=True, is_combinable=True) dupeplan = Plan.objects.create(name='DUPE', allow_multiple=True, is_combinable=True)
service.rates.create(plan=dupeplan, quantity=1, price=0) service.rates.create(plan=dupeplan, quantity=1, price=0)
service.rates.create(plan=dupeplan, quantity=3, price=9) service.rates.create(plan=dupeplan, quantity=3, price=9)
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity) self.assertEqual(rate['quantity'], result.quantity)
account.plans.create(plan=dupeplan) account.plans.create(plan=dupeplan)
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
rates = [ rates = [
{'price': Decimal('0.00'), 'quantity': 4}, {'price': decimal.Decimal('0.00'), 'quantity': 4},
{'price': Decimal('9.00'), 'quantity': 5}, {'price': decimal.Decimal('9.00'), 'quantity': 5},
{'price': Decimal('1.00'), 'quantity': 21}, {'price': decimal.Decimal('1.00'), 'quantity': 21},
] ]
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity) self.assertEqual(rate['quantity'], result.quantity)
hyperplan = Plan.objects.create(name='HYPER', allow_multiple=True, is_combinable=False) hyperplan = Plan.objects.create(name='HYPER', allow_multiple=False, is_combinable=False)
service.rates.create(plan=hyperplan, quantity=1, price=0) service.rates.create(plan=hyperplan, quantity=1, price=0)
service.rates.create(plan=hyperplan, quantity=20, price=5) service.rates.create(plan=hyperplan, quantity=20, price=5)
account.plans.create(plan=hyperplan) account.plans.create(plan=hyperplan)
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
rates = [ rates = [
{'price': Decimal('0.00'), 'quantity': 19}, {'price': decimal.Decimal('0.00'), 'quantity': 19},
{'price': Decimal('5.00'), 'quantity': 11} {'price': decimal.Decimal('5.00'), 'quantity': 11}
] ]
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity) self.assertEqual(rate['quantity'], result.quantity)
hyperplan.is_combinable = True hyperplan.is_combinable = True
hyperplan.save() hyperplan.save()
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
rates = [ rates = [
{'price': Decimal('0.00'), 'quantity': 23}, {'price': decimal.Decimal('0.00'), 'quantity': 23},
{'price': Decimal('1.00'), 'quantity': 7} {'price': decimal.Decimal('1.00'), 'quantity': 7}
] ]
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
@ -313,67 +300,99 @@ class OrderTests(BaseTestCase):
service.rate_algorithm = service.MATCH_PRICE service.rate_algorithm = service.MATCH_PRICE
service.save() service.save()
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
self.assertEqual(1, len(results)) self.assertEqual(1, len(results))
self.assertEqual(Decimal('1.00'), results[0].price) self.assertEqual(decimal.Decimal('1.00'), results[0].price)
self.assertEqual(30, results[0].quantity) self.assertEqual(30, results[0].quantity)
hyperplan.delete() hyperplan.delete()
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 8) results = service.rate_method(results, 8)
self.assertEqual(1, len(results)) self.assertEqual(1, len(results))
self.assertEqual(Decimal('9.00'), results[0].price) self.assertEqual(decimal.Decimal('9.00'), results[0].price)
self.assertEqual(8, results[0].quantity) self.assertEqual(8, results[0].quantity)
superplan.delete() superplan.delete()
results = service.get_rates(account) results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30) results = service.rate_method(results, 30)
self.assertEqual(1, len(results)) self.assertEqual(1, len(results))
self.assertEqual(Decimal('9.00'), results[0].price) self.assertEqual(decimal.Decimal('9.00'), results[0].price)
self.assertEqual(30, results[0].quantity) self.assertEqual(30, results[0].quantity)
# def test_ftp_account_1_year_fiexed(self): def test_rates_allow_multiple(self):
# service = self.create_service() service = self.create_ftp_service()
# now = timezone.now().date()etb account = self.create_account()
# month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH dupeplan = Plan.objects.create(name='DUPE', allow_multiple=True, is_combinable=True)
# ini = datetime.datetime(year=now.year, month=month, account.plans.create(plan=dupeplan)
# day=1, tzinfo=timezone.get_current_timezone()) service.rates.create(plan=dupeplan, quantity=1, price=0)
# order = service.orders.all()[0] service.rates.create(plan=dupeplan, quantity=3, price=9)
# order.registered_on = ini results = service.get_rates(account, cache=False)
# order.save() results = service.rate_method(results, 30)
# bp = ini rates = [
# bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False) {'price': decimal.Decimal('0.00'), 'quantity': 2},
# print bills[0][1][0].subtotal {'price': decimal.Decimal('9.00'), 'quantity': 28},
# print bills ]
# bp = ini + relativedelta.relativedelta(months=12) for rate, result in zip(rates, results):
# bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False) self.assertEqual(rate['price'], result.price)
# print bills[0][1][0].subtotal self.assertEqual(rate['quantity'], result.quantity)
# print bills
# def test_ftp_account_2_year_fiexed(self):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(years=2)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(40, bills[0].get_total())
#
# def test_ftp_account_6_month_fixed(self):
# service = self.create_service()
# bp = timezone.now().date() + relativedelta.relativedelta(months=6)
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
# self.assertEqual(6, bills[0].get_total())
#
# def test_ftp_account_next_billing_point(self):
# service = self.create_service()
# now = timezone.now().date()
# bp_month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
# if date.month > bp_month:
# bp = datetime.datetime(year=now.year+1, month=bp_month,
# day=1, tzinfo=timezone.get_current_timezone())
# else:
# bp = datetime.datetime(year=now.year, month=bp_month,
# day=1, tzinfo=timezone.get_current_timezone())
#
# days = (bp - now).days
# bills = service.orders.bill(billing_point=bp, fixed_point=False)
# self.assertEqual(40, bills[0].get_total())
account.plans.create(plan=dupeplan)
results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30)
rates = [
{'price': decimal.Decimal('0.00'), 'quantity': 4},
{'price': decimal.Decimal('9.00'), 'quantity': 26},
]
for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity)
account.plans.create(plan=dupeplan)
results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30)
rates = [
{'price': decimal.Decimal('0.00'), 'quantity': 6},
{'price': decimal.Decimal('9.00'), 'quantity': 24},
]
for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity)
def test_ftp_account_1_year_fiexed(self):
service = self.create_ftp_service()
user = self.create_ftp()
bp = timezone.now().date() + relativedelta.relativedelta(years=1)
bills = service.orders.bill(billing_point=bp, fixed_point=True)
self.assertEqual(10, bills[0].get_total())
def test_ftp_account_2_year_fiexed(self):
service = self.create_ftp_service()
user = self.create_ftp()
bp = timezone.now().date() + relativedelta.relativedelta(years=2)
bills = service.orders.bill(billing_point=bp, fixed_point=True)
self.assertEqual(20, bills[0].get_total())
def test_ftp_account_6_month_fixed(self):
service = self.create_ftp_service()
self.create_ftp()
bp = timezone.now().date() + relativedelta.relativedelta(months=6)
bills = service.orders.bill(billing_point=bp, fixed_point=True)
self.assertEqual(5, bills[0].get_total())
def test_ftp_account_next_billing_point(self):
service = self.create_ftp_service()
self.create_ftp()
now = timezone.now()
bp_month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
if now.month > bp_month:
bp = datetime.datetime(year=now.year+1, month=bp_month,
day=1, tzinfo=timezone.get_current_timezone())
else:
bp = datetime.datetime(year=now.year, month=bp_month,
day=1, tzinfo=timezone.get_current_timezone())
bills = service.orders.bill(billing_point=now, fixed_point=False)
size = decimal.Decimal((bp - now).days)/365
error = decimal.Decimal(0.05)
self.assertGreater(10*size+error*(10*size), bills[0].get_total())
self.assertLess(10*size-error*(10*size), bills[0].get_total())

View File

@ -11,7 +11,7 @@ def process_transactions(modeladmin, request, queryset):
msg = _("Selected transactions must be on '{state}' state") msg = _("Selected transactions must be on '{state}' state")
messages.error(request, msg.format(state=Transaction.WAITTING_PROCESSING)) messages.error(request, msg.format(state=Transaction.WAITTING_PROCESSING))
return return
for method, transactions in queryset.group_by('source__method'): for method, transactions in queryset.group_by('source__method').iteritems():
if method is not None: if method is not None:
method = PaymentMethod.get_plugin(method) method = PaymentMethod.get_plugin(method)
procs = method.process(transactions) procs = method.process(transactions)

View File

@ -137,7 +137,7 @@ def resource_inline_factory(resources):
if not running_syncdb(): if not running_syncdb():
# not run during syncdb # not run during syncdb
for ct, resources in Resource.objects.group_by('content_type'): for ct, resources in Resource.objects.group_by('content_type').iteritems():
inline = resource_inline_factory(resources) inline = resource_inline_factory(resources)
model = ct.model_class() model = ct.model_class()
insertattr(model, 'inlines', inline) insertattr(model, 'inlines', inline)

View File

@ -178,7 +178,7 @@ def create_resource_relation():
return self return self
relation = GenericRelation('resources.ResourceData') relation = GenericRelation('resources.ResourceData')
for ct, resources in Resource.objects.group_by('content_type'): for ct, resources in Resource.objects.group_by('content_type').iteritems():
model = ct.model_class() model = ct.model_class()
model.add_to_class('resource_set', relation) model.add_to_class('resource_set', relation)
model.resources = ResourceHandler() model.resources = ResourceHandler()

View File

@ -26,7 +26,7 @@ class ResourceSerializer(serializers.ModelSerializer):
if not running_syncdb(): if not running_syncdb():
# TODO why this is even loaded during syncdb? # TODO why this is even loaded during syncdb?
# Create nested serializers on target models # Create nested serializers on target models
for ct, resources in Resource.objects.group_by('content_type'): for ct, resources in Resource.objects.group_by('content_type').iteritems():
model = ct.model_class() model = ct.model_class()
try: try:
router.insert(model, 'resources', ResourceSerializer, required=False, many=True, source='resource_set') router.insert(model, 'resources', ResourceSerializer, required=False, many=True, source='resource_set')

View File

@ -1,5 +1,6 @@
from threading import currentThread from threading import currentThread
from django.core.cache.backends.dummy import DummyCache
from django.core.cache.backends.locmem import LocMemCache from django.core.cache.backends.locmem import LocMemCache
@ -16,14 +17,12 @@ class RequestCache(LocMemCache):
def get_request_cache(): def get_request_cache():
""" """
Returns per-request cache when running RequestCacheMiddleware otherwise a Returns per-request cache when running RequestCacheMiddleware otherwise a
new LocMemCache instance (when running periodic tasks or shell) DummyCache instance (when running periodic tasks, tests or shell)
""" """
try: try:
return _request_cache[currentThread()] return _request_cache[currentThread()]
except KeyError: except KeyError:
cache = RequestCache() return DummyCache('dummy', {})
_request_cache[currentThread()] = cache
return cache
class RequestCacheMiddleware(object): class RequestCacheMiddleware(object):
@ -33,7 +32,6 @@ class RequestCacheMiddleware(object):
cache.clear() cache.clear()
def process_response(self, request, response): def process_response(self, request, response):
# TODO not sure if this actually saves memory, remove otherwise
if currentThread() in _request_cache: if currentThread() in _request_cache:
_request_cache[currentThread()].clear() _request_cache[currentThread()].clear()
return response return response

View File

@ -1,28 +1,30 @@
from collections import OrderedDict
from .utils import get_field_value from .utils import get_field_value
def group_by(qset, *fields, **kwargs): def group_by(qset, *fields):
""" group_by iterator with support for multiple nested fields """ """ 100% in python in order to preserve original order_by """
ix = kwargs.get('ix', 0) first = OrderedDict()
if ix is 0: num = len(fields)
qset = qset.order_by(*fields)
group = []
first = True
for obj in qset: for obj in qset:
ix = 0
group = first
while ix < num:
try: try:
current = get_field_value(obj, fields[ix]) current = get_field_value(obj, fields[ix])
except AttributeError: except AttributeError:
# Intermediary relation does not exists # Intermediary relation does not exists
current = None current = None
if first or current == previous: if ix < num-1:
group.append(obj) try:
group = group[current]
except KeyError:
group[current] = OrderedDict()
else: else:
if ix < len(fields)-1: try:
group = group_by(group, *fields, ix=ix+1) group[current].append(obj)
yield previous, group except KeyError:
group = [obj] group[current] = [obj]
previous = current ix += 1
first = False return first
if ix < len(fields)-1:
group = group_by(group, *fields, ix=ix+1)
yield previous, group