diff --git a/TODO.md b/TODO.md index 8aa6b8d7..aa42b62e 100644 --- a/TODO.md +++ b/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 * 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) * 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') ?? -* 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? * 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 -* 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 * https://blog.flameeyes.eu/2011/01/mostly-unknown-openssh-tricks diff --git a/orchestra/apps/issues/actions.py b/orchestra/apps/issues/actions.py index f119bc3e..9bf73d20 100644 --- a/orchestra/apps/issues/actions.py +++ b/orchestra/apps/issues/actions.py @@ -2,7 +2,7 @@ import sys from django.contrib import messages 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 @@ -99,7 +99,11 @@ def mark_as_unread(modeladmin, request, queryset): for ticket in queryset: ticket.mark_as_unread_by(request.user) 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) @@ -109,7 +113,11 @@ def mark_as_read(modeladmin, request, queryset): for ticket in queryset: ticket.mark_as_read_by(request.user) 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) diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py index f9797efa..31a3a084 100644 --- a/orchestra/apps/orders/actions.py +++ b/orchestra/apps/orders/actions.py @@ -107,3 +107,32 @@ class BillSelectedOrders(object): 'bills': bills, }) 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) + diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index 368d7f46..ace28745 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -3,23 +3,27 @@ from django.utils import timezone from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ +from orchestra.admin import ChangeListDefaultFilter from orchestra.admin.utils import admin_link, admin_date from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.utils.humanize import naturaldate -from .actions import BillSelectedOrders -from .filters import ActiveOrderListFilter, BilledOrderListFilter +from .actions import BillSelectedOrders, mark_as_ignored, mark_as_not_ignored +from .filters import IgnoreOrderListFilter, ActiveOrderListFilter, BilledOrderListFilter from .models import Order, MetricStorage -class OrderAdmin(AccountAdminMixin, admin.ModelAdmin): +class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin): list_display = ( 'id', 'service', 'account_link', 'content_object_link', 'display_registered_on', 'display_billed_until', 'display_cancelled_on' ) list_display_links = ('id', 'service') - list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',) - actions = (BillSelectedOrders(),) + list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',) + default_changelist_filters = ( + ('ignore', '0'), + ) + actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) date_hierarchy = 'registered_on' content_object_link = admin_link('content_object', order=False) diff --git a/orchestra/apps/orders/filters.py b/orchestra/apps/orders/filters.py index 303d0538..9198a3c7 100644 --- a/orchestra/apps/orders/filters.py +++ b/orchestra/apps/orders/filters.py @@ -1,6 +1,7 @@ from django.contrib.admin import SimpleListFilter from django.db.models import Q from django.utils import timezone +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -47,3 +48,40 @@ class BilledOrderListFilter(SimpleListFilter): Q(billed_until__lt=timezone.now()) ) 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, + } diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 17d01cc3..3b39c66f 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -77,7 +77,7 @@ class OrderQuerySet(models.QuerySet): Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end)) ) 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): return self.filter(billed_until__isnull=False, billed_until__gt=ini, @@ -136,8 +136,12 @@ class Order(models.Model): if account_id is None: # New account workaround -> user.account_id == None continue - order = cls.objects.create(content_object=instance, - service=service, account_id=account_id) + ignore = False + 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)) else: order = orders.get() @@ -170,6 +174,14 @@ class Order(models.Model): self.save(update_fields=['cancelled_on']) 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): if kwargs.pop('changes', False): ini, end = args diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py index 3334b5c9..811769fa 100644 --- a/orchestra/apps/services/admin.py +++ b/orchestra/apps/services/admin.py @@ -38,7 +38,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): (None, { 'classes': ('wide',), 'fields': ('description', 'content_type', 'match', 'handler_type', - 'is_active') + 'ignore_superusers', 'is_active') }), (_("Billing options"), { 'classes': ('wide',), diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 9e3c5182..5522de52 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -109,6 +109,8 @@ class Service(models.Model): "here allow to."), choices=ServiceHandler.get_plugin_choices()) 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_period = models.CharField(_("billing period"), max_length=16, help_text=_("Renewal period for recurring invoicing"), @@ -126,14 +128,6 @@ class Service(models.Model): (FIXED_DATE, _("Fixed billing 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, help_text=_("Designates whether this service should be billed as " " membership fee or not")) @@ -161,14 +155,6 @@ class Service(models.Model): (MATCH_PRICE, _("Match 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, help_text=_("Defines the cancellation behaviour of this service"), choices=( @@ -178,24 +164,6 @@ class Service(models.Model): (REFUND, _("Refund")), ), 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, help_text=_("Designates whether this service should be paid after " "consumtion (postpay/on demand) or prepaid"), @@ -204,24 +172,6 @@ class Service(models.Model): (POSTPAY, _("Postpay (on demand)")), ), 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): return self.description diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index 7efc5b22..1364d16d 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -180,10 +180,6 @@ function install_requirements () { update-locale LANG=en_US.UTF-8 fi - run apt-get update - run apt-get install -y $APT - run pip install $PIP - # Install ca certificates if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then mkdir -p /usr/local/share/ca-certificates/cacert.org @@ -192,6 +188,11 @@ function install_requirements () { http://www.cacert.org/certs/class3.crt update-ca-certificates 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 ... 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