Improvements in Admin UI

This commit is contained in:
Marc 2014-10-21 15:29:36 +00:00
parent ed0e51b73f
commit 147c1d0dd6
8 changed files with 145 additions and 53 deletions

View File

@ -1,4 +1,5 @@
import copy import copy
from urlparse import parse_qs
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
@ -25,19 +26,22 @@ class AutoresponseInline(admin.StackedInline):
return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs) return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs)
class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin): class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'name', 'account_link', 'filtering', 'display_addresses' 'name', 'account_link', 'filtering', 'display_addresses'
) )
list_filter = (HasAddressListFilter, 'filtering') list_filter = (HasAddressListFilter, 'filtering')
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
'fields': ('account', 'name', 'password1', 'password2', 'filtering'), 'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'),
}), }),
(_("Custom filtering"), { (_("Custom filtering"), {
'classes': ('collapse',), 'classes': ('collapse',),
'fields': ('custom_filtering',), 'fields': ('custom_filtering',),
}), }),
(_("Addresses"), {
'fields': ('addresses',)
}),
) )
fieldsets = ( fieldsets = (
(None, { (None, {
@ -48,10 +52,10 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
'fields': ('custom_filtering',), 'fields': ('custom_filtering',),
}), }),
(_("Addresses"), { (_("Addresses"), {
'fields': ('addresses_field',) 'fields': ('addresses',)
}), }),
) )
readonly_fields = ('account_link', 'display_addresses', 'addresses_field') readonly_fields = ('account_link', 'display_addresses')
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
add_form = MailboxCreationForm add_form = MailboxCreationForm
form = MailboxChangeForm form = MailboxChangeForm
@ -73,24 +77,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',) fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',)
return fieldsets return fieldsets
def addresses_field(self, mailbox): def get_form(self, *args, **kwargs):
""" Address form field with "Add address" button """ form = super(MailboxAdmin, self).get_form(*args, **kwargs)
account = mailbox.account form.modeladmin = self
add_url = reverse('admin:mailboxes_address_add') return form
add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
img = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">' def save_model(self, request, obj, form, change):
onclick = 'onclick="return showAddAnotherPopup(this);"' """ save hacky mailbox.addresses """
add_link = '<a href="{add_url}" {onclick}>{img} Add address</a>'.format( super(MailboxAdmin, self).save_model(request, obj, form, change)
add_url=add_url, onclick=onclick, img=img) obj.addresses = form.cleaned_data['addresses']
value = '%s<br><br>' % 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 += '<li><a href="%s">%s</a></li>' % (url, name)
value = '<ul>%s</ul>' % value
return mark_safe('<div style="padding-left: 10px;">%s</div>' % value)
addresses_field.short_description = _("Addresses")
addresses_field.allow_tags = True
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
@ -139,6 +134,15 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
qs = super(AddressAdmin, self).get_queryset(request) qs = super(AddressAdmin, self).get_queryset(request)
return qs.select_related('domain') 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) admin.site.register(Mailbox, MailboxAdmin)
admin.site.register(Address, AddressAdmin) admin.site.register(Address, AddressAdmin)

View File

@ -1,10 +1,41 @@
from django import forms 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 django.utils.translation import ugettext_lazy as _
from orchestra.forms import UserCreationForm, UserChangeForm 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): def clean_custom_filtering(self):
filtering = self.cleaned_data['filtering'] filtering = self.cleaned_data['filtering']
custom_filtering = self.cleaned_data['custom_filtering'] custom_filtering = self.cleaned_data['custom_filtering']
@ -13,11 +44,12 @@ class CleanCustomFilteringMixin(object):
return custom_filtering return custom_filtering
class MailboxChangeForm(CleanCustomFilteringMixin, UserChangeForm):
class MailboxChangeForm(UserChangeForm, MailboxForm):
pass pass
class MailboxCreationForm(CleanCustomFilteringMixin, UserCreationForm): class MailboxCreationForm(UserCreationForm, MailboxForm):
def clean_name(self): def clean_name(self):
# Since model.clean() will check this, this is redundant, # 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 # 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) self._meta.model._default_manager.get(name=name)
except self._meta.model.DoesNotExist: except self._meta.model.DoesNotExist:
return name return name
raise forms.ValidationError(self.error_messages['duplicate_name']) raise forms.ValidationError(self.error_messages['duplicate_username'])
class AddressForm(forms.ModelForm): class AddressForm(forms.ModelForm):
def clean(self): def clean(self):
cleaned_data = super(AddressForm, self).clean() 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")) raise forms.ValidationError(_("Mailboxes or forward address should be provided"))

