Added support for order ignoring
This commit is contained in:
parent
0417d18961
commit
0dcdb4ba79
10
TODO.md
10
TODO.md
|
@ -42,8 +42,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
|
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
|
||||||
* pip install pyinotify
|
* pip install pyinotify
|
||||||
|
|
||||||
* create custom field that returns backend python objects
|
|
||||||
|
|
||||||
* Timezone awareness on monitoring system (reading server-side logs with different TZ than orchestra) maybe a settings value? (use UTC internally, timezone.localtime() when interacting with servers)
|
* Timezone awareness on monitoring system (reading server-side logs with different TZ than orchestra) maybe a settings value? (use UTC internally, timezone.localtime() when interacting with servers)
|
||||||
|
|
||||||
* EMAIL backend operations which contain stderr messages (because under certain failures status code is still 0)
|
* EMAIL backend operations which contain stderr messages (because under certain failures status code is still 0)
|
||||||
|
@ -90,8 +88,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
|
|
||||||
* mail backend related_models = ('resources__content_type') ??
|
* mail backend related_models = ('resources__content_type') ??
|
||||||
|
|
||||||
* ignore orders (mark orders as ignored), ignore orchestra related orders by default (or do not generate them on the first place) ignore superuser orders?
|
|
||||||
|
|
||||||
* Domain backend PowerDNS Bind validation support?
|
* Domain backend PowerDNS Bind validation support?
|
||||||
|
|
||||||
* Maildir billing tests/ webdisk billing tests (avg metric)
|
* Maildir billing tests/ webdisk billing tests (avg metric)
|
||||||
|
@ -121,12 +117,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
|
|
||||||
* DOC: Complitely decouples scripts execution, billing, service definition
|
* DOC: Complitely decouples scripts execution, billing, service definition
|
||||||
|
|
||||||
* Create SystemUser on account creation. username=username, is_main=True,
|
|
||||||
* Exclude is_main=True from queryset filter default is_main=False
|
|
||||||
* self referencing group.
|
|
||||||
* Unify all users
|
|
||||||
|
|
||||||
|
|
||||||
* delete main user -> delete account or prevent delete main user
|
* delete main user -> delete account or prevent delete main user
|
||||||
|
|
||||||
* https://blog.flameeyes.eu/2011/01/mostly-unknown-openssh-tricks
|
* https://blog.flameeyes.eu/2011/01/mostly-unknown-openssh-tricks
|
||||||
|
|
|
@ -2,7 +2,7 @@ import sys
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.decorators import action_with_confirmation
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
|
|
||||||
|
@ -99,7 +99,11 @@ def mark_as_unread(modeladmin, request, queryset):
|
||||||
for ticket in queryset:
|
for ticket in queryset:
|
||||||
ticket.mark_as_unread_by(request.user)
|
ticket.mark_as_unread_by(request.user)
|
||||||
modeladmin.log_change(request, ticket, 'Marked as unread')
|
modeladmin.log_change(request, ticket, 'Marked as unread')
|
||||||
msg = _("%s selected tickets have been marked as unread.") % queryset.count()
|
num = len(queryset)
|
||||||
|
msg = ungettext(
|
||||||
|
_("Selected ticket has been marked as unread."),
|
||||||
|
_("%i selected tickets have been marked as unread.") % num,
|
||||||
|
num)
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,7 +113,11 @@ def mark_as_read(modeladmin, request, queryset):
|
||||||
for ticket in queryset:
|
for ticket in queryset:
|
||||||
ticket.mark_as_read_by(request.user)
|
ticket.mark_as_read_by(request.user)
|
||||||
modeladmin.log_change(request, ticket, 'Marked as read')
|
modeladmin.log_change(request, ticket, 'Marked as read')
|
||||||
msg = _("%s selected tickets have been marked as read.") % queryset.count()
|
num = len(queryset)
|
||||||
|
msg = ungettext(
|
||||||
|
_("Selected ticket has been marked as read."),
|
||||||
|
_("%i selected tickets have been marked as read.") % num,
|
||||||
|
num)
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -107,3 +107,32 @@ class BillSelectedOrders(object):
|
||||||
'bills': bills,
|
'bills': bills,
|
||||||
})
|
})
|
||||||
return render(request, self.template, self.context)
|
return render(request, self.template, self.context)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def mark_as_ignored(modeladmin, request, queryset):
|
||||||
|
""" Mark orders as ignored """
|
||||||
|
for order in queryset:
|
||||||
|
order.mark_as_ignored()
|
||||||
|
modeladmin.log_change(request, order, 'Marked as ignored')
|
||||||
|
num = len(queryset)
|
||||||
|
msg = ungettext(
|
||||||
|
_("Selected order has been marked as ignored."),
|
||||||
|
_("%i selected orders have been marked as ignored.") % num,
|
||||||
|
num)
|
||||||
|
modeladmin.message_user(request, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def mark_as_not_ignored(modeladmin, request, queryset):
|
||||||
|
""" Mark orders as ignored """
|
||||||
|
for order in queryset:
|
||||||
|
order.mark_as_not_ignored()
|
||||||
|
modeladmin.log_change(request, order, 'Marked as not ignored')
|
||||||
|
num = len(queryset)
|
||||||
|
msg = ungettext(
|
||||||
|
_("Selected order has been marked as not ignored."),
|
||||||
|
_("%i selected orders have been marked as not ignored.") % num,
|
||||||
|
num)
|
||||||
|
modeladmin.message_user(request, msg)
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,27 @@ from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.admin import ChangeListDefaultFilter
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_link, admin_date
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils.humanize import naturaldate
|
from orchestra.utils.humanize import naturaldate
|
||||||
|
|
||||||
from .actions import BillSelectedOrders
|
from .actions import BillSelectedOrders, mark_as_ignored, mark_as_not_ignored
|
||||||
from .filters import ActiveOrderListFilter, BilledOrderListFilter
|
from .filters import IgnoreOrderListFilter, ActiveOrderListFilter, BilledOrderListFilter
|
||||||
from .models import Order, MetricStorage
|
from .models import Order, MetricStorage
|
||||||
|
|
||||||
|
|
||||||
class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
|
class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'service', 'account_link', 'content_object_link',
|
'id', 'service', 'account_link', 'content_object_link',
|
||||||
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
||||||
)
|
)
|
||||||
list_display_links = ('id', 'service')
|
list_display_links = ('id', 'service')
|
||||||
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
|
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',)
|
||||||
actions = (BillSelectedOrders(),)
|
default_changelist_filters = (
|
||||||
|
('ignore', '0'),
|
||||||
|
)
|
||||||
|
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
|
||||||
date_hierarchy = 'registered_on'
|
date_hierarchy = 'registered_on'
|
||||||
|
|
||||||
content_object_link = admin_link('content_object', order=False)
|
content_object_link = admin_link('content_object', order=False)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,3 +48,40 @@ class BilledOrderListFilter(SimpleListFilter):
|
||||||
Q(billed_until__lt=timezone.now())
|
Q(billed_until__lt=timezone.now())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class IgnoreOrderListFilter(SimpleListFilter):
|
||||||
|
""" Filter Nodes by group according to request.user """
|
||||||
|
title = _("Ignore")
|
||||||
|
parameter_name = 'ignore'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('0', _("Not ignored")),
|
||||||
|
('1', _("Ignored")),
|
||||||
|
('2', _("All")),
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == '0':
|
||||||
|
return queryset.filter(ignore=False)
|
||||||
|
elif self.value() == '1':
|
||||||
|
return queryset.filter(ignore=True)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def choices(self, cl):
|
||||||
|
""" Enable default selection different than All """
|
||||||
|
for lookup, title in self.lookup_choices:
|
||||||
|
title = title._proxy____args[0]
|
||||||
|
selected = self.value() == force_text(lookup)
|
||||||
|
if not selected and title == "Not ignored" and self.value() is None:
|
||||||
|
selected = True
|
||||||
|
# end of workaround
|
||||||
|
yield {
|
||||||
|
'selected': selected,
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.parameter_name: lookup,
|
||||||
|
}, []),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ class OrderQuerySet(models.QuerySet):
|
||||||
Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end))
|
Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end))
|
||||||
)
|
)
|
||||||
ids = self.values_list('id', flat=True)
|
ids = self.values_list('id', flat=True)
|
||||||
return self.model.objects.filter(qs).exclude(id__in=ids)
|
return self.model.objects.filter(qs).exclude(id__in=ids, ignore=True)
|
||||||
|
|
||||||
def pricing_orders(self, ini, end):
|
def pricing_orders(self, ini, end):
|
||||||
return self.filter(billed_until__isnull=False, billed_until__gt=ini,
|
return self.filter(billed_until__isnull=False, billed_until__gt=ini,
|
||||||
|
@ -136,8 +136,12 @@ class Order(models.Model):
|
||||||
if account_id is None:
|
if account_id is None:
|
||||||
# New account workaround -> user.account_id == None
|
# New account workaround -> user.account_id == None
|
||||||
continue
|
continue
|
||||||
order = cls.objects.create(content_object=instance,
|
ignore = False
|
||||||
service=service, account_id=account_id)
|
account = getattr(instance, 'account', instance)
|
||||||
|
if account.is_superuser:
|
||||||
|
ignore = service.ignore_superusers
|
||||||
|
order = cls.objects.create(content_object=instance, service=service,
|
||||||
|
account_id=account_id, ignore=ignore)
|
||||||
logger.info("CREATED new order id: {id}".format(id=order.id))
|
logger.info("CREATED new order id: {id}".format(id=order.id))
|
||||||
else:
|
else:
|
||||||
order = orders.get()
|
order = orders.get()
|
||||||
|
@ -170,6 +174,14 @@ class Order(models.Model):
|
||||||
self.save(update_fields=['cancelled_on'])
|
self.save(update_fields=['cancelled_on'])
|
||||||
logger.info("CANCELLED order id: {id}".format(id=self.id))
|
logger.info("CANCELLED order id: {id}".format(id=self.id))
|
||||||
|
|
||||||
|
def mark_as_ignored(self):
|
||||||
|
self.ignore = True
|
||||||
|
self.save(update_fields=['ignore'])
|
||||||
|
|
||||||
|
def mark_as_not_ignored(self):
|
||||||
|
self.ignore = False
|
||||||
|
self.save(update_fields=['ignore'])
|
||||||
|
|
||||||
def get_metric(self, *args, **kwargs):
|
def get_metric(self, *args, **kwargs):
|
||||||
if kwargs.pop('changes', False):
|
if kwargs.pop('changes', False):
|
||||||
ini, end = args
|
ini, end = args
|
||||||
|
|
|
@ -38,7 +38,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('description', 'content_type', 'match', 'handler_type',
|
'fields': ('description', 'content_type', 'match', 'handler_type',
|
||||||
'is_active')
|
'ignore_superusers', 'is_active')
|
||||||
}),
|
}),
|
||||||
(_("Billing options"), {
|
(_("Billing options"), {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
|
|
|
@ -109,6 +109,8 @@ class Service(models.Model):
|
||||||
"here allow to."),
|
"here allow to."),
|
||||||
choices=ServiceHandler.get_plugin_choices())
|
choices=ServiceHandler.get_plugin_choices())
|
||||||
is_active = models.BooleanField(_("active"), default=True)
|
is_active = models.BooleanField(_("active"), default=True)
|
||||||
|
ignore_superusers = models.BooleanField(_("ignore superusers"), default=True,
|
||||||
|
help_text=_("Designates whether superuser orders are marked as ignored by default or not"))
|
||||||
# Billing
|
# Billing
|
||||||
billing_period = models.CharField(_("billing period"), max_length=16,
|
billing_period = models.CharField(_("billing period"), max_length=16,
|
||||||
help_text=_("Renewal period for recurring invoicing"),
|
help_text=_("Renewal period for recurring invoicing"),
|
||||||
|
@ -126,14 +128,6 @@ class Service(models.Model):
|
||||||
(FIXED_DATE, _("Fixed billing date")),
|
(FIXED_DATE, _("Fixed billing date")),
|
||||||
),
|
),
|
||||||
default=FIXED_DATE)
|
default=FIXED_DATE)
|
||||||
# delayed_billing = models.CharField(_("delayed billing"), max_length=16,
|
|
||||||
# help_text=_("Period in which this service will be ignored for billing"),
|
|
||||||
# choices=(
|
|
||||||
# (NEVER, _("No delay (inmediate billing)")),
|
|
||||||
# (TEN_DAYS, _("Ten days")),
|
|
||||||
# (ONE_MONTH, _("One month")),
|
|
||||||
# ),
|
|
||||||
# default=ONE_MONTH, blank=True)
|
|
||||||
is_fee = models.BooleanField(_("fee"), default=False,
|
is_fee = models.BooleanField(_("fee"), default=False,
|
||||||
help_text=_("Designates whether this service should be billed as "
|
help_text=_("Designates whether this service should be billed as "
|
||||||
" membership fee or not"))
|
" membership fee or not"))
|
||||||
|
@ -161,14 +155,6 @@ class Service(models.Model):
|
||||||
(MATCH_PRICE, _("Match price")),
|
(MATCH_PRICE, _("Match price")),
|
||||||
),
|
),
|
||||||
default=STEP_PRICE)
|
default=STEP_PRICE)
|
||||||
# orders_effect = models.CharField(_("orders effect"), max_length=16,
|
|
||||||
# help_text=_("Defines the lookup behaviour when using orders for "
|
|
||||||
# "the pricing rate computation of this service."),
|
|
||||||
# choices=(
|
|
||||||
# (REGISTER_OR_RENEW, _("Register or renew events")),
|
|
||||||
# (CONCURRENT, _("Active at every given time")),
|
|
||||||
# ),
|
|
||||||
# default=CONCURRENT)
|
|
||||||
on_cancel = models.CharField(_("on cancel"), max_length=16,
|
on_cancel = models.CharField(_("on cancel"), max_length=16,
|
||||||
help_text=_("Defines the cancellation behaviour of this service"),
|
help_text=_("Defines the cancellation behaviour of this service"),
|
||||||
choices=(
|
choices=(
|
||||||
|
@ -178,24 +164,6 @@ class Service(models.Model):
|
||||||
(REFUND, _("Refund")),
|
(REFUND, _("Refund")),
|
||||||
),
|
),
|
||||||
default=DISCOUNT)
|
default=DISCOUNT)
|
||||||
# on_broken_period = models.CharField(_("on broken period", max_length=16,
|
|
||||||
# help_text=_("Defines the billing behaviour when periods are incomplete on register and on cancel"),
|
|
||||||
# choices=(
|
|
||||||
# (NOTHING, _("Nothing, period is atomic")),
|
|
||||||
# (DISCOUNT, _("Bill partially")),
|
|
||||||
# (COMPENSATE, _("Compensate on cancel")),
|
|
||||||
# (REFUND, _("Refund on cancel")),
|
|
||||||
# ),
|
|
||||||
# default=DISCOUNT)
|
|
||||||
# granularity = models.CharField(_("granularity"), max_length=16,
|
|
||||||
# help_text=_("Defines the minimum size a period can be broken into"),
|
|
||||||
# choices=(
|
|
||||||
# (DAILY, _("One day")),
|
|
||||||
# (MONTHLY, _("One month")),
|
|
||||||
# (ANUAL, _("One year")),
|
|
||||||
# ),
|
|
||||||
# default=DAILY,
|
|
||||||
# )
|
|
||||||
payment_style = models.CharField(_("payment style"), max_length=16,
|
payment_style = models.CharField(_("payment style"), max_length=16,
|
||||||
help_text=_("Designates whether this service should be paid after "
|
help_text=_("Designates whether this service should be paid after "
|
||||||
"consumtion (postpay/on demand) or prepaid"),
|
"consumtion (postpay/on demand) or prepaid"),
|
||||||
|
@ -204,24 +172,6 @@ class Service(models.Model):
|
||||||
(POSTPAY, _("Postpay (on demand)")),
|
(POSTPAY, _("Postpay (on demand)")),
|
||||||
),
|
),
|
||||||
default=PREPAY)
|
default=PREPAY)
|
||||||
# trial_period = models.CharField(_("trial period"), max_length=16, blank=True,
|
|
||||||
# help_text=_("Period in which no charge will be issued"),
|
|
||||||
# choices=(
|
|
||||||
# (NEVER, _("No trial")),
|
|
||||||
# (TEN_DAYS, _("Ten days")),
|
|
||||||
# (ONE_MONTH, _("One month")),
|
|
||||||
# ),
|
|
||||||
# default=NEVER)
|
|
||||||
# refund_period = models.CharField(_("refund period"), max_length=16,
|
|
||||||
# help_text=_("Period in which automatic refund will be performed on "
|
|
||||||
# "service cancellation"),
|
|
||||||
# choices=(
|
|
||||||
# (NEVER, _("Never refund")),
|
|
||||||
# (TEN_DAYS, _("Ten days")),
|
|
||||||
# (ONE_MONTH, _("One month")),
|
|
||||||
# (ALWAYS, _("Always refund")),
|
|
||||||
# ),
|
|
||||||
# default=NEVER, blank=True)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.description
|
return self.description
|
||||||
|
|
|
@ -180,10 +180,6 @@ function install_requirements () {
|
||||||
update-locale LANG=en_US.UTF-8
|
update-locale LANG=en_US.UTF-8
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run apt-get update
|
|
||||||
run apt-get install -y $APT
|
|
||||||
run pip install $PIP
|
|
||||||
|
|
||||||
# Install ca certificates
|
# Install ca certificates
|
||||||
if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then
|
if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then
|
||||||
mkdir -p /usr/local/share/ca-certificates/cacert.org
|
mkdir -p /usr/local/share/ca-certificates/cacert.org
|
||||||
|
@ -192,6 +188,11 @@ function install_requirements () {
|
||||||
http://www.cacert.org/certs/class3.crt
|
http://www.cacert.org/certs/class3.crt
|
||||||
update-ca-certificates
|
update-ca-certificates
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
run apt-get update
|
||||||
|
run apt-get install -y $APT
|
||||||
|
run pip install $PIP
|
||||||
|
|
||||||
# Some versions of rabbitmq-server will not start automatically by default unless ...
|
# Some versions of rabbitmq-server will not start automatically by default unless ...
|
||||||
sed -i "s/# Default-Start:.*/# Default-Start: 2 3 4 5/" /etc/init.d/rabbitmq-server
|
sed -i "s/# Default-Start:.*/# Default-Start: 2 3 4 5/" /etc/init.d/rabbitmq-server
|
||||||
sed -i "s/# Default-Stop:.*/# Default-Stop: 0 1 6/" /etc/init.d/rabbitmq-server
|
sed -i "s/# Default-Stop:.*/# Default-Stop: 0 1 6/" /etc/init.d/rabbitmq-server
|
||||||
|
|
Loading…
Reference in New Issue