Imporvements on payments, bills and upgraded domain label validation to RFC 1123

This commit is contained in:
Marc Aymerich 2015-06-02 12:59:49 +00:00
parent a8cad48fed
commit a17e8d9b8c
19 changed files with 215 additions and 80 deletions

11
TODO.md
View file

@ -407,20 +407,17 @@ uwsgi --reload /tmp/project-master.pid
touch /tmp/somefile touch /tmp/somefile
# Change zone ttl # Change zone ttl
# batch zone edditing # batch zone edditing
# inherit registers from parent? # inherit registers from parent?
# Disable pagination on membership fees (allways one page)
# datetime metric storage granularity: otherwise innacurate detection of billed metric on order.billed_on # datetime metric storage granularity: otherwise innacurate detection of billed metric on order.billed_on
# Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~' # Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~'
serailzer self.instance on create. serailzer self.instance on create.
# generate Direct debit q19 on a protected path, or store it on the transaction.proc # set_password serializer: "just-the-password" not {"password": "password"}
# regenerate direct debit q19
# add transproc.method for regeneration
# TODO wrapp admin delete: delete proc undo processing on related transactions # use namedtuples!
# Negative transactionsx

View file

@ -161,6 +161,9 @@ STATIC_URL = '/static/'
# Example: "/home/media/media.lawrence.com/static/" # Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Absolute filesystem path to the directory that will hold user-uploaded files.
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Path used for database translations files # Path used for database translations files
LOCALE_PATHS = ( LOCALE_PATHS = (

View file

@ -1,5 +1,6 @@
from django.conf.urls import include, url from django.conf.urls import include, url
urlpatterns = [ urlpatterns = [
url(r'', include('orchestra.urls')), url(r'', include('orchestra.urls')),
] ]

View file

@ -17,7 +17,7 @@ from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions from . import settings, actions
from .filters import BillTypeListFilter, HasBillContactListFilter, PositivePriceListFilter from .filters import BillTypeListFilter, HasBillContactListFilter, TotalListFilter, PaymentStateListFilter
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
@ -39,7 +39,7 @@ class BillLineInline(admin.TabularInline):
order_link = admin_link('order', display='pk') order_link = admin_link('order', display='pk')
def display_total(self, line): def display_total(self, line):
total = line.get_total() total = line.compute_total()
sublines = line.sublines.all() sublines = line.sublines.all()
if sublines: if sublines:
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines]) content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
@ -89,7 +89,7 @@ class ClosedBillLineInline(BillLineInline):
display_subtotal.allow_tags = True display_subtotal.allow_tags = True
def display_total(self, line): def display_total(self, line):
return line.get_total() return line.compute_total()
display_total.short_description = _("Total") display_total.short_description = _("Total")
display_total.allow_tags = True display_total.allow_tags = True
@ -180,7 +180,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'number', 'type_link', 'account_link', 'created_on_display', 'number', 'type_link', 'account_link', 'created_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent' 'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
) )
list_filter = (BillTypeListFilter, 'is_open', 'is_sent', PositivePriceListFilter) list_filter = (BillTypeListFilter, 'is_open', 'is_sent', TotalListFilter, PaymentStateListFilter)
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments') add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
fieldsets = ( fieldsets = (
(None, { (None, {
@ -238,7 +238,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
state = bill.get_payment_state_display().upper() state = bill.get_payment_state_display().upper()
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
return '<a href="{url}" style="color:{color}">{name}</a>'.format( return '<a href="{url}" style="color:{color}">{name}</a>'.format(
url=url, color=color, name=state) url=url, color=color, name=state)
display_payment_state.allow_tags = True display_payment_state.allow_tags = True
display_payment_state.short_description = _("Payment") display_payment_state.short_description = _("Payment")

View file

@ -1,5 +1,7 @@
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -37,21 +39,25 @@ class BillTypeListFilter(SimpleListFilter):
} }
class PositivePriceListFilter(SimpleListFilter): class TotalListFilter(SimpleListFilter):
title = _("positive price") title = _("total")
parameter_name = 'positive_price' parameter_name = 'total'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('True', _("Yes")), ('gt', mark_safe("total &gt; 0")),
('False', _("No")), ('eq', "total = 0"),
('lt', mark_safe("total &lt; 0")),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'True': if self.value() == 'gt':
return queryset.filter(computed_total__gt=0) return queryset.filter(computed_total__gt=0)
if self.value() == 'False': elif self.value() == 'eq':
return queryset.filter(computed_total__lte=0) return queryset.filter(computed_total=0)
elif self.value() == 'lt':
return queryset.filter(computed_total__lt=0)
return queryset
class HasBillContactListFilter(SimpleListFilter): class HasBillContactListFilter(SimpleListFilter):
@ -68,5 +74,45 @@ class HasBillContactListFilter(SimpleListFilter):
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'True': if self.value() == 'True':
return queryset.filter(billcontact__isnull=False) return queryset.filter(billcontact__isnull=False)
if self.value() == 'False': elif self.value() == 'False':
return queryset.filter(billcontact__isnull=True) return queryset.filter(billcontact__isnull=True)
class PaymentStateListFilter(SimpleListFilter):
title = _("payment state")
parameter_name = 'payment_state'
def lookups(self, request, model_admin):
return (
('OPEN', _("Open")),
('PAID', _("Paid")),
('PENDING', _("Pending")),
('BAD_DEBT', _("Bad debt")),
)
def queryset(self, request, queryset):
Transaction = queryset.model.transactions.related.related_model
if self.value() == 'OPEN':
return queryset.filter(Q(is_open=True)|Q(type=queryset.model.PROFORMA))
elif self.value() == 'PAID':
zeros = queryset.filter(computed_total=0).values_list('id', flat=True)
ammounts = Transaction.objects.exclude(bill_id__in=zeros).secured().group_by('bill_id')
paid = []
for bill_id, total in queryset.exclude(computed_total=0).values_list('id', 'computed_total'):
try:
ammount = sum([t.ammount for t in ammounts[bill_id]])
except KeyError:
pass
else:
if abs(total) <= abs(ammount):
paid.append(bill_id)
return queryset.filter(Q(computed_total=0)|Q(id__in=paid))
elif self.value() == 'PENDING':
has_transaction = queryset.exclude(transactions__isnull=True)
non_rejected = has_transaction.exclude(transactions__state=Transaction.REJECTED)
non_rejected = non_rejected.values_list('id', flat=True).distinct()
return queryset.filter(pk__in=non_rejected)
elif self.value() == 'BAD_DEBT':
non_rejected = queryset.exclude(transactions__state=Transaction.REJECTED)
non_rejected = non_rejected.values_list('id', flat=True).distinct()
return queryset.exclude(pk__in=non_rejected)

View file

@ -93,7 +93,7 @@ class Bill(models.Model):
due_on = models.DateField(_("due on"), null=True, blank=True) due_on = models.DateField(_("due on"), null=True, blank=True)
updated_on = models.DateField(_("updated on"), auto_now=True) updated_on = models.DateField(_("updated on"), auto_now=True)
# TODO allways compute total or what? # TODO allways compute total or what?
total = models.DecimalField(max_digits=12, decimal_places=2, default=0) total = models.DecimalField(max_digits=12, decimal_places=2, null=True)
comments = models.TextField(_("comments"), blank=True) comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True) html = models.TextField(_("HTML"), blank=True)
@ -105,6 +105,10 @@ class Bill(models.Model):
def __str__(self): def __str__(self):
return self.number return self.number
@classmethod
def get_class_type(cls):
return cls.__name__.upper()
@cached_property @cached_property
def seller(self): def seller(self):
return Account.get_main().billcontact return Account.get_main().billcontact
@ -113,20 +117,29 @@ class Bill(models.Model):
def buyer(self): def buyer(self):
return self.account.billcontact return self.account.billcontact
@property
def has_multiple_pages(self):
return self.type != self.FEE
@cached_property @cached_property
def payment_state(self): def payment_state(self):
if self.is_open or self.get_type() == self.PROFORMA: if self.is_open or self.get_type() == self.PROFORMA:
return self.OPEN return self.OPEN
secured = self.transactions.secured().amount() or 0 secured = self.transactions.secured().amount() or 0
if secured >= self.total: if abs(secured) >= abs(self.get_total()):
return self.PAID return self.PAID
elif self.transactions.exclude_rejected().exists(): elif self.transactions.exclude_rejected().exists():
return self.PENDING return self.PENDING
return self.BAD_DEBT return self.BAD_DEBT
@property def get_total(self):
def has_multiple_pages(self): if not self.is_open:
return self.type != self.FEE return self.total
try:
return self.computed_total
except AttributeError:
self.computed_total = self.compute_total()
return self.computed_total
def get_payment_state_display(self): def get_payment_state_display(self):
value = self.payment_state value = self.payment_state
@ -135,10 +148,6 @@ class Bill(models.Model):
def get_current_transaction(self): def get_current_transaction(self):
return self.transactions.exclude_rejected().first() return self.transactions.exclude_rejected().first()
@classmethod
def get_class_type(cls):
return cls.__name__.upper()
def get_type(self): def get_type(self):
return self.type or self.get_class_type() return self.type or self.get_class_type()
@ -177,7 +186,7 @@ class Bill(models.Model):
payment = self.account.paymentsources.get_default() payment = self.account.paymentsources.get_default()
if not self.due_on: if not self.due_on:
self.due_on = self.get_due_date(payment=payment) self.due_on = self.get_due_date(payment=payment)
self.total = self.get_total() self.total = self.compute_total()
transaction = None transaction = None
if self.get_type() != self.PROFORMA: if self.get_type() != self.PROFORMA:
transaction = self.transactions.create(bill=self, source=payment, amount=self.total) transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
@ -241,7 +250,7 @@ class Bill(models.Model):
self.number = self.get_number() self.number = self.get_number()
super(Bill, self).save(*args, **kwargs) super(Bill, self).save(*args, **kwargs)
def get_subtotals(self): def compute_subtotals(self):
subtotals = {} subtotals = {}
lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0))) lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
for tax, total in lines.values_list('tax', 'totals'): for tax, total in lines.values_list('tax', 'totals'):
@ -250,7 +259,7 @@ class Bill(models.Model):
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2)) subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
return subtotals return subtotals
def get_total(self): def compute_total(self):
totals = self.lines.annotate( totals = self.lines.annotate(
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)) totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
return round(totals.aggregate(Sum('totals'))['totals__sum'], 2) return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)
@ -304,7 +313,7 @@ class BillLine(models.Model):
def __str__(self): def __str__(self):
return "#%i" % self.pk return "#%i" % self.pk
def get_total(self): def compute_total(self):
""" Computes subline discounts """ """ Computes subline discounts """
if self.pk: if self.pk:
return self.subtotal + sum([sub.total for sub in self.sublines.all()]) return self.subtotal + sum([sub.total for sub in self.sublines.all()])

