Improvements in Admin UI
This commit is contained in:
parent
ed0e51b73f
commit
147c1d0dd6
|
@ -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 = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">'
|
||||
onclick = 'onclick="return showAddAnotherPopup(this);"'
|
||||
add_link = '<a href="{add_url}" {onclick}>{img} Add address</a>'.format(
|
||||
add_url=add_url, onclick=onclick, img=img)
|
||||
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
|
||||
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):
|
||||
|
@ -139,6 +134,15 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
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)
|
||||
admin.site.register(Address, AddressAdmin)
|
||||
|
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
||||
capfirst(related.get_type().lower()), change_url(related), related, verb))
|
||||
mark_safe('Transaction: <a href="{}">{}</a> 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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue