Imporvements on payments, bills and upgraded domain label validation to RFC 1123
This commit is contained in:
parent
a8cad48fed
commit
a17e8d9b8c
11
TODO.md
11
TODO.md
|
@ -407,20 +407,17 @@ uwsgi --reload /tmp/project-master.pid
|
|||
touch /tmp/somefile
|
||||
|
||||
|
||||
|
||||
# Change zone ttl
|
||||
# batch zone edditing
|
||||
# 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
|
||||
|
||||
# Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~'
|
||||
serailzer self.instance on create.
|
||||
|
||||
# generate Direct debit q19 on a protected path, or store it on the transaction.proc
|
||||
# regenerate direct debit q19
|
||||
# add transproc.method for regeneration
|
||||
# set_password serializer: "just-the-password" not {"password": "password"}
|
||||
|
||||
# TODO wrapp admin delete: delete proc undo processing on related transactions
|
||||
# use namedtuples!
|
||||
|
||||
# Negative transactionsx
|
||||
|
|
|
@ -161,6 +161,9 @@ STATIC_URL = '/static/'
|
|||
# Example: "/home/media/media.lawrence.com/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
|
||||
LOCALE_PATHS = (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.conf.urls import include, url
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'', include('orchestra.urls')),
|
||||
]
|
||||
|
|
|
@ -17,7 +17,7 @@ from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
|||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ class BillLineInline(admin.TabularInline):
|
|||
order_link = admin_link('order', display='pk')
|
||||
|
||||
def display_total(self, line):
|
||||
total = line.get_total()
|
||||
total = line.compute_total()
|
||||
sublines = line.sublines.all()
|
||||
if 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
|
||||
|
||||
def display_total(self, line):
|
||||
return line.get_total()
|
||||
return line.compute_total()
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
||||
|
@ -180,7 +180,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
'number', 'type_link', 'account_link', 'created_on_display',
|
||||
'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')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
|
@ -238,7 +238,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
state = bill.get_payment_state_display().upper()
|
||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||
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.short_description = _("Payment")
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.contrib.admin import SimpleListFilter
|
||||
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 _
|
||||
|
||||
|
||||
|
@ -37,21 +39,25 @@ class BillTypeListFilter(SimpleListFilter):
|
|||
}
|
||||
|
||||
|
||||
class PositivePriceListFilter(SimpleListFilter):
|
||||
title = _("positive price")
|
||||
parameter_name = 'positive_price'
|
||||
class TotalListFilter(SimpleListFilter):
|
||||
title = _("total")
|
||||
parameter_name = 'total'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('True', _("Yes")),
|
||||
('False', _("No")),
|
||||
('gt', mark_safe("total > 0")),
|
||||
('eq', "total = 0"),
|
||||
('lt', mark_safe("total < 0")),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'True':
|
||||
if self.value() == 'gt':
|
||||
return queryset.filter(computed_total__gt=0)
|
||||
if self.value() == 'False':
|
||||
return queryset.filter(computed_total__lte=0)
|
||||
elif self.value() == 'eq':
|
||||
return queryset.filter(computed_total=0)
|
||||
elif self.value() == 'lt':
|
||||
return queryset.filter(computed_total__lt=0)
|
||||
return queryset
|
||||
|
||||
|
||||
class HasBillContactListFilter(SimpleListFilter):
|
||||
|
@ -68,5 +74,45 @@ class HasBillContactListFilter(SimpleListFilter):
|
|||
def queryset(self, request, queryset):
|
||||
if self.value() == 'True':
|
||||
return queryset.filter(billcontact__isnull=False)
|
||||
if self.value() == 'False':
|
||||
elif self.value() == 'False':
|
||||
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)
|
||||
|
|
|
@ -93,7 +93,7 @@ class Bill(models.Model):
|
|||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||
updated_on = models.DateField(_("updated on"), auto_now=True)
|
||||
# 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)
|
||||
html = models.TextField(_("HTML"), blank=True)
|
||||
|
||||
|
@ -105,6 +105,10 @@ class Bill(models.Model):
|
|||
def __str__(self):
|
||||
return self.number
|
||||
|
||||
@classmethod
|
||||
def get_class_type(cls):
|
||||
return cls.__name__.upper()
|
||||
|
||||
@cached_property
|
||||
def seller(self):
|
||||
return Account.get_main().billcontact
|
||||
|
@ -113,20 +117,29 @@ class Bill(models.Model):
|
|||
def buyer(self):
|
||||
return self.account.billcontact
|
||||
|
||||
@property
|
||||
def has_multiple_pages(self):
|
||||
return self.type != self.FEE
|
||||
|
||||
@cached_property
|
||||
def payment_state(self):
|
||||
if self.is_open or self.get_type() == self.PROFORMA:
|
||||
return self.OPEN
|
||||
secured = self.transactions.secured().amount() or 0
|
||||
if secured >= self.total:
|
||||
if abs(secured) >= abs(self.get_total()):
|
||||
return self.PAID
|
||||
elif self.transactions.exclude_rejected().exists():
|
||||
return self.PENDING
|
||||
return self.BAD_DEBT
|
||||
|
||||
@property
|
||||
def has_multiple_pages(self):
|
||||
return self.type != self.FEE
|
||||
def get_total(self):
|
||||
if not self.is_open:
|
||||
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):
|
||||
value = self.payment_state
|
||||
|
@ -135,10 +148,6 @@ class Bill(models.Model):
|
|||
def get_current_transaction(self):
|
||||
return self.transactions.exclude_rejected().first()
|
||||
|
||||
@classmethod
|
||||
def get_class_type(cls):
|
||||
return cls.__name__.upper()
|
||||
|
||||
def get_type(self):
|
||||
return self.type or self.get_class_type()
|
||||
|
||||
|
@ -177,7 +186,7 @@ class Bill(models.Model):
|
|||
payment = self.account.paymentsources.get_default()
|
||||
if not self.due_on:
|
||||
self.due_on = self.get_due_date(payment=payment)
|
||||
self.total = self.get_total()
|
||||
self.total = self.compute_total()
|
||||
transaction = None
|
||||
if self.get_type() != self.PROFORMA:
|
||||
transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
|
||||
|
@ -241,7 +250,7 @@ class Bill(models.Model):
|
|||
self.number = self.get_number()
|
||||
super(Bill, self).save(*args, **kwargs)
|
||||
|
||||
def get_subtotals(self):
|
||||
def compute_subtotals(self):
|
||||
subtotals = {}
|
||||
lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
|
||||
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))
|
||||
return subtotals
|
||||
|
||||
def get_total(self):
|
||||
def compute_total(self):
|
||||
totals = self.lines.annotate(
|
||||
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
|
||||
return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)
|
||||
|
@ -304,7 +313,7 @@ class BillLine(models.Model):
|
|||
def __str__(self):
|
||||
return "#%i" % self.pk
|
||||
|
||||
def get_total(self):
|
||||
def compute_total(self):
|
||||
""" Computes subline discounts """
|
||||
if self.pk:
|
||||
return self.subtotal + sum([sub.total for sub in self.sublines.all()])
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
</div>
|
||||
<div id="totals">
|
||||
<br> <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-value">{{ subtotal | first }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
|
|
|
@ -49,21 +49,19 @@ def validate_zone_interval(value):
|
|||
|
||||
def validate_zone_label(value):
|
||||
"""
|
||||
http://www.ietf.org/rfc/rfc1035.txt
|
||||
The labels must follow the rules for ARPANET host names. They must
|
||||
start with a letter, end with a letter or digit, and have as interior
|
||||
characters only letters, digits, and hyphen. There are also some
|
||||
restrictions on the length. Labels must be 63 characters or less.
|
||||
Allowable characters in a label for a host name are only ASCII letters, digits, and the `-' character.
|
||||
Labels may not be all numbers, but may have a leading digit (e.g., 3com.com).
|
||||
Labels must end and begin only with a letter or digit. See [RFC 1035] and [RFC 1123].
|
||||
"""
|
||||
if not re.match(r'^[a-z][\.\-0-9a-z]*[\.0-9a-z]$', value):
|
||||
msg = _("Labels must start with a letter, end with a letter or digit, "
|
||||
"and have as interior characters only letters, digits, and hyphen")
|
||||
if not re.match(r'^[a-z0-9][\.\-0-9a-z]*[\.0-9a-z]$', value):
|
||||
msg = _("Labels must start and end with a letter or digit, "
|
||||
"and have as interior characters only letters, digits, and hyphen.")
|
||||
raise ValidationError(msg)
|
||||
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)
|
||||
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):
|
||||
|
|
|
@ -14,8 +14,6 @@ from .models import Message
|
|||
|
||||
|
||||
def send_message(message, num=0, connection=None, bulk=100):
|
||||
if not message.pk:
|
||||
message.save()
|
||||
if num >= bulk:
|
||||
connection.close()
|
||||
connection = None
|
||||
|
|
|
@ -10,6 +10,7 @@ from . import engine, settings
|
|||
|
||||
@task
|
||||
def send_message(message):
|
||||
message.save()
|
||||
engine.send_message(message)
|
||||
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.utils.translation import ungettext, ugettext_lazy as _
|
|||
from orchestra.admin.decorators import action_with_confirmation
|
||||
from orchestra.admin.utils import change_url
|
||||
|
||||
from . import helpers
|
||||
from .methods import PaymentMethod
|
||||
from .models import Transaction
|
||||
|
||||
|
@ -175,25 +176,9 @@ commit.verbose_name = _("Commit")
|
|||
|
||||
def delete_selected(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:
|
||||
related_transactions.extend(process.transactions.filter(state=Transaction.WAITTING_EXECUTION))
|
||||
related_transactions = helpers.pre_delete_processes(modelamdin, request, queryset)
|
||||
response = actions.delete_selected(modeladmin, request, queryset)
|
||||
if response is None:
|
||||
# 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, "%i related transactions marked as waitting for processing." % num)
|
||||
if response is None:
|
||||
helpers.post_delete_processes(modelamdin, request, related_transactions)
|
||||
return response
|
||||
delete_selected.short_description = actions.delete_selected.short_description
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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.plugins.admin import SelectPluginAdminMixin
|
||||
|
||||
from . import actions
|
||||
from . import actions, helpers
|
||||
from .methods import PaymentMethod
|
||||
from .models import PaymentSource, Transaction, TransactionProcess
|
||||
|
||||
|
@ -166,6 +167,14 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
elif obj.state == TransactionProcess.ABORTED:
|
||||
exclude = ['mark_process_as_executed', 'abort', 'commit']
|
||||
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)
|
||||
|
|
37
orchestra/contrib/payments/helpers.py
Normal file
37
orchestra/contrib/payments/helpers.py
Normal 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
|
||||
))
|
|
@ -185,7 +185,9 @@ class SEPADirectDebit(PaymentMethod):
|
|||
data = transaction.source.data
|
||||
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
||||
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
|
||||
str(abs(transaction.amount)),
|
||||
|
@ -207,7 +209,7 @@ class SEPADirectDebit(PaymentMethod):
|
|||
)
|
||||
),
|
||||
E.Dbtr( # Debtor
|
||||
E.Nm(account.name), # Name
|
||||
E.Nm(account.billcontact.get_name()), # Name
|
||||
),
|
||||
E.DbtrAcct( # Debtor Account
|
||||
E.Id(
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
|
||||
from orchestra.models.fields import PrivateFileField
|
||||
from orchestra.models.queryset import group_by
|
||||
|
||||
from . import settings
|
||||
|
@ -164,7 +165,7 @@ class TransactionProcess(models.Model):
|
|||
)
|
||||
|
||||
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)
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<p>{{ content_message }}</p>
|
||||
<ul>
|
||||
{% 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 %}
|
||||
<ul><li>File: <a href="{{ proc.file.url }}">{{ proc.file }}</a></li></ul>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import os
|
||||
|
||||
from django.core import exceptions
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models.fields.files import FileField, FieldFile
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from ..forms.fields import MultiSelectFormField
|
||||
|
@ -58,7 +62,32 @@ class NullableCharField(models.CharField):
|
|||
return value or None
|
||||
|
||||
|
||||
if isinstalled('south'):
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
add_introspection_rules([], ["^orchestra\.models\.fields\.MultiSelectField"])
|
||||
add_introspection_rules([], ["^orchestra\.models\.fields\.NullableCharField"])
|
||||
class PrivateFieldFile(FieldFile):
|
||||
@property
|
||||
def url(self):
|
||||
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
|
||||
|
|
|
@ -21,11 +21,10 @@ urlpatterns = [
|
|||
'rest_framework.authtoken.views.obtain_auth_token',
|
||||
name='api-token-auth'
|
||||
),
|
||||
# TODO make this private
|
||||
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
|
||||
'document_root': settings.MEDIA_ROOT,
|
||||
'show_indexes': True
|
||||
})
|
||||
url(r'^media/(.+)/(.+)/(.+)/(.+)/(.+)$',
|
||||
'orchestra.views.serve_private_media',
|
||||
name='private-media'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
20
orchestra/views.py
Normal file
20
orchestra/views.py
Normal 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()
|
Loading…
Reference in a new issue