View file

@ -107,7 +107,7 @@
</div> </div>
<div id="totals"> <div id="totals">
<br>&nbsp;<br> <br>&nbsp;<br>
{% for tax, subtotal in bill.get_subtotals.items %} {% for tax, subtotal in bill.compute_subtotals.items %}
<span class="subtotal column-title">{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}</span> <span class="subtotal column-title">{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}</span>
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span> <span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
<br> <br>

View file

@ -49,21 +49,19 @@ def validate_zone_interval(value):
def validate_zone_label(value): def validate_zone_label(value):
""" """
http://www.ietf.org/rfc/rfc1035.txt Allowable characters in a label for a host name are only ASCII letters, digits, and the `-' character.
The labels must follow the rules for ARPANET host names. They must Labels may not be all numbers, but may have a leading digit (e.g., 3com.com).
start with a letter, end with a letter or digit, and have as interior Labels must end and begin only with a letter or digit. See [RFC 1035] and [RFC 1123].
characters only letters, digits, and hyphen. There are also some
restrictions on the length. Labels must be 63 characters or less.
""" """
if not re.match(r'^[a-z][\.\-0-9a-z]*[\.0-9a-z]$', value): if not re.match(r'^[a-z0-9][\.\-0-9a-z]*[\.0-9a-z]$', value):
msg = _("Labels must start with a letter, end with a letter or digit, " msg = _("Labels must start and end with a letter or digit, "
"and have as interior characters only letters, digits, and hyphen") "and have as interior characters only letters, digits, and hyphen.")
raise ValidationError(msg) raise ValidationError(msg)
if not value.endswith('.'): if not value.endswith('.'):
msg = _("Use a fully expanded domain name ending with a dot") msg = _("Use a fully expanded domain name ending with a dot.")
raise ValidationError(msg) raise ValidationError(msg)
if len(value) > 63: if len(value) > 63:
raise ValidationError(_("Labels must be 63 characters or less")) raise ValidationError(_("Labels must be 63 characters or less."))
def validate_mx_record(value): def validate_mx_record(value):

