Preliminar implementation of payment methods
This commit is contained in:
parent
72ef63ffdf
commit
bef7af084b
|
@ -62,6 +62,8 @@ def get_account_items():
|
|||
if isinstalled('orchestra.apps.payments'):
|
||||
url = reverse('admin:payments_transaction_changelist')
|
||||
childrens.append(items.MenuItem(_("Transactions"), url))
|
||||
url = reverse('admin:payments_paymentsource_changelist')
|
||||
childrens.append(items.MenuItem(_("Payment Sources"), url))
|
||||
if isinstalled('orchestra.apps.issues'):
|
||||
url = reverse('admin:issues_ticket_changelist')
|
||||
childrens.append(items.MenuItem(_("Tickets"), url))
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link
|
||||
from orchestra.core import services
|
||||
from orchestra.core import services, accounts
|
||||
|
||||
from .filters import HasMainUserListFilter
|
||||
from .forms import AccountCreationForm, AccountChangeForm
|
||||
|
@ -60,10 +60,13 @@ class AccountAdmin(ExtendedModelAdmin):
|
|||
if not account.is_active:
|
||||
messages.warning(request, 'This account is disabled.')
|
||||
context = {
|
||||
# TODO not services but everythin (payments, bills, etc)
|
||||
'services': sorted(
|
||||
[ model._meta for model in services.get() if model is not Account ],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
),
|
||||
'accounts': sorted(
|
||||
[ model._meta for model in accounts.get() if model is not Account ],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
)
|
||||
}
|
||||
context.update(extra_context or {})
|
||||
|
@ -83,8 +86,9 @@ class AccountAdmin(ExtendedModelAdmin):
|
|||
def get_queryset(self, request):
|
||||
""" Select related for performance """
|
||||
# TODO move invoicecontact to contacts
|
||||
qs = super(AccountAdmin, self).get_queryset(request)
|
||||
related = ('user', 'invoicecontact')
|
||||
return super(AccountAdmin, self).get_queryset(request).select_related(*related)
|
||||
return qs.select_related(*related)
|
||||
|
||||
|
||||
admin.site.register(Account, AccountAdmin)
|
||||
|
|
|
@ -3,11 +3,28 @@
|
|||
|
||||
|
||||
{% block object-tools-items %}
|
||||
|
||||
|
||||
{% for service in services %}
|
||||
<li>
|
||||
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
|
||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
|
||||
<ul class="object-tools">
|
||||
{% for account in accounts %}
|
||||
<li>
|
||||
<a href="{% url account|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ account.verbose_name_plural|capfirst }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
|
||||
|
||||
<ul class="object-tools">
|
||||
<li>
|
||||
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
||||
</li>
|
||||
|
@ -15,6 +32,6 @@
|
|||
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
|
||||
</li>
|
||||
{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ from django.db import models
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import accounts
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
|
@ -87,6 +89,10 @@ class Bill(models.Model):
|
|||
self.ident = '{prefix}{year}{number}'.format(
|
||||
prefix=prefix, year=year, number=number)
|
||||
|
||||
def close(self):
|
||||
self.status = self.CLOSED
|
||||
self.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.bill_type:
|
||||
self.bill_type = type(self).get_type()
|
||||
|
@ -146,3 +152,5 @@ class BillLine(BaseBillLine):
|
|||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||
related_name='amendment_lines', null=True, blank=True)
|
||||
|
||||
|
||||
accounts.register(Bill)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||
|
||||
from .models import Bill, BillLine
|
||||
|
||||
|
||||
|
@ -8,8 +10,13 @@ class BillLineSerializer(serializers.HyperlinkedModelSerializer):
|
|||
model = BillLine
|
||||
|
||||
|
||||
class BillSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
lines = BillLineSerializer(source='billlines')
|
||||
|
||||
class Meta:
|
||||
model = Bill
|
||||
fields = (
|
||||
'url', 'ident', 'bill_type', 'status', 'created_on', 'due_on',
|
||||
'comments', 'html', 'lines'
|
||||
)
|
||||
|
|
|
@ -30,10 +30,10 @@ class MailmanTraffic(ServiceMonitor):
|
|||
MAILMAN_LOG="$4"
|
||||
|
||||
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
|
||||
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \
|
||||
| awk '\"$LAST_DATE\"<=$0 && $0<=\"%s\"' \
|
||||
| sed 's/.*size=\([0-9]*\).*/\\1/' \
|
||||
| tr '\\n' '+' \
|
||||
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \\
|
||||
| awk '"$LAST_DATE"<=$0 && $0<="%s"' \\
|
||||
| sed 's/.*size=\([0-9]*\).*/\\1/' \\
|
||||
| tr '\\n' '+' \\
|
||||
| xargs -i echo {} )
|
||||
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
|
||||
}""" % current_date))
|
||||
|
|
|
@ -5,7 +5,7 @@ from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
|||
from .models import List
|
||||
|
||||
|
||||
class ListSerializer(AccountSerializerMixin, serializers.ModelSerializer):
|
||||
class ListSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = List
|
||||
fields = ('name', 'address_name', 'address_domain',)
|
||||
fields = ('url', 'name', 'address_name', 'address_domain')
|
||||
|
|
|
@ -71,6 +71,13 @@ class BackendOperationInline(admin.TabularInline):
|
|||
return False
|
||||
|
||||
|
||||
def display_mono(field):
|
||||
def display(self, log):
|
||||
return monospace_format(escape(getattr(log, field)))
|
||||
display.short_description = _(field)
|
||||
return display
|
||||
|
||||
|
||||
class BackendLogAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'backend', 'server_link', 'display_state', 'exit_code',
|
||||
|
@ -91,22 +98,10 @@ class BackendLogAdmin(admin.ModelAdmin):
|
|||
display_last_update = admin_date('last_update')
|
||||
display_created = admin_date('created')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||
|
||||
def mono_script(self, log):
|
||||
return monospace_format(escape(log.script))
|
||||
mono_script.short_description = _("script")
|
||||
|
||||
def mono_stdout(self, log):
|
||||
return monospace_format(escape(log.stdout))
|
||||
mono_stdout.short_description = _("stdout")
|
||||
|
||||
def mono_stderr(self, log):
|
||||
return monospace_format(escape(log.stderr))
|
||||
mono_stderr.short_description = _("stderr")
|
||||
|
||||
def mono_traceback(self, log):
|
||||
return monospace_format(escape(log.traceback))
|
||||
mono_traceback.short_description = _("traceback")
|
||||
mono_script = display_mono('script')
|
||||
mono_stdout = display_mono('stdout')
|
||||
mono_stderr = display_mono('stderr')
|
||||
mono_traceback = display_mono('traceback')
|
||||
|
||||
def get_queryset(self, request):
|
||||
""" Order by structured name and imporve performance """
|
||||
|
|
14
orchestra/apps/orders/api.py
Normal file
14
orchestra/apps/orders/api.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import viewsets
|
||||
|
||||
from orchestra.api import router
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
||||
from .models import Order
|
||||
from .serializers import OrderSerializer
|
||||
|
||||
|
||||
class OrderViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||
model = Order
|
||||
serializer_class = OrderSerializer
|
||||
|
||||
router.register(r'orders', OrderViewSet)
|
|
@ -10,7 +10,7 @@ from django.utils import timezone
|
|||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import caches, services
|
||||
from orchestra.core import caches, services, accounts
|
||||
from orchestra.models import queryset
|
||||
from orchestra.utils.apps import autodiscover
|
||||
|
||||
|
@ -175,7 +175,8 @@ class Service(models.Model):
|
|||
return services
|
||||
|
||||
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
|
||||
# @cached_property
|
||||
# @property
|
||||
@cached_property
|
||||
def handler(self):
|
||||
""" Accessor of this service handler instance """
|
||||
if self.handler_type:
|
||||
|
@ -338,3 +339,6 @@ def update_orders(sender, **kwargs):
|
|||
related = helpers.get_related_objects(instance)
|
||||
if related:
|
||||
Order.update_orders(related)
|
||||
|
||||
|
||||
accounts.register(Order)
|
||||
|
|
14
orchestra/apps/orders/serializers.py
Normal file
14
orchestra/apps/orders/serializers.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||
|
||||
from .models import Order
|
||||
|
||||
|
||||
class OrderSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
'url', 'registered_on', 'cancelled_on', 'billed_on', 'billed_until',
|
||||
'description'
|
||||
)
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from orchestra.admin.utils import admin_colored, admin_link
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
|
||||
from .methods import BankTransfer
|
||||
from .models import PaymentSource, Transaction
|
||||
|
||||
|
||||
|
@ -17,14 +19,21 @@ STATE_COLORS = {
|
|||
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'bill_link', 'account_link', 'method', 'display_state', 'amount'
|
||||
'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount'
|
||||
)
|
||||
list_filter = ('method', 'state')
|
||||
list_filter = ('source__method', 'state')
|
||||
|
||||
bill_link = admin_link('bill')
|
||||
account_link = admin_link('bill__account')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||
|
||||
|
||||
admin.site.register(PaymentSource)
|
||||
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||
list_filter = ('method', 'is_active')
|
||||
form = BankTransfer().get_form()
|
||||
# TODO select payment source method
|
||||
|
||||
|
||||
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
||||
admin.site.register(Transaction, TransactionAdmin)
|
||||
|
|
46
orchestra/apps/payments/forms.py
Normal file
46
orchestra/apps/payments/forms.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_iban.forms import IBANFormField
|
||||
|
||||
|
||||
class PaymentSourceDataForm(forms.ModelForm):
|
||||
class Meta:
|
||||
exclude = ('data', 'method')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentSourceDataForm, self).__init__(*args, **kwargs)
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
for field in self.declared_fields:
|
||||
initial = self.fields[field].initial
|
||||
self.fields[field].initial = instance.data.get(field, initial)
|
||||
|
||||
def save(self, commit=True):
|
||||
plugin = self.plugin
|
||||
self.instance.method = plugin.get_plugin_name()
|
||||
self.instance.data = {
|
||||
field: self.cleaned_data[field] for field in self.declared_fields
|
||||
}
|
||||
return super(PaymentSourceDataForm, self).save(commit=commit)
|
||||
|
||||
|
||||
class BankTransferForm(PaymentSourceDataForm):
|
||||
iban = IBANFormField(label='IBAN',
|
||||
widget=forms.TextInput(attrs={'size': '50'}))
|
||||
name = forms.CharField(max_length=128, label=_("Name"),
|
||||
widget=forms.TextInput(attrs={'size': '50'}))
|
||||
|
||||
|
||||
class CreditCardForm(PaymentSourceDataForm):
|
||||
label = forms.CharField(max_length=128, label=_("Label"),
|
||||
help_text=_("Use a name such as \"Jo's Visa\" to remember which "
|
||||
"card it is."))
|
||||
first_name = forms.CharField(max_length=128)
|
||||
last_name = forms.CharField(max_length=128)
|
||||
address = forms.CharField(max_length=128)
|
||||
zip = forms.CharField(max_length=128)
|
||||
city = forms.CharField(max_length=128)
|
||||
country = forms.CharField(max_length=128)
|
||||
card_number = forms.CharField(max_length=128)
|
||||
expiration_date = forms.CharField(max_length=128)
|
||||
security_code = forms.CharField(max_length=128)
|
|
@ -1,15 +1,52 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
|
||||
from rest_framework import serializers
|
||||
|
||||
from orchestra.utils import plugins
|
||||
|
||||
from .forms import BankTransferForm, CreditCardForm
|
||||
|
||||
|
||||
class PaymentMethod(plugins.Plugin):
|
||||
label_field = 'label'
|
||||
number_field = 'number'
|
||||
|
||||
__metaclass__ = plugins.PluginMount
|
||||
|
||||
def get_form(self):
|
||||
self.form.plugin = self
|
||||
return self.form
|
||||
|
||||
def get_serializer(self):
|
||||
self.serializer.plugin = self
|
||||
return self.serializer
|
||||
|
||||
def get_label(self, data):
|
||||
return data[self.label_field]
|
||||
|
||||
def get_number(self, data):
|
||||
return data[self.number_field]
|
||||
|
||||
|
||||
class BankTransferSerializer(serializers.Serializer):
|
||||
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
|
||||
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
|
||||
name = serializers.CharField(label=_("Name"), max_length=128)
|
||||
|
||||
|
||||
class CreditCardSerializer(serializers.Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class BankTransfer(PaymentMethod):
|
||||
verbose_name = _("Bank transfer")
|
||||
label_field = 'name'
|
||||
number_field = 'iban'
|
||||
form = BankTransferForm
|
||||
serializer = BankTransferSerializer
|
||||
|
||||
|
||||
class CreditCard(PaymentMethod):
|
||||
verbose_name = _("Credit card")
|
||||
form = CreditCardForm
|
||||
serializer = CreditCardSerializer
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
|
||||
from orchestra.core import accounts
|
||||
|
||||
from . import settings
|
||||
from .methods import PaymentMethod
|
||||
|
||||
|
@ -13,6 +17,25 @@ class PaymentSource(models.Model):
|
|||
choices=PaymentMethod.get_plugin_choices())
|
||||
data = JSONField(_("data"))
|
||||
is_active = models.BooleanField(_("is active"), default=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.label or str(self.account)
|
||||
|
||||
@cached_property
|
||||
def label(self):
|
||||
try:
|
||||
plugin = PaymentMethod.get_plugin(self.method)()
|
||||
except KeyError:
|
||||
return None
|
||||
return plugin.get_label(self.data)
|
||||
|
||||
@cached_property
|
||||
def number(self):
|
||||
try:
|
||||
plugin = PaymentMethod.get_plugin(self.method)()
|
||||
except KeyError:
|
||||
return None
|
||||
return plugin.get_number(self.data)
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
|
@ -34,8 +57,8 @@ class Transaction(models.Model):
|
|||
# TODO account fk?
|
||||
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
||||
related_name='transactions')
|
||||
method = models.CharField(_("payment method"), max_length=32,
|
||||
choices=PaymentMethod.get_plugin_choices())
|
||||
source = models.ForeignKey(PaymentSource, verbose_name=_("source"),
|
||||
related_name='transactions')
|
||||
state = models.CharField(_("state"), max_length=32, choices=STATES,
|
||||
default=WAITTING_PROCESSING)
|
||||
data = JSONField(_("data"))
|
||||
|
@ -47,3 +70,7 @@ class Transaction(models.Model):
|
|||
|
||||
def __unicode__(self):
|
||||
return "Transaction {}".format(self.id)
|
||||
|
||||
|
||||
accounts.register(PaymentSource)
|
||||
accounts.register(Transaction)
|
||||
|
|
|
@ -1,14 +1,42 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import PaymentSource, PaymentSource
|
||||
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||
|
||||
from .methods import PaymentMethod
|
||||
from .models import PaymentSource, Transaction
|
||||
|
||||
|
||||
class PaymentSourceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = PaymentSource
|
||||
|
||||
|
||||
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
fields = ('url', 'method', 'data', 'is_active')
|
||||
|
||||
def validate_data(self, attrs, source):
|
||||
plugin = PaymentMethod.get_plugin(attrs['method'])
|
||||
serializer_class = plugin().get_serializer()
|
||||
serializer = serializer_class(data=attrs[source])
|
||||
if not serializer.is_valid():
|
||||
raise serializers.ValidationError(serializer.errors)
|
||||
return attrs
|
||||
|
||||
def transform_data(self, obj, value):
|
||||
if not obj:
|
||||
return {}
|
||||
if obj.method:
|
||||
plugin = PaymentMethod.get_plugin(obj.method)
|
||||
serializer_class = plugin().get_serializer()
|
||||
return serializer_class().to_native(obj.data)
|
||||
return obj.data
|
||||
|
||||
def metadata(self):
|
||||
meta = super(PaymentSourceSerializer, self).metadata()
|
||||
meta['data'] = {
|
||||
method.get_plugin_name(): method().get_serializer()().metadata()
|
||||
for method in PaymentMethod.get_plugins()
|
||||
}
|
||||
return meta
|
||||
|
||||
|
||||
class TransactionSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = PaymentSource
|
||||
model = Transaction
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.db import models
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
from orchestra.core import accounts
|
||||
|
||||
from . import settings
|
||||
|
||||
|
@ -32,4 +32,4 @@ class Rate(models.Model):
|
|||
return "{}-{}".format(str(self.value), self.quantity)
|
||||
|
||||
|
||||
services.register(Pack, menu=False)
|
||||
accounts.register(Pack)
|
||||
|
|
|
@ -25,10 +25,11 @@ class ResourceSerializer(serializers.ModelSerializer):
|
|||
|
||||
if not running_syncdb():
|
||||
# TODO why this is even loaded during syncdb?
|
||||
# Create nested serializers on target models
|
||||
for ct, resources in Resource.objects.group_by('content_type'):
|
||||
model = ct.model_class()
|
||||
try:
|
||||
router.insert(model, 'resources', ResourceSerializer, required=False, many=True)
|
||||
router.insert(model, 'resources', ResourceSerializer, required=False, many=True, source='resource_set')
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class SystemUserBackend(ServiceController):
|
|||
if [[ $( id %(username)s ) ]]; then
|
||||
usermod --password '%(password)s' %(username)s
|
||||
else
|
||||
useradd %(username)s --password '%(password)s' \
|
||||
useradd %(username)s --password '%(password)s' \\
|
||||
--shell /bin/false
|
||||
fi
|
||||
mkdir -p %(home)s
|
||||
|
@ -74,8 +74,8 @@ class FTPTraffic(ServiceMonitor):
|
|||
INI_DATE=$2
|
||||
USERNAME="$3"
|
||||
LOG_FILE="$4"
|
||||
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \
|
||||
| grep " \\[${USERNAME}\\] " \
|
||||
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
|
||||
| grep " \\[${USERNAME}\\] " \\
|
||||
| awk -v ini="${INI_DATE}" '
|
||||
BEGIN {
|
||||
end = "%s"
|
||||
|
|
|
@ -24,20 +24,20 @@ class Apache2Backend(ServiceController):
|
|||
extra_conf += self.get_security(site)
|
||||
context['extra_conf'] = extra_conf
|
||||
|
||||
apache_conf = Template(
|
||||
"# {{ banner }}\n"
|
||||
"<VirtualHost *:{{ site.port }}>\n"
|
||||
" ServerName {{ site.domains.all|first }}\n"
|
||||
"{% if site.domains.all|slice:\"1:\" %}"
|
||||
" ServerAlias {{ site.domains.all|slice:\"1:\"|join:' ' }}\n"
|
||||
"{% endif %}"
|
||||
" CustomLog {{ logs }} common\n"
|
||||
" SuexecUserGroup {{ user }} {{ group }}\n"
|
||||
"{% for line in extra_conf.splitlines %}"
|
||||
" {{ line | safe }}\n"
|
||||
"{% endfor %}"
|
||||
"</VirtualHost>\n"
|
||||
)
|
||||
apache_conf = Template(textwrap.dedent("""\
|
||||
# {{ banner }}
|
||||
<VirtualHost *:{{ site.port }}>
|
||||
ServerName {{ site.domains.all|first }}
|
||||
{% if site.domains.all|slice:"1:" %}
|
||||
ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}
|
||||
{% endif %}
|
||||
CustomLog {{ logs }} common
|
||||
SuexecUserGroup {{ user }} {{ group }}
|
||||
{% for line in extra_conf.splitlines %}"
|
||||
{{ line | safe }}
|
||||
{% endfor %}
|
||||
</VirtualHost>"""
|
||||
))
|
||||
apache_conf = apache_conf.render(Context(context))
|
||||
apache_conf += self.get_protections(site)
|
||||
context['apache_conf'] = apache_conf
|
||||
|
|
|
@ -144,6 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
|
|||
'orchestra.apps.orders.models.Order',
|
||||
'orchestra.apps.prices.models.Pack',
|
||||
'orchestra.apps.bills.models.Bill',
|
||||
# 'orchestra.apps.payments.models.PaymentSource',
|
||||
'orchestra.apps.payments.models.Transaction',
|
||||
'orchestra.apps.issues.models.Ticket',
|
||||
),
|
||||
|
@ -184,6 +185,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
|||
'orders/service': 'price.png',
|
||||
'prices/pack': 'Pack.png',
|
||||
'bills/bill': 'invoice.png',
|
||||
'payments/paymentsource': 'card_in_use.png',
|
||||
'payments/transaction': 'transaction.png',
|
||||
'issues/ticket': 'Ticket_star.png',
|
||||
# Administration
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Service(object):
|
||||
_registry = {}
|
||||
class Register(object):
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._registry
|
||||
|
@ -18,4 +19,6 @@ class Service(object):
|
|||
return self._registry
|
||||
|
||||
|
||||
services = Service()
|
||||
services = Register()
|
||||
# TODO rename to something else
|
||||
accounts = Register()
|
||||
|
|
|
@ -1,38 +1,21 @@
|
|||
def group_by(qset, *fields):
|
||||
def group_by(qset, *fields, **kwargs):
|
||||
""" group_by iterator with support for multiple nested fields """
|
||||
def nest(objects, ix):
|
||||
objs = []
|
||||
result = []
|
||||
first = True
|
||||
for obj in objects:
|
||||
current = getattr(obj, fields[ix])
|
||||
if first or current == previous:
|
||||
objs.append(obj)
|
||||
else:
|
||||
if ix < len(fields)-1:
|
||||
objs = nest(list(objs), ix+1)
|
||||
result.append((previous, objs))
|
||||
objs = [obj]
|
||||
previous = current
|
||||
first = False
|
||||
if ix < len(fields)-1:
|
||||
objs = nest(list(objs), ix+1)
|
||||
result.append((current, objs))
|
||||
return result
|
||||
|
||||
objs = []
|
||||
ix = kwargs.get('ix', 0)
|
||||
if ix is 0:
|
||||
qset = qset.order_by(*fields)
|
||||
group = []
|
||||
first = True
|
||||
for obj in qset.order_by(*fields):
|
||||
current = getattr(obj, fields[0])
|
||||
for obj in qset:
|
||||
current = getattr(obj, fields[ix])
|
||||
if first or current == previous:
|
||||
objs.append(obj)
|
||||
group.append(obj)
|
||||
else:
|
||||
if len(fields) > 1:
|
||||
objs = nest(objs, 1)
|
||||
yield previous, objs
|
||||
objs = [obj]
|
||||
if ix < len(fields)-1:
|
||||
group = group_by(group, *fields, ix=ix+1)
|
||||
yield previous, group
|
||||
group = [obj]
|
||||
previous = current
|
||||
first = False
|
||||
if len(fields) > 1:
|
||||
objs = nest(objs, 1)
|
||||
yield current, objs
|
||||
if ix < len(fields)-1:
|
||||
group = group_by(group, *fields, ix=ix+1)
|
||||
yield previous, group
|
||||
|
|
BIN
orchestra/static/orchestra/icons/card_in_use.png
Normal file
BIN
orchestra/static/orchestra/icons/card_in_use.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
53
orchestra/static/orchestra/icons/card_in_use.svg
Normal file
53
orchestra/static/orchestra/icons/card_in_use.svg
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 24 24"
|
||||
id="Layer_1"
|
||||
version="1.0"
|
||||
viewBox="0 0 48 48"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
width="100%"
|
||||
height="100%"
|
||||
sodipodi:docname="card_in_use.svg"
|
||||
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/card_in_use.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"><metadata
|
||||
id="metadata11"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs9" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
id="namedview7"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.9532167"
|
||||
inkscape:cx="8.2115642"
|
||||
inkscape:cy="-0.18909649"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><path
|
||||
d="m 40.305084,9.1949147 h -20 c -2.2,0 -4,1.8000003 -4,4.0000003 v 14 c 0,0 10.6,-6.6 13.2,-4 2.6,2.6 -2.6,8 -2.6,8 h 13.4 c 2.2,0 4,-1.8 4,-4 v -14 c 0,-2.2 -1.8,-4.0000003 -4,-4.0000003 z m 0,8.0000003 h -20 v -4 h 20 v 4 z"
|
||||
id="path3"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2e3436" /><path
|
||||
d="m 17.905084,13.194915 c -1.2,0 -3.2,0.6 -5.4,2.6 l -6.1999996,5.4 v 16 h 7.5999996 c 6.8,0 8.4,-2 11.4,-4.8 3,-2.8 7.2,-5.8 4.2,-9.2 -1.8,-2.2 -4.2,-1.8 -8.4,1.2 -2.2,1.4 -4.2,2.8 -4.2,2.8"
|
||||
stroke-miterlimit="10"
|
||||
id="path5"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#2e3436;stroke-width:4.00000047999999975;stroke-linecap:round;stroke-miterlimit:10" /></svg>
|
After Width: | Height: | Size: 2.3 KiB |
Loading…
Reference in a new issue