Preliminar implementation of SEPA payment system

This commit is contained in:
Marc 2014-07-29 14:29:59 +00:00
parent bef7af084b
commit 30cc1d9922
14 changed files with 333 additions and 29 deletions

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib import admin from django.contrib import admin
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from .utils import set_default_filter from .utils import set_url_query
class ExtendedModelAdmin(admin.ModelAdmin): class ExtendedModelAdmin(admin.ModelAdmin):
@ -55,9 +55,9 @@ class ChangeListDefaultFilter(object):
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
""" Default filter as 'my_nodes=True' """ """ Default filter as 'my_nodes=True' """
defaults = [] defaults = []
for queryarg, value in self.default_changelist_filters: for key, value in self.default_changelist_filters:
set_default_filter(queryarg, request, value) set_url_query(request, key, value)
defaults.append(queryarg) defaults.append(key)
# hack response cl context in order to hook default filter awaearness into search_form.html template # hack response cl context in order to hook default filter awaearness into search_form.html template
response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context) response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context)
if hasattr(response, 'context_data') and 'cl' in response.context_data: if hasattr(response, 'context_data') and 'cl' in response.context_data:

View File

@ -63,13 +63,13 @@ def wrap_admin_view(modeladmin, view):
return update_wrapper(wrapper, view) return update_wrapper(wrapper, view)
def set_default_filter(queryarg, request, value): def set_url_query(request, key, value):
""" set default filters for changelist_view """ """ set default filters for changelist_view """
if queryarg not in request.GET: if key not in request.GET:
request_copy = request.GET.copy() request_copy = request.GET.copy()
if callable(value): if callable(value):
value = value(request) value = value(request)
request_copy[queryarg] = value request_copy[key] = value
request.GET = request_copy request.GET = request_copy
request.META['QUERY_STRING'] = request.GET.urlencode() request.META['QUERY_STRING'] = request.GET.urlencode()

View File