View file

@ -14,8 +14,6 @@ from .models import Message
def send_message(message, num=0, connection=None, bulk=100): def send_message(message, num=0, connection=None, bulk=100):
if not message.pk:
message.save()
if num >= bulk: if num >= bulk:
connection.close() connection.close()
connection = None connection = None

View file

@ -10,6 +10,7 @@ from . import engine, settings
@task @task
def send_message(message): def send_message(message):
message.save()
engine.send_message(message) engine.send_message(message)

View file

@ -11,6 +11,7 @@ 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
from . import helpers
from .methods import PaymentMethod from .methods import PaymentMethod
from .models import Transaction from .models import Transaction
@ -175,25 +176,9 @@ commit.verbose_name = _("Commit")
def delete_selected(modeladmin, request, queryset): def delete_selected(modeladmin, request, queryset):
""" Has to have same name as admin.actions.delete_selected """ """ Has to have same name as admin.actions.delete_selected """
if not queryset: related_transactions = helpers.pre_delete_processes(modelamdin, request, queryset)
messages.warning(request, "No transaction process selected.")
return
if queryset.exclude(transactions__state=Transaction.WAITTING_EXECUTION).exists():
messages.error(request, "Done nothing. Not all related transactions in waitting execution.")
return
# Store before deleting
related_transactions = []
for process in queryset:
related_transactions.extend(process.transactions.filter(state=Transaction.WAITTING_EXECUTION))
response = actions.delete_selected(modeladmin, request, queryset) response = actions.delete_selected(modeladmin, request, queryset)
if response is None: if response is None:
# Confirmation helpers.post_delete_processes(modelamdin, request, related_transactions)
num = 0
for transaction in related_transactions:
transaction.state = Transaction.WAITTING_PROCESSING
transaction.save(update_fields=('state',))
num += 1
modeladmin.log_change(request, transaction, _("Unprocessed"))
messages.success(request, "%i related transactions marked as waitting for processing." % num)
return response return response
delete_selected.short_description = actions.delete_selected.short_description delete_selected.short_description = actions.delete_selected.short_description

