From 147c1d0dd6f4227292956aac8ca6a844ee90c67f Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 21 Oct 2014 15:29:36 +0000 Subject: [PATCH] Improvements in Admin UI --- orchestra/apps/mailboxes/admin.py | 48 ++++++++++++++++-------------- orchestra/apps/mailboxes/forms.py | 43 ++++++++++++++++++++++---- orchestra/apps/mailboxes/models.py | 3 -- orchestra/apps/orders/admin.py | 4 +-- orchestra/apps/orders/models.py | 16 ++++++---- orchestra/apps/payments/actions.py | 42 ++++++++++++++++++++------ orchestra/apps/payments/admin.py | 40 +++++++++++++++++++++---- orchestra/apps/services/models.py | 2 +- 8 files changed, 145 insertions(+), 53 deletions(-) diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py index 7e4e663e..209110b6 100644 --- a/orchestra/apps/mailboxes/admin.py +++ b/orchestra/apps/mailboxes/admin.py @@ -1,4 +1,5 @@ import copy +from urlparse import parse_qs from django import forms from django.contrib import admin @@ -25,19 +26,22 @@ class AutoresponseInline(admin.StackedInline): return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs) -class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin): +class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): list_display = ( 'name', 'account_link', 'filtering', 'display_addresses' ) list_filter = (HasAddressListFilter, 'filtering') add_fieldsets = ( (None, { - 'fields': ('account', 'name', 'password1', 'password2', 'filtering'), + 'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'), }), (_("Custom filtering"), { 'classes': ('collapse',), 'fields': ('custom_filtering',), }), + (_("Addresses"), { + 'fields': ('addresses',) + }), ) fieldsets = ( (None, { @@ -48,10 +52,10 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm 'fields': ('custom_filtering',), }), (_("Addresses"), { - 'fields': ('addresses_field',) + 'fields': ('addresses',) }), ) - readonly_fields = ('account_link', 'display_addresses', 'addresses_field') + readonly_fields = ('account_link', 'display_addresses') change_readonly_fields = ('name',) add_form = MailboxCreationForm form = MailboxChangeForm @@ -73,24 +77,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',) return fieldsets - def addresses_field(self, mailbox): - """ Address form field with "Add address" button """ - account = mailbox.account - add_url = reverse('admin:mailboxes_address_add') - add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk) - img = 'Add Another' - onclick = 'onclick="return showAddAnotherPopup(this);"' - add_link = '{img} Add address'.format( - add_url=add_url, onclick=onclick, img=img) - value = '%s