@ -5,10 +5,11 @@ from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.parse import parse_qsl
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import wrap_admin_view, admin_link from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query
from orchestra.core import services, accounts from orchestra.core import services, accounts
from .filters import HasMainUserListFilter from .filters import HasMainUserListFilter
@ -129,6 +130,8 @@ class AccountAdminMixin(object):
""" Provide basic account support to ModelAdmin and AdminInline classes """ """ Provide basic account support to ModelAdmin and AdminInline classes """
readonly_fields = ('account_link',) readonly_fields = ('account_link',)
filter_by_account_fields = [] filter_by_account_fields = []
change_list_template = 'admin/accounts/account/change_list.html'
change_form_template = 'admin/accounts/account/change_form.html'
def account_link(self, instance): def account_link(self, instance):
account = instance.account if instance.pk else self.account account = instance.account if instance.pk else self.account
@ -162,6 +165,48 @@ class AccountAdminMixin(object):
formfield.queryset = formfield.queryset.filter(account=self.account) formfield.queryset = formfield.queryset.filter(account=self.account)
return formfield return formfield
def get_account_from_preserve_filters(self, request):
preserved_filters = self.get_preserved_filters(request)
preserved_filters = dict(parse_qsl(preserved_filters))
cl_filters = preserved_filters.get('_changelist_filters')
if cl_filters:
return dict(parse_qsl(cl_filters)).get('account')
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
account_id = self.get_account_from_preserve_filters(request)
verb = 'change' if object_id else 'add'
if not object_id:
if account_id:
# Preselect account
set_url_query(request, 'account', account_id)
context = {
'from_account': bool(account_id),
'account': not account_id or Account.objects.get(pk=account_id),
'account_opts': Account._meta,
}
context.update(extra_context or {})
return super(AccountAdminMixin, self).changeform_view(request,
object_id=object_id, form_url=form_url, extra_context=context)
def changelist_view(self, request, extra_context=None):
account_id = request.GET.get('account')
context = {
'from_account': False
}
if account_id:
opts = self.model._meta
account = Account.objects.get(pk=account_id)
context = {
'from_account': True,
'title': _("Select %s to change for %s") % (
opts.verbose_name, account.name),
'account': not account_id or Account.objects.get(pk=account_id),
'account_opts': Account._meta,
}
context.update(extra_context or {})
return super(AccountAdminMixin, self).changelist_view(request,
extra_context=context)
class SelectAccountAdminMixin(AccountAdminMixin): class SelectAccountAdminMixin(AccountAdminMixin):
""" Provides support for accounts on ModelAdmin """ """ Provides support for accounts on ModelAdmin """
@ -196,14 +241,21 @@ class SelectAccountAdminMixin(AccountAdminMixin):
def add_view(self, request, form_url='', extra_context=None): def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """ """ Redirects to select account view if required """
if request.user.is_superuser: if request.user.is_superuser:
if 'account' in request.GET or Account.objects.count() == 1: from_account_id = self.get_account_from_preserve_filters(request)
if from_account_id:
set_url_query(request, 'account', from_account_id)
account_id = request.GET.get('account')
if account_id or Account.objects.count() == 1:
kwargs = {} kwargs = {}
if 'account' in request.GET: if account_id:
kwargs = dict(pk=request.GET['account']) kwargs = dict(pk=account_id)
self.account = Account.objects.get(**kwargs) self.account = Account.objects.get(**kwargs)
opts = self.model._meta opts = self.model._meta
context = { context = {
'title': _("Add %s for %s") % (opts.verbose_name, self.account.name) 'title': _("Add %s for %s") % (opts.verbose_name, self.account.name),
'from_account': bool(from_account_id),
'account': self.account,
'account_opts': Account._meta,
} }
context.update(extra_context or {}) context.update(extra_context or {})
return super(AccountAdminMixin, self).add_view(request, return super(AccountAdminMixin, self).add_view(request,

View File

@ -2,18 +2,34 @@
{% load i18n admin_urls admin_static admin_modify %} {% load i18n admin_urls admin_static admin_modify %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
{% if from_account %}
&rsaquo; <a href="{% url 'admin:app_list' app_label=account_opts.app_label %}">{{ account_opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url account_opts|admin_urlname:'changelist' %}">{{ account_opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url account_opts|admin_urlname:'change' account.pk|admin_urlquote %}">{{ account|truncatewords:"18" }}</a>
&rsaquo; {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}?account={{ account.pk }}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
{% else %}
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
{% endif %}
&rsaquo; {% if add %}{% trans 'Add' %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endblock %}
{% block object-tools-items %} {% block object-tools-items %}
{% if services %}
{% for service in services %} {% for service in services %}
<li> <li>
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a> <a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5> <h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
{% endif %}
{% if accounts %}
<ul class="object-tools"> <ul class="object-tools">
{% for account in accounts %} {% for account in accounts %}
<li> <li>
@ -21,10 +37,9 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</p>
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5> <h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
<ul class="object-tools"> <ul class="object-tools">
{% endif %}
<li> <li>
<a href="disable/" class="historylink">{% trans "Disable" %}</a> <a href="disable/" class="historylink">{% trans "Disable" %}</a>
</li> </li>
@ -32,6 +47,5 @@
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a> <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
</li> </li>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
{% if from_account %}
&rsaquo; <a href="{% url 'admin:app_list' app_label=account_opts.app_label %}">{{ account_opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url account_opts|admin_urlname:'changelist' %}">{{ account_opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url account_opts|admin_urlname:'change' account.pk|admin_urlquote %}">{{ account|truncatewords:"18" }}</a>
{% else %}
&rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
{% endif %}
&rsaquo; {{ cl.opts.verbose_name_plural|capfirst }}
</div>
{% endblock %}
{% block object-tools-items %}
{% if from_account %}
<li>
<a href="./" class="historylink">{% trans 'Show all' %}</a>
</li>
{% endif %}
<li>
{% url cl.opts|admin_urlname:'add' as add_url %}
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
{% endblock %}

View File

@ -12,9 +12,16 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
class BillLineInline(admin.TabularInline): class BillLineInline(admin.TabularInline):
model = BillLine model = BillLine
fields = (
'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
)
class BudgetLineInline(admin.TabularInline): class BudgetLineInline(admin.TabularInline):
model = Budget model = Budget
fields = (
'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
)
class BillAdmin(AccountAdminMixin, admin.ModelAdmin): class BillAdmin(AccountAdminMixin, admin.ModelAdmin):

View File

@ -1,10 +1,11 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import AtLeastOneRequiredInlineFormSet from orchestra.admin import AtLeastOneRequiredInlineFormSet
from orchestra.admin.utils import insertattr from orchestra.admin.utils import insertattr
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from .filters import HasInvoiceContactListFilter from .filters import HasInvoiceContactListFilter
from .models import Contact, InvoiceContact from .models import Contact, InvoiceContact
@ -19,6 +20,50 @@ class ContactAdmin(AccountAdminMixin, admin.ModelAdmin):
'contact__user__username', 'short_name', 'full_name', 'phone', 'phone2', 'contact__user__username', 'short_name', 'full_name', 'phone', 'phone2',
'email' 'email'
) )
fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('account_link', 'short_name', 'full_name')
}),
(_("Email"), {
'classes': ('wide',),
'fields': ('email', 'email_usage',)
}),
(_("Phone"), {
'classes': ('wide',),
'fields': ('phone', 'phone2'),
}),
(_("Postal address"), {
'classes': ('wide',),
'fields': ('address', ('zipcode', 'city'), 'country')
}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('account', 'short_name', 'full_name')
}),
(_("Email"), {
'classes': ('wide',),
'fields': ('email', 'email_usage',)
}),
(_("Phone"), {
'classes': ('wide',),
'fields': ('phone', 'phone_alternative'),
}),
(_("Postal address"), {
'classes': ('wide',),
'fields': ('address', ('zip_code', 'city'), 'country')
}),
)
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
admin.site.register(Contact, ContactAdmin) admin.site.register(Contact, ContactAdmin)
@ -32,6 +77,8 @@ class InvoiceContactInline(admin.StackedInline):
""" Make value input widget bigger """ """ Make value input widget bigger """
if db_field.name == 'address': if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
return super(InvoiceContactInline, self).formfield_for_dbfield(db_field, **kwargs) return super(InvoiceContactInline, self).formfield_for_dbfield(db_field, **kwargs)
@ -62,6 +109,9 @@ insertattr(AccountAdmin, 'inlines', ContactInline)
insertattr(AccountAdmin, 'inlines', InvoiceContactInline) insertattr(AccountAdmin, 'inlines', InvoiceContactInline)
insertattr(AccountAdmin, 'list_display', has_invoice) insertattr(AccountAdmin, 'list_display', has_invoice)
insertattr(AccountAdmin, 'list_filter', HasInvoiceContactListFilter) insertattr(AccountAdmin, 'list_filter', HasInvoiceContactListFilter)
for field in ('contacts__short_name', 'contacts__full_name', 'contacts__phone', search_fields = (
'contacts__phone2', 'contacts__email'): 'contacts__short_name', 'contacts__full_name', 'contacts__phone',
'contacts__phone2', 'contacts__email'
)
for field in search_fields:
insertattr(AccountAdmin, 'search_fields', field) insertattr(AccountAdmin, 'search_fields', field)

View File

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import accounts
from orchestra.models.fields import MultiSelectField from orchestra.models.fields import MultiSelectField
from . import settings from . import settings
@ -15,8 +16,8 @@ class Contact(models.Model):
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True, email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
choices=settings.CONTACTS_EMAIL_USAGES, choices=settings.CONTACTS_EMAIL_USAGES,
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES) default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
phone = models.CharField(_("Phone"), max_length=32, blank=True) phone = models.CharField(_("phone"), max_length=32, blank=True)
phone2 = models.CharField(_("Alternative Phone"), max_length=32, blank=True) phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True)
address = models.TextField(_("address"), blank=True) address = models.TextField(_("address"), blank=True)
city = models.CharField(_("city"), max_length=128, blank=True, city = models.CharField(_("city"), max_length=128, blank=True,
default=settings.CONTACTS_DEFAULT_CITY) default=settings.CONTACTS_DEFAULT_CITY)
@ -39,3 +40,6 @@ class InvoiceContact(models.Model):
country = models.CharField(_("country"), max_length=20, country = models.CharField(_("country"), max_length=20,
default=settings.CONTACTS_DEFAULT_COUNTRY) default=settings.CONTACTS_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64) vat = models.CharField(_("VAT number"), max_length=64)
accounts.register(Contact)

View File

@ -88,7 +88,7 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
('is_active', 'True'), ('is_active', 'True'),
) )
content_object_link = admin_link('content_object') 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

@ -186,7 +186,7 @@ class Service(models.Model):
def clean(self): def clean(self):
content_type = self.handler.get_content_type() content_type = self.handler.get_content_type()
if self.content_type != content_type: if self.content_type != content_type:
msg =_("Content type must be equal to '%s'." % str(content_type)) msg =_("Content type must be equal to '%s'.") % str(content_type)
raise ValidationError(msg) raise ValidationError(msg)
if not self.match: if not self.match:
msg =_("Match should be provided") msg =_("Match should be provided")

View File

@ -1,3 +1,9 @@
import random
import string
from lxml import etree
from lxml.builder import E
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
from rest_framework import serializers from rest_framework import serializers
@ -45,6 +51,116 @@ class BankTransfer(PaymentMethod):
form = BankTransferForm form = BankTransferForm
serializer = BankTransferSerializer serializer = BankTransferSerializer
def set_id(self):
size=6
chars=string.ascii_uppercase + string.digits
self.payment_id = ''.join(random.choice(chars) for _ in range(size))
def _process_transactions(self, transactions):
for transaction in transactions:
account = transaction.account
data = transaction.data
transaction.info = self.payment_id
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save()
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
),
E.InstdAmt(transaction.amount, Ccy="EUR"), # Instructed Amount
E.DrctDbtTx( # Direct Debit Transaction
E.MndtRltdInf( # Mandate Related Info
E.MndtId(str(account.id)), # Mandate Id
E.DtOfSgntr( # Date of Signature
account.registered_on.strfrm("%Y-%m-%d")
)
)
),
E.DbtrAgt( # Debtor Agent
E.FinInstnId( # Financial Institution Id
E.Othr(
E.Id('NOTPROVIDED')
)
)
),
E.Dbtr( # Debtor
E.Nm(account.name), # Name
),
E.DbtrAcct( # Debtor Account
E.Id(
E.IBAN(data['iban'])
),
),
)
def process(self, transactions)
self.set_id()
creditor_name = settings.PAYMENTS_DD_CREDITOR_NAME
creditor_iban = settings.PAYMENTS_DD_CREDITOR_IBAN
creditor_bic = settings.PAYMENTS_DD_CREDITOR_BIC
creditor_at02_id = settings.PAYMENTS_DD_CREDITOR_AT02_ID
now = timezone.now()
total = str(sum([transaction.amount for transaction in transactions]))
sepa = E.Document(
E.CstmrDrctDbtInitn(
E.GrpHdr( # Group Header
E.MsgId(self.payment_id), # Message Id
E.CreDtTm(now.strftime("%Y-%m-%dT%H:%M:%S")), # Creation Date Time
E.NbOfTxs(str(len(transactions))), # Number of Transactions
E.CtrlSum(total), # Control Sum
E.InitgPty( # Initiating Party
E.Nm(creditor_name), # Name
E.Id( # Identification
E.OrgId( # Organisation Id
E.Othr(
E.Id(creditor_at_02)
)
)
)
)
),
E.PmtInf( # Payment Info
E.PmtInfId(self.payment_id), # Payment Id
E.PmtMtd("DD"), # Payment Method
E.NbOfTxs(str(len(transactions))), # Number of Transactions
E.CtrlSum(total), # Control Sum
E.PmtTpInf( # Payment Type Info
E.SvcLvl( # Service Level
E.Cd("SEPA") # Code
),
E.LclInstrm( # Local Instrument
E.Cd("CORE") # Code
),
E.SeqTp("RCUR") # Sequence Type
),
E.ReqdColltnDt(now.strfrm("%Y-%m-%d")), # Requested Collection Date
E.Cdtr( # Creditor
E.Nm(creditor_name)
),
E.CdtrAcct( # Creditor Account
E.Id(
E.IBAN(creditor_iban)
)
),
E.CdtrAgt( # Creditor Agent
E.FinInstnId( # Financial Institution Id
E.BIC(creditor_bic)
)
),
*list(self._process_transactions(transactions)) # Transactions
)
), {
'xmlns': "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02",
'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"
}
)
# http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip
schema = etree.parse('pain.008.001.02.xsd')
schema.assertValid(sepa)
# TODO where to save this shit?
# TODO new model? Payment with batch support, How this relates to transaction?
return etree.tostring(page, pretty_print=True, xml_declaration=True)
class CreditCard(PaymentMethod): class CreditCard(PaymentMethod):
verbose_name = _("Credit card") verbose_name = _("Credit card")

View File

@ -2,3 +2,13 @@ from django.conf import settings
PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur') PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur')
PAYMENTS_DD_CREDITOR_NAME = getattr(settings, 'PAYMENTS_DD_CREDITOR_NAME',
'Orchestra')
PAYMENTS_DD_CREDITOR_IBAN = getattr(settings, 'PAYMENTS_DD_CREDITOR_IBAN',
'InvalidIBAN')
PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC',
'InvalidBIC')
PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID',
'InvalidAT02ID')

View File

@ -89,7 +89,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_mailboxes(self, address): def display_mailboxes(self, address):
boxes = [] boxes = []
for mailbox in address.mailboxes(): for mailbox in address.mailboxes.all():
user = mailbox.user user = mailbox.user
url = reverse('admin:users_user_mailbox_change', args=(user.pk,)) url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
boxes.append('<a href="%s">%s</a>' % (url, user.username)) boxes.append('<a href="%s">%s</a>' % (url, user.username))

View File

@ -1,3 +1,5 @@
import re
from django import forms from django import forms
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -20,9 +22,13 @@ class ShowTextWidget(forms.Widget):
else: else:
final_value = '<br/>'.join(value.split('\n')) final_value = '<br/>'.join(value.split('\n'))
if self.warning: if self.warning:
final_value = u'<ul class="messagelist"><li class="warning">%s</li></ul>' %(final_value) final_value = (
u'<ul class="messagelist"><li class="warning">%s</li></ul>'
% final_value)
if self.hidden: if self.hidden:
final_value = u'%s<input type="hidden" name="%s" value="%s"/>' % (final_value, name, value) final_value = (
u'%s<input type="hidden" name="%s" value="%s"/>'
% (final_value, name, value))
return mark_safe(final_value) return mark_safe(final_value)
def _has_changed(self, initial, data): def _has_changed(self, initial, data):
@ -44,3 +50,16 @@ class ReadOnlyWidget(forms.Widget):
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
return self.original_value return self.original_value
def paddingCheckboxSelectMultiple(padding):
""" Ugly hack to render this widget nicely on Django admin """
widget = forms.CheckboxSelectMultiple()
old_render = widget.render
def render(self, *args, **kwargs):
value = old_render(self, *args, **kwargs)
value = re.sub(r'^<ul id=(.*)>',
r'<ul id=\1 style="padding-left:%ipx">' % padding, value, 1)
return mark_safe(value)
widget.render = render
return widget