View file

@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin
@ -7,7 +8,7 @@ from orchestra.admin.utils import admin_colored, admin_link
from orchestra.contrib.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.contrib.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from orchestra.plugins.admin import SelectPluginAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin
from . import actions from . import actions, helpers
from .methods import PaymentMethod from .methods import PaymentMethod
from .models import PaymentSource, Transaction, TransactionProcess from .models import PaymentSource, Transaction, TransactionProcess
@ -167,6 +168,14 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
exclude = ['mark_process_as_executed', 'abort', 'commit'] exclude = ['mark_process_as_executed', 'abort', 'commit']
return [action for action in actions if action.__name__ not in exclude] return [action for action in actions if action.__name__ not in exclude]
def delete_view(self, request, object_id, extra_context=None):
queryset = self.model.objects.filter(id=object_id)
related_transactions = helpers.pre_delete_processes(self, request, queryset)
response = super(TransactionProcessAdmin, self).delete_view(request, object_id, extra_context)
if isinstance(response, HttpResponseRedirect):
helpers.post_delete_processes(self, request, related_transactions)
return response
admin.site.register(PaymentSource, PaymentSourceAdmin) admin.site.register(PaymentSource, PaymentSourceAdmin)
admin.site.register(Transaction, TransactionAdmin) admin.site.register(Transaction, TransactionAdmin)

View file

@ -0,0 +1,37 @@
from django.contrib import messages
from django.utils.translation import ungettext, ugettext_lazy as _
from .models import Transaction
def pre_delete_processes(modeladmin, request, queryset):
""" Has to have same name as admin.actions.delete_selected """
if not queryset:
messages.warning(request,
_("No transaction process selected."))
return
if queryset.exclude(transactions__state=Transaction.WAITTING_EXECUTION).exists():
messages.error(request,
_("Done nothing. Not all related transactions in waitting execution."))
return
# Store before deleting
related_transactions = []
for process in queryset:
waitting_execution = process.transactions.filter(state=Transaction.WAITTING_EXECUTION)
related_transactions.extend(waitting_execution)
return related_transactions
def post_delete_processes(modeladmin, request, related_transactions):
# Confirmation
num = 0
for transaction in related_transactions:
transaction.state = Transaction.WAITTING_PROCESSING
transaction.save(update_fields=('state',))
num += 1
modeladmin.log_change(request, transaction, _("Unprocessed"))
messages.success(request, ungettext(
"One related transaction has been marked as <i>waitting for processing</i>",
"%i related transactions have been marked as <i>waitting for processing</i>." % num,
num
))

View file