' % add_link - for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'): - url = reverse('admin:mailboxes_address_change', args=(pk,)) - name = '%s@%s' % (name, domain) - value += '
  • %s
  • ' % (url, name) - value = '' % value - return mark_safe('
    %s
    ' % value) - addresses_field.short_description = _("Addresses") - addresses_field.allow_tags = True + def get_form(self, *args, **kwargs): + form = super(MailboxAdmin, self).get_form(*args, **kwargs) + form.modeladmin = self + return form + + def save_model(self, request, obj, form, change): + """ save hacky mailbox.addresses """ + super(MailboxAdmin, self).save_model(request, obj, form, change) + obj.addresses = form.cleaned_data['addresses'] class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): @@ -138,6 +133,15 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): """ Select related for performance """ qs = super(AddressAdmin, self).get_queryset(request) return qs.select_related('domain') + + def get_fields(self, request, obj=None): + """ Remove mailboxes field when creating address from a popup i.e. from mailbox add form """ + fields = super(AddressAdmin, self).get_fields(request, obj=obj) + if '_to_field' in parse_qs(request.META['QUERY_STRING']): + # Add address popup + fields = list(fields) + fields.remove('mailboxes') + return fields admin.site.register(Mailbox, MailboxAdmin) diff --git a/orchestra/apps/mailboxes/forms.py b/orchestra/apps/mailboxes/forms.py index f05bb145..7d9c376e 100644 --- a/orchestra/apps/mailboxes/forms.py +++ b/orchestra/apps/mailboxes/forms.py @@ -1,10 +1,41 @@ from django import forms +from django.contrib.admin import widgets +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.forms import UserCreationForm, UserChangeForm +from orchestra.utils.python import AttrDict + +from .models import Address, Mailbox -class CleanCustomFilteringMixin(object): +class MailboxForm(forms.ModelForm): + """ hacky form for adding reverse M2M form field for Mailbox.addresses """ + addresses = forms.ModelMultipleChoiceField(queryset=Address.objects, required=False, + widget=widgets.FilteredSelectMultiple(verbose_name=_('Pizzas'), is_stacked=False)) + + def __init__(self, *args, **kwargs): + super(MailboxForm, self).__init__(*args, **kwargs) + field = AttrDict(**{ + 'to': Address, + 'get_related_field': lambda: AttrDict(name='id'), + }) + widget = self.fields['addresses'].widget + self.fields['addresses'].widget = widgets.RelatedFieldWidgetWrapper(widget, field, + self.modeladmin.admin_site, can_add_related=True) + old_render = self.fields['addresses'].widget.render + def render(*args, **kwargs): + output = old_render(*args, **kwargs) + args = 'account=%i' % self.modeladmin.account.pk + output = output.replace('/add/?', '/add/?%s&' % args) + return mark_safe(output) + self.fields['addresses'].widget.render = render + queryset = self.fields['addresses'].queryset + self.fields['addresses'].queryset = queryset.filter(account=self.modeladmin.account.pk) + + if self.instance and self.instance.pk: + self.fields['addresses'].initial = self.instance.addresses.all() + def clean_custom_filtering(self): filtering = self.cleaned_data['filtering'] custom_filtering = self.cleaned_data['custom_filtering'] @@ -13,11 +44,12 @@ class CleanCustomFilteringMixin(object): return custom_filtering -class MailboxChangeForm(CleanCustomFilteringMixin, UserChangeForm): + +class MailboxChangeForm(UserChangeForm, MailboxForm): pass -class MailboxCreationForm(CleanCustomFilteringMixin, UserCreationForm): +class MailboxCreationForm(UserCreationForm, MailboxForm): def clean_name(self): # Since model.clean() will check this, this is redundant, # but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth @@ -26,11 +58,12 @@ class MailboxCreationForm(CleanCustomFilteringMixin, UserCreationForm): self._meta.model._default_manager.get(name=name) except self._meta.model.DoesNotExist: return name - raise forms.ValidationError(self.error_messages['duplicate_name']) + raise forms.ValidationError(self.error_messages['duplicate_username']) class AddressForm(forms.ModelForm): def clean(self): cleaned_data = super(AddressForm, self).clean() - if not cleaned_data['mailboxes'] and not cleaned_data['forward']: + if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']: raise forms.ValidationError(_("Mailboxes or forward address should be provided")) + diff --git a/orchestra/apps/mailboxes/models.py b/orchestra/apps/mailboxes/models.py index d183cd9e..05bcc208 100644 --- a/orchestra/apps/mailboxes/models.py +++ b/orchestra/apps/mailboxes/models.py @@ -29,9 +29,6 @@ class Mailbox(models.Model): help_text=_("Arbitrary email filtering in sieve language. " "This overrides any automatic junk email filtering")) is_active = models.BooleanField(_("active"), default=True) -# addresses = models.ManyToManyField('mailboxes.Address', -# verbose_name=_("addresses"), -# related_name='mailboxes', blank=True) class Meta: verbose_name_plural = _("mailboxes") diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index ace28745..bc77ec53 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -15,10 +15,9 @@ from .models import Order, MetricStorage class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin): list_display = ( - 'id', 'service', 'account_link', 'content_object_link', + 'id', 'service_link', 'account_link', 'content_object_link', 'display_registered_on', 'display_billed_until', 'display_cancelled_on' ) - list_display_links = ('id', 'service') list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',) default_changelist_filters = ( ('ignore', '0'), @@ -26,6 +25,7 @@ class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin): actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) date_hierarchy = 'registered_on' + service_link = admin_link('service') content_object_link = admin_link('content_object', order=False) display_registered_on = admin_date('registered_on') display_cancelled_on = admin_date('cancelled_on') diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 88821a98..38994cf7 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -69,18 +69,24 @@ class OrderQuerySet(models.QuerySet): for service, orders in services.iteritems(): if not service.rates.exists(): continue + ini = datetime.date.max end = datetime.date.min bp = None for order in orders: bp = service.handler.get_billing_point(order, **options) end = max(end, bp) - # FIXME exclude cancelled except cancelled and billed > ini - qs = qs | Q( - Q(service=service, account=account_id, registered_on__lt=end) & - Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end)) + ini = min(ini, order.billed_until or order.registered_on) + qs |= Q( + Q(service=service, account=account_id, registered_on__lt=end) & Q( + Q(billed_until__isnull=True) | Q(billed_until__lt=end) + ) & Q( + Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini) + ) ) + if not qs: + return self.model.objects.none() ids = self.values_list('id', flat=True) - return self.model.objects.filter(qs).exclude(id__in=ids, ignore=True) + return self.model.objects.filter(qs).exclude(id__in=ids) def pricing_orders(self, ini, end): return self.filter(billed_until__isnull=False, billed_until__gt=ini, diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py index b29b191c..9d702895 100644 --- a/orchestra/apps/payments/actions.py +++ b/orchestra/apps/payments/actions.py @@ -5,7 +5,7 @@ from django.db import transaction from django.shortcuts import render from django.utils.safestring import mark_safe from django.utils.text import capfirst -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.utils import change_url @@ -47,7 +47,11 @@ def mark_as_executed(modeladmin, request, queryset, extra_context={}): for trans in queryset: trans.mark_as_executed() modeladmin.log_change(request, trans, _("Executed")) - msg = _("%s selected transactions have been marked as executed.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected transaction has been marked as executed."), + _("%s selected transactions have been marked as executed.") % num, + num) modeladmin.message_user(request, msg) mark_as_executed.url_name = 'execute' mark_as_executed.verbose_name = _("Mark as executed") @@ -59,7 +63,11 @@ def mark_as_secured(modeladmin, request, queryset): for trans in queryset: trans.mark_as_secured() modeladmin.log_change(request, trans, _("Secured")) - msg = _("%s selected transactions have been marked as secured.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected transaction has been marked as secured."), + _("%s selected transactions have been marked as secured.") % num, + num) modeladmin.message_user(request, msg) mark_as_secured.url_name = 'secure' mark_as_secured.verbose_name = _("Mark as secured") @@ -71,7 +79,11 @@ def mark_as_rejected(modeladmin, request, queryset): for trans in queryset: trans.mark_as_rejected() modeladmin.log_change(request, trans, _("Rejected")) - msg = _("%s selected transactions have been marked as rejected.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected transaction has been marked as rejected."), + _("%s selected transactions have been marked as rejected.") % num, + num) modeladmin.message_user(request, msg) mark_as_rejected.url_name = 'reject' mark_as_rejected.verbose_name = _("Mark as rejected") @@ -89,8 +101,8 @@ def _format_display_objects(modeladmin, request, queryset, related): attr, verb = related for related in getattr(obj.transactions, attr)(): subobjects.append( - mark_safe('{0}: {2} will be marked as {3}'.format( - capfirst(related.get_type().lower()), change_url(related), related, verb)) + mark_safe('Transaction: {} will be marked as {}'.format( + change_url(related), related, verb)) ) objects.append(subobjects) return {'display_objects': objects} @@ -106,7 +118,11 @@ def mark_process_as_executed(modeladmin, request, queryset): for process in queryset: process.mark_as_executed() modeladmin.log_change(request, process, _("Executed")) - msg = _("%s selected processes have been marked as executed.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected process has been marked as executed."), + _("%s selected processes have been marked as executed.") % num, + num) modeladmin.message_user(request, msg) mark_process_as_executed.url_name = 'executed' mark_process_as_executed.verbose_name = _("Mark as executed") @@ -118,7 +134,11 @@ def abort(modeladmin, request, queryset): for process in queryset: process.abort() modeladmin.log_change(request, process, _("Aborted")) - msg = _("%s selected processes have been aborted.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected process has been aborted."), + _("%s selected processes have been aborted.") % num, + num) modeladmin.message_user(request, msg) abort.url_name = 'abort' abort.verbose_name = _("Abort") @@ -130,7 +150,11 @@ def commit(modeladmin, request, queryset): for trans in queryset: trans.mark_as_rejected() modeladmin.log_change(request, trans, _("Rejected")) - msg = _("%s selected transactions have been marked as rejected.") % queryset.count() + num = len(queryset) + msg = ungettext( + _("One selected transaction has been marked as rejected."), + _("%s selected transactions have been marked as rejected.") % num, + num) modeladmin.message_user(request, msg) commit.url_name = 'commit' commit.verbose_name = _("Commit") diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 843f248f..f5eb98ef 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -2,9 +2,9 @@ from django.contrib import admin from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ -from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin +from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin, ExtendedModelAdmin from orchestra.admin.utils import admin_colored, admin_link -from orchestra.apps.accounts.admin import AccountAdminMixin +from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from . import actions from .methods import PaymentMethod @@ -51,19 +51,47 @@ class TransactionInline(admin.TabularInline): return False -class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin): +class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): list_display = ( 'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'amount', 'process_link' ) list_filter = ('source__method', 'state') + fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ( + 'account_link', + 'bill_link', + 'source_link', + 'display_state', + 'amount', + 'currency', + 'process_link' + ) + }), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ( + 'bill', + 'source', + 'display_state', + 'amount', + 'currency', + 'process' + ) + }), + ) actions = ( actions.process_transactions, actions.mark_as_executed, actions.mark_as_secured, actions.mark_as_rejected ) change_view_actions = actions - filter_by_account_fields = ['source'] - readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link') + filter_by_account_fields = ('bill', 'source') + change_readonly_fields = ('amount', 'currency') + readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link') bill_link = admin_link('bill') source_link = admin_link('source') @@ -93,7 +121,7 @@ class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdm class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): list_display = ('id', 'file_url', 'display_transactions', 'created_at') fields = ('data', 'file_url', 'created_at') - readonly_fields = ('file_url', 'display_transactions', 'created_at') + readonly_fields = ('data', 'file_url', 'display_transactions', 'created_at') inlines = [TransactionInline] actions = (actions.mark_process_as_executed, actions.abort, actions.commit) change_view_actions = actions diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 3f11d9f7..256ef131 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -41,7 +41,7 @@ class ContractedPlan(models.Model): return str(self.plan) def clean(self): - if not self.pk and not self.plan.allow_multipls: + if not self.pk and not self.plan.allow_multiples: if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): raise ValidationError("A contracted plan for this account already exists")