View File

@ -29,9 +29,6 @@ class Mailbox(models.Model):
help_text=_("Arbitrary email filtering in sieve language. " help_text=_("Arbitrary email filtering in sieve language. "
"This overrides any automatic junk email filtering")) "This overrides any automatic junk email filtering"))
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
# addresses = models.ManyToManyField('mailboxes.Address',
# verbose_name=_("addresses"),
# related_name='mailboxes', blank=True)
class Meta: class Meta:
verbose_name_plural = _("mailboxes") verbose_name_plural = _("mailboxes")

View File

@ -15,10 +15,9 @@ from .models import Order, MetricStorage
class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin): class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
list_display = ( 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' 'display_registered_on', 'display_billed_until', 'display_cancelled_on'
) )
list_display_links = ('id', 'service')
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',) list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',)
default_changelist_filters = ( default_changelist_filters = (
('ignore', '0'), ('ignore', '0'),
@ -26,6 +25,7 @@ class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
date_hierarchy = 'registered_on' date_hierarchy = 'registered_on'
service_link = admin_link('service')
content_object_link = admin_link('content_object', order=False) content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on') display_registered_on = admin_date('registered_on')
display_cancelled_on = admin_date('cancelled_on') display_cancelled_on = admin_date('cancelled_on')

View File

@ -69,18 +69,24 @@ class OrderQuerySet(models.QuerySet):
for service, orders in services.iteritems(): for service, orders in services.iteritems():
if not service.rates.exists(): if not service.rates.exists():
continue continue
ini = datetime.date.max
end = datetime.date.min end = datetime.date.min
bp = None bp = None
for order in orders: for order in orders:
bp = service.handler.get_billing_point(order, **options) bp = service.handler.get_billing_point(order, **options)
end = max(end, bp) end = max(end, bp)
# FIXME exclude cancelled except cancelled and billed > ini ini = min(ini, order.billed_until or order.registered_on)
qs = qs | Q( qs |= Q(
Q(service=service, account=account_id, registered_on__lt=end) & Q(service=service, account=account_id, registered_on__lt=end) & Q(
Q(Q(billed_until__isnull=True) | Q(billed_until__lt=end)) 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) 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): 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,

View File

@ -5,7 +5,7 @@ from django.db import transaction
from django.shortcuts import render from django.shortcuts import render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import capfirst 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.decorators import action_with_confirmation
from orchestra.admin.utils import change_url from orchestra.admin.utils import change_url
@ -47,7 +47,11 @@ def mark_as_executed(modeladmin, request, queryset, extra_context={}):
for trans in queryset: for trans in queryset:
trans.mark_as_executed() trans.mark_as_executed()
modeladmin.log_change(request, trans, _("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) modeladmin.message_user(request, msg)
mark_as_executed.url_name = 'execute' mark_as_executed.url_name = 'execute'
mark_as_executed.verbose_name = _("Mark as executed") mark_as_executed.verbose_name = _("Mark as executed")
@ -59,7 +63,11 @@ def mark_as_secured(modeladmin, request, queryset):
for trans in queryset: for trans in queryset:
trans.mark_as_secured() trans.mark_as_secured()
modeladmin.log_change(request, trans, _("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) modeladmin.message_user(request, msg)
mark_as_secured.url_name = 'secure' mark_as_secured.url_name = 'secure'
mark_as_secured.verbose_name = _("Mark as secured") mark_as_secured.verbose_name = _("Mark as secured")
@ -71,7 +79,11 @@ def mark_as_rejected(modeladmin, request, queryset):
for trans in queryset: for trans in queryset:
trans.mark_as_rejected() trans.mark_as_rejected()
modeladmin.log_change(request, trans, _("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) modeladmin.message_user(request, msg)
mark_as_rejected.url_name = 'reject' mark_as_rejected.url_name = 'reject'
mark_as_rejected.verbose_name = _("Mark as rejected") mark_as_rejected.verbose_name = _("Mark as rejected")
@ -89,8 +101,8 @@ def _format_display_objects(modeladmin, request, queryset, related):
attr, verb = related attr, verb = related
for related in getattr(obj.transactions, attr)(): for related in getattr(obj.transactions, attr)():
subobjects.append( subobjects.append(
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format( mark_safe('Transaction: <a href="{}">{}</a> will be marked as {}'.format(
capfirst(related.get_type().lower()), change_url(related), related, verb)) change_url(related), related, verb))
) )
objects.append(subobjects) objects.append(subobjects)
return {'display_objects': objects} return {'display_objects': objects}
@ -106,7 +118,11 @@ def mark_process_as_executed(modeladmin, request, queryset):
for process in queryset: for process in queryset:
process.mark_as_executed() process.mark_as_executed()
modeladmin.log_change(request, process, _("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) modeladmin.message_user(request, msg)
mark_process_as_executed.url_name = 'executed' mark_process_as_executed.url_name = 'executed'
mark_process_as_executed.verbose_name = _("Mark as executed") mark_process_as_executed.verbose_name = _("Mark as executed")
@ -118,7 +134,11 @@ def abort(modeladmin, request, queryset):
for process in queryset: for process in queryset:
process.abort() process.abort()
modeladmin.log_change(request, process, _("Aborted")) 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) modeladmin.message_user(request, msg)
abort.url_name = 'abort' abort.url_name = 'abort'
abort.verbose_name = _("Abort") abort.verbose_name = _("Abort")
@ -130,7 +150,11 @@ def commit(modeladmin, request, queryset):
for trans in queryset: for trans in queryset:
trans.mark_as_rejected() trans.mark_as_rejected()
modeladmin.log_change(request, trans, _("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) modeladmin.message_user(request, msg)
commit.url_name = 'commit' commit.url_name = 'commit'
commit.verbose_name = _("Commit") commit.verbose_name = _("Commit")

View File

@ -2,9 +2,9 @@ from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ 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.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 . import actions
from .methods import PaymentMethod from .methods import PaymentMethod
@ -51,19 +51,47 @@ class TransactionInline(admin.TabularInline):
return False return False
class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin): class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'id', 'bill_link', 'account_link', 'source_link', 'display_state',
'amount', 'process_link' 'amount', 'process_link'
) )
list_filter = ('source__method', 'state') 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 = (
actions.process_transactions, actions.mark_as_executed, actions.process_transactions, actions.mark_as_executed,
actions.mark_as_secured, actions.mark_as_rejected actions.mark_as_secured, actions.mark_as_rejected
) )
change_view_actions = actions change_view_actions = actions
filter_by_account_fields = ['source'] filter_by_account_fields = ('bill', 'source')
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link') change_readonly_fields = ('amount', 'currency')
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
bill_link = admin_link('bill') bill_link = admin_link('bill')
source_link = admin_link('source') source_link = admin_link('source')
@ -93,7 +121,7 @@ class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdm
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'created_at') list_display = ('id', 'file_url', 'display_transactions', 'created_at')
fields = ('data', 'file_url', '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] inlines = [TransactionInline]
actions = (actions.mark_process_as_executed, actions.abort, actions.commit) actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
change_view_actions = actions change_view_actions = actions

View File

@ -41,7 +41,7 @@ class ContractedPlan(models.Model):
return str(self.plan) return str(self.plan)
def clean(self): 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(): if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
raise ValidationError("A contracted plan for this account already exists") raise ValidationError("A contracted plan for this account already exists")