@ -185,7 +185,9 @@ class SEPADirectDebit(PaymentMethod):
data = transaction.source.data data = transaction.source.data
yield E.DrctDbtTxInf( # Direct Debit Transaction Info yield E.DrctDbtTxInf( # Direct Debit Transaction Info
E.PmtId( # Payment Id E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End E.EndToEndId( # Payment Id/End to End
str(transaction.bill.number)+'-'+str(transaction.id)
)
), ),
E.InstdAmt( # Instructed Amount E.InstdAmt( # Instructed Amount
str(abs(transaction.amount)), str(abs(transaction.amount)),
@ -207,7 +209,7 @@ class SEPADirectDebit(PaymentMethod):
) )
), ),
E.Dbtr( # Debtor E.Dbtr( # Debtor
E.Nm(account.name), # Name E.Nm(account.billcontact.get_name()), # Name
), ),
E.DbtrAcct( # Debtor Account E.DbtrAcct( # Debtor Account
E.Id( E.Id(

View file

@ -4,6 +4,7 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField from jsonfield import JSONField
from orchestra.models.fields import PrivateFileField
from orchestra.models.queryset import group_by from orchestra.models.queryset import group_by
from . import settings from . import settings
@ -164,7 +165,7 @@ class TransactionProcess(models.Model):
) )
data = JSONField(_("data"), blank=True) data = JSONField(_("data"), blank=True)
file = models.FileField(_("file"), blank=True) file = PrivateFileField(_("file"), blank=True)
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED) state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
created_at = models.DateTimeField(_("created"), auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
updated_at = models.DateTimeField(_("updated"), auto_now=True) updated_at = models.DateTimeField(_("updated"), auto_now=True)

View file

@ -6,7 +6,7 @@
<p>{{ content_message }}</p> <p>{{ content_message }}</p>
<ul> <ul>
{% for proc in processes %} {% for proc in processes %}
<li> <a href="{% url admin:payments_transactionprocess_change' proc.pk|admin_urlquote %}">Process #{{ proc.id }}</a> <li> <a href="{% url 'admin:payments_transactionprocess_change' proc.pk|admin_urlquote %}">Process #{{ proc.id }}</a>
{% if proc.file %} {% if proc.file %}
<ul><li>File: <a href="{{ proc.file.url }}">{{ proc.file }}</a></li></ul> <ul><li>File: <a href="{{ proc.file.url }}">{{ proc.file }}</a></li></ul>
{% endif %} {% endif %}

View file

@ -1,5 +1,9 @@
import os
from django.core import exceptions from django.core import exceptions
from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.fields.files import FileField, FieldFile
from django.utils.text import capfirst from django.utils.text import capfirst
from ..forms.fields import MultiSelectFormField from ..forms.fields import MultiSelectFormField
@ -58,7 +62,32 @@ class NullableCharField(models.CharField):
return value or None return value or None
if isinstalled('south'): class PrivateFieldFile(FieldFile):
from south.modelsinspector import add_introspection_rules @property
add_introspection_rules([], ["^orchestra\.models\.fields\.MultiSelectField"]) def url(self):
add_introspection_rules([], ["^orchestra\.models\.fields\.NullableCharField"]) self._require_file()
app_label = self.instance._meta.app_label
model_name = self.instance._meta.object_name.lower()
field_name = self.field.name
pk = self.instance.pk
filename = os.path.basename(self.path)
args = [app_label, model_name, field_name, pk, filename]
return reverse('private-media', args=args)
@property
def condition(self):
return self.field.condition
@property
def attachment(self):
return self.field.attachment
class PrivateFileField(FileField):
attr_class = PrivateFieldFile
def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, attachment=True,
condition=lambda request, instance: request.user.is_superuser, **kwargs):
super(PrivateFileField, self).__init__(verbose_name, name, upload_to, storage, **kwargs)
self.condition = condition
self.attachment = attachment

View file

@ -21,11 +21,10 @@ urlpatterns = [
'rest_framework.authtoken.views.obtain_auth_token', 'rest_framework.authtoken.views.obtain_auth_token',
name='api-token-auth' name='api-token-auth'
), ),
# TODO make this private url(r'^media/(.+)/(.+)/(.+)/(.+)/(.+)$',
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', { 'orchestra.views.serve_private_media',
'document_root': settings.MEDIA_ROOT, name='private-media'
'show_indexes': True ),
})
] ]

20
orchestra/views.py Normal file
View file

@ -0,0 +1,20 @@
from django.http import Http404
from django.contrib.admin.util import unquote
from django.core.exceptions import PermissionDenied
from django.db.models import get_model
from django.shortcuts import get_object_or_404
from django.views.static import serve
def serve_private_media(request, app_label, model_name, field_name, object_id, filename):
model = get_model(app_label, model_name)
if model is None:
raise Http404('')
instance = get_object_or_404(model, pk=unquote(object_id))
if not hasattr(instance, field_name):
raise Http404('')
field = getattr(instance, field_name)
if field.condition(request, instance):
return serve(request, field.name, document_root=field.storage.location)
else:
raise PermissionDenied()