Added database translations support
This commit is contained in:
parent
124124da6c
commit
bcfc453a95
55
TODO.md
55
TODO.md
|
@ -83,7 +83,8 @@
|
|||
|
||||
* print open invoices as proforma?
|
||||
|
||||
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest
|
||||
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest --nologcapture¶
|
||||
|
||||
|
||||
|
||||
* ForeignKey.swappable
|
||||
|
@ -222,13 +223,55 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
|
|||
* autoexpand mailbox.filter according to filtering options
|
||||
|
||||
* allow empty metric pack for default rates? changes on rating algo
|
||||
* rates plan verbose name!"!
|
||||
* IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0
|
||||
* IMPORTANT maildis updae and metric storage ?? threshold ? or what?
|
||||
* IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0 or quantity 0 ?
|
||||
|
||||
* Improve performance of admin change lists with debug toolbar and prefech_related
|
||||
* and miscellaneous.service.name == 'domini-registre'
|
||||
* DOMINI REGISTRE MIGRATION SCRIPTS
|
||||
|
||||
* detect subdomains accounts correctly with subdomains: i.e. www.marcay.pangea.org
|
||||
* lines too long on invoice, double lines or cut
|
||||
* lines too long on invoice, double lines or cut, and make margin wider
|
||||
* PHP_TIMEOUT env variable in sync with fcgid idle timeout
|
||||
|
||||
* payment methods icons
|
||||
* use server.name | server.address on python backends, like gitlab instead of settings?
|
||||
* saas change password feature (the only way of re.running a backend)
|
||||
|
||||
* TODO raise404, here and everywhere
|
||||
* display subline links on billlines
|
||||
* update service orders on a celery task?
|
||||
|
||||
* billline quantity eval('10x100') instead of miningless description '(10*100)'
|
||||
|
||||
* order metric increases inside billed until period
|
||||
* do more test, make sure billed until doesn't get uodated whhen services are billed with les metric, and don't upgrade billed_until when undoing under this circumstances
|
||||
|
||||
* move normurlpath to orchestra.utils from websites.utils
|
||||
|
||||
* one time service metric change should update last value, only record for recurring invoicing.
|
||||
|
||||
* write down insights
|
||||
|
||||
* pluggable rate algorithms, with help_text, and change some services to match price
|
||||
|
||||
* translation app, with generates the trans files from models
|
||||
* use english on services defs and so on, an translate them on render time
|
||||
* (miscellaneous.service.ident or '').startswith()
|
||||
|
||||
|
||||
|
||||
Translation
|
||||
-----------
|
||||
|
||||
python manage.py makemessages -l ca --domain database
|
||||
|
||||
mkdir locale
|
||||
django-admin.py makemessages -l ca
|
||||
django-admin.py compilemessages -l ca
|
||||
|
||||
https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#joining-strings-string-concat
|
||||
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils import translation
|
||||
translation.activate('ca')
|
||||
ugettext("Fuck you")
|
||||
|
||||
|
|
|
@ -111,3 +111,68 @@ def send_bills(modeladmin, request, queryset):
|
|||
modeladmin.log_change(request, bill, 'Sent')
|
||||
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
|
||||
send_bills.url_name = 'send'
|
||||
|
||||
|
||||
def undo_billing(modeladmin, request, queryset):
|
||||
group = {}
|
||||
for line in queryset.select_related('order'):
|
||||
if line.order_id:
|
||||
try:
|
||||
group[line.order].append(line)
|
||||
except KeyError:
|
||||
group[line.order] = [line]
|
||||
# TODO force incomplete info
|
||||
for order, lines in group.iteritems():
|
||||
# Find path from ini to end
|
||||
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
||||
if not getattr(self, attr):
|
||||
raise ValidationError(_("Not enough information stored for undoing"))
|
||||
sorted(lines, key=lambda l: l.created_on)
|
||||
if 'a' != order.billed_on:
|
||||
raise ValidationError(_("Dates don't match"))
|
||||
prev = order.billed_on
|
||||
for ix in xrange(0, len(lines)):
|
||||
if lines[ix].order_b: # TODO we need to look at the periods here
|
||||
pass
|
||||
order.billed_until = self.order_billed_until
|
||||
order.billed_on = self.order_billed_on
|
||||
|
||||
# TODO son't check for account equality
|
||||
def move_lines(modeladmin, request, queryset):
|
||||
# Validate
|
||||
account = None
|
||||
for line in queryset.select_related('bill'):
|
||||
bill = line.bill
|
||||
if bill.state != bill.OPEN:
|
||||
messages.error(request, _("Can not move lines which are not in open state."))
|
||||
return
|
||||
elif not account:
|
||||
account = bill.account
|
||||
elif bill.account != account:
|
||||
messages.error(request, _("Can not move lines from different accounts"))
|
||||
return
|
||||
target = request.GET.get('target')
|
||||
if not target:
|
||||
# select target
|
||||
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
||||
target = Bill.objects.get(pk=int(pk))
|
||||
if target.account != account:
|
||||
messages.error(request, _("Target account different than lines account."))
|
||||
return
|
||||
if request.POST.get('post') == 'generic_confirmation':
|
||||
for line in queryset:
|
||||
line.bill = target
|
||||
line.save(update_fields=['bill'])
|
||||
# TODO bill history update
|
||||
messages.success(request, _("Lines moved"))
|
||||
# Final confirmation
|
||||
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
||||
|
||||
|
||||
def copy_lines(modeladmin, request, queryset):
|
||||
# same as move, but changing action behaviour
|
||||
pass
|
||||
|
||||
|
||||
def delete_lines(modeladmin, request, queryset):
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django import forms
|
||||
from django.conf.urls import patterns, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.core.urlresolvers import reverse
|
||||
|
@ -12,8 +13,7 @@ from orchestra.admin.utils import admin_date, insertattr
|
|||
from orchestra.apps.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||
|
||||
from . import settings
|
||||
from .actions import download_bills, view_bill, close_bills, send_bills, validate_contact
|
||||
from . import settings, actions
|
||||
from .filters import BillTypeListFilter, HasBillContactListFilter
|
||||
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
|
||||
|
||||
|
@ -84,6 +84,36 @@ class ClosedBillLineInline(BillLineInline):
|
|||
return False
|
||||
|
||||
|
||||
class BillLineManagerAdmin(admin.ModelAdmin):
|
||||
list_display = ('description', 'rate', 'quantity', 'tax', 'subtotal')
|
||||
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qset = super(BillLineManagerAdmin, self).get_queryset(request)
|
||||
return qset.filter(bill_id__in=self.bill_ids)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
GET = request.GET.copy()
|
||||
bill_ids = GET.pop('bill_ids', ['0'])[0]
|
||||
request.GET = GET
|
||||
bill_ids = [int(id) for id in bill_ids.split(',')]
|
||||
self.bill_ids = bill_ids
|
||||
if not bill_ids:
|
||||
return
|
||||
elif len(bill_ids) > 1:
|
||||
title = _("Manage bill lines of multiple bills.")
|
||||
else:
|
||||
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
|
||||
bill = Bill.objects.get(pk=bill_ids[0])
|
||||
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.ident)
|
||||
title = mark_safe(_("Manage %s bill lines.") % bill_link)
|
||||
context = {
|
||||
'title': title,
|
||||
}
|
||||
context.update(extra_context or {})
|
||||
return super(BillLineManagerAdmin, self).changelist_view(request, context)
|
||||
|
||||
|
||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'number', 'type_link', 'account_link', 'created_on_display',
|
||||
|
@ -101,8 +131,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
'fields': ('html',),
|
||||
}),
|
||||
)
|
||||
actions = [download_bills, close_bills, send_bills]
|
||||
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
|
||||
change_view_actions = [
|
||||
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
|
||||
]
|
||||
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
||||
inlines = [BillLineInline, ClosedBillLineInline]
|
||||
|
@ -144,6 +176,17 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
display_payment_state.allow_tags = True
|
||||
display_payment_state.short_description = _("Payment")
|
||||
|
||||
def get_urls(self):
|
||||
""" Hook bill lines management URLs on bill admin """
|
||||
urls = super(BillAdmin, self).get_urls()
|
||||
admin_site = self.admin_site
|
||||
extra_urls = patterns("",
|
||||
url("^manage-lines/$",
|
||||
admin_site.admin_view(BillLineManagerAdmin(BillLine, admin_site).changelist_view),
|
||||
name='bills_bill_manage_lines'),
|
||||
)
|
||||
return extra_urls + urls
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = super(BillAdmin, self).get_readonly_fields(request, obj)
|
||||
if obj and not obj.is_open:
|
||||
|
@ -187,7 +230,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
def change_view(self, request, object_id, **kwargs):
|
||||
# TODO raise404, here and everywhere
|
||||
bill = self.get_object(request, unquote(object_id))
|
||||
validate_contact(request, bill, error=False)
|
||||
actions.validate_contact(request, bill, error=False)
|
||||
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,346 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-03-29 10:17+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: actions.py:35
|
||||
msgid "Download"
|
||||
msgstr "Blod"
|
||||
|
||||
#: actions.py:45
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:53
|
||||
msgid "Selected bills should be in open state"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:71
|
||||
msgid "Selected bills have been closed"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:80
|
||||
#, python-format
|
||||
msgid "<a href=\"%s\">One related transaction</a> has been created"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:81
|
||||
#, python-format
|
||||
msgid "<a href=\"%s\">%i related transactions</a> have been created"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:87
|
||||
msgid "Are you sure about closing the following bills?"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:88
|
||||
msgid ""
|
||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||
"payment source for the selected bills"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:101
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:112
|
||||
msgid "Resend"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:129 models.py:308
|
||||
msgid "Not enough information stored for undoing"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:132 models.py:310
|
||||
msgid "Dates don't match"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:147
|
||||
msgid "Can not move lines which are not in open state."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:152
|
||||
msgid "Can not move lines from different accounts"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:160
|
||||
msgid "Target account different than lines account."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:167
|
||||
msgid "Lines moved"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:41 forms.py:12
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:69
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:77
|
||||
msgid "Subtotal"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:104
|
||||
msgid "Manage bill lines of multiple bills."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:109
|
||||
#, python-format
|
||||
msgid "Manage %s bill lines."
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:129
|
||||
msgid "Raw"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:147
|
||||
msgid "lines"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:152
|
||||
msgid "total"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:160 models.py:85 models.py:339
|
||||
msgid "type"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:177
|
||||
msgid "Payment"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:17
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:18 models.py:75
|
||||
msgid "Invoice"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:19 models.py:76
|
||||
msgid "Amendment invoice"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:20 models.py:77
|
||||
msgid "Fee"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:21
|
||||
msgid "Amendment fee"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:22
|
||||
msgid "Pro-forma"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:42
|
||||
msgid "has bill contact"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:47
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:48
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:9
|
||||
msgid "Number"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:11
|
||||
msgid "Account"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:13
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:10
|
||||
msgid ""
|
||||
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:17
|
||||
msgid "Related"
|
||||
msgstr ""
|
||||
|
||||
#: helpers.py:24
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:20 models.py:83
|
||||
msgid "account"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:22
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:23
|
||||
msgid "Account full name will be used when left blank."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:24
|
||||
msgid "address"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:25
|
||||
msgid "city"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:27
|
||||
msgid "zip code"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:28
|
||||
msgid "Enter a valid zipcode."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:29
|
||||
msgid "country"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:32
|
||||
msgid "VAT number"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:64
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:65
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:66
|
||||
msgid "Bad debt"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:78
|
||||
msgid "Amendment Fee"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:79
|
||||
msgid "Pro forma"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:82
|
||||
msgid "number"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:86
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:87
|
||||
msgid "closed on"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:88
|
||||
msgid "open"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:89
|
||||
msgid "sent"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90
|
||||
msgid "due on"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:91
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:93
|
||||
msgid "comments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:94
|
||||
msgid "HTML"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:270
|
||||
msgid "bill"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:271 models.py:336
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:272
|
||||
msgid "rate"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:273
|
||||
msgid "quantity"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:274
|
||||
msgid "subtotal"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:275
|
||||
msgid "tax"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:281
|
||||
msgid "Informative link back to the order"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:282
|
||||
msgid "order billed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:283
|
||||
msgid "order billed until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:284
|
||||
msgid "created"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:286
|
||||
msgid "amended line"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:329
|
||||
msgid "Volume"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:330
|
||||
msgid "Compensation"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:331
|
||||
msgid "Other"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:335
|
||||
msgid "bill line"
|
||||
msgstr ""
|
|
@ -274,8 +274,11 @@ class BillLine(models.Model):
|
|||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||
tax = models.PositiveIntegerField(_("tax"))
|
||||
# Undo
|
||||
# initial = models.DateTimeField(null=True)
|
||||
# end = models.DateTimeField(null=True)
|
||||
|
||||
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
||||
help_text=_("Informative link back to the order"))
|
||||
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
|
||||
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
||||
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
|
||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
|
|
|
@ -30,34 +30,33 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
|
|||
return target
|
||||
|
||||
def clean(self):
|
||||
""" inherit related top domain account, when exists """
|
||||
""" inherit related parent domain account, when exists """
|
||||
cleaned_data = super(BatchDomainCreationAdminForm, self).clean()
|
||||
if not cleaned_data['account']:
|
||||
account = None
|
||||
for name in [cleaned_data['name']] + self.extra_names:
|
||||
domain = Domain(name=name)
|
||||
top = domain.get_top()
|
||||
if not top:
|
||||
parent = domain.get_parent()
|
||||
if not parent:
|
||||
# Fake an account to make django validation happy
|
||||
account_model = self.fields['account']._queryset.model
|
||||
cleaned_data['account'] = account_model()
|
||||
raise ValidationError({
|
||||
'account': _("An account should be provided for top domain names."),
|
||||
})
|
||||
elif account and top.account != account:
|
||||
elif account and parent.account != account:
|
||||
# Fake an account to make django validation happy
|
||||
account_model = self.fields['account']._queryset.model
|
||||
cleaned_data['account'] = account_model()
|
||||
raise ValidationError({
|
||||
'account': _("Provided domain names belong to different accounts."),
|
||||
})
|
||||
account = top.account
|
||||
account = parent.account
|
||||
cleaned_data['account'] = account
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class RecordInlineFormSet(forms.models.BaseInlineFormSet):
|
||||
# TODO
|
||||
def clean(self):
|
||||
""" Checks if everything is consistent """
|
||||
if any(self.errors):
|
||||
|
|
|
@ -17,7 +17,7 @@ def domain_for_validation(instance, records):
|
|||
|
||||
if not domain.pk:
|
||||
# top domain lookup for new domains
|
||||
domain.top = domain.get_top()
|
||||
domain.top = domain.get_parent(top=True)
|
||||
if domain.top:
|
||||
# is a subdomain
|
||||
subdomains = [sub for sub in domain.top.subdomains.all() if sub.pk != domain.pk]
|
||||
|
|
|
@ -27,15 +27,18 @@ class Domain(models.Model):
|
|||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_top_domain(cls, name):
|
||||
def get_parent_domain(cls, name, top=False):
|
||||
""" get the next domain on the chain """
|
||||
split = name.split('.')
|
||||
top = None
|
||||
parent = None
|
||||
for i in range(1, len(split)-1):
|
||||
name = '.'.join(split[i:])
|
||||
domain = Domain.objects.filter(name=name)
|
||||
if domain:
|
||||
top = domain.get()
|
||||
return top
|
||||
parent = domain.get()
|
||||
if not top:
|
||||
return parent
|
||||
return parent
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
|
@ -57,7 +60,7 @@ class Domain(models.Model):
|
|||
""" create top relation """
|
||||
update = False
|
||||
if not self.pk:
|
||||
top = self.get_top()
|
||||
top = self.get_parent(top=True)
|
||||
if top:
|
||||
self.top = top
|
||||
self.account_id = self.account_id or top.account_id
|
||||
|
@ -90,8 +93,8 @@ class Domain(models.Model):
|
|||
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
||||
return self.origin.subdomain_set.all().prefetch_related('records')
|
||||
|
||||
def get_top(self):
|
||||
return type(self).get_top_domain(self.name)
|
||||
def get_parent(self, top=False):
|
||||
return type(self).get_parent_domain(self.name, top=top)
|
||||
|
||||
def render_zone(self):
|
||||
origin = self.origin
|
||||
|
|
|
@ -30,7 +30,7 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
|||
def clean_name(self, attrs, source):
|
||||
""" prevent users creating subdomains of other users domains """
|
||||
name = attrs[source]
|
||||
top = Domain.get_top_domain(name)
|
||||
top = Domain.get_parent_domain(name)
|
||||
if top and top.account != self.account:
|
||||
raise ValidationError(_("Can not create subdomains of other users domains"))
|
||||
return attrs
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from orchestra.apps.contacts import settings as contacts_settings
|
||||
from orchestra.apps.contacts.models import Contact
|
||||
from orchestra.core.translations import ModelTranslation
|
||||
from orchestra.models.fields import MultiSelectField
|
||||
from orchestra.utils import send_email_template
|
||||
|
||||
|
@ -12,6 +13,7 @@ from . import settings
|
|||
|
||||
class Queue(models.Model):
|
||||
name = models.CharField(_("name"), max_length=128, unique=True)
|
||||
verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True)
|
||||
default = models.BooleanField(_("default"), default=False)
|
||||
notify = MultiSelectField(_("notify"), max_length=256, blank=True,
|
||||
choices=Contact.EMAIL_USAGES,
|
||||
|
@ -19,7 +21,7 @@ class Queue(models.Model):
|
|||
help_text=_("Contacts to notify by email"))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
return self.verbose_name or self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" mark as default queue if needed """
|
||||
|
@ -190,3 +192,6 @@ class TicketTracker(models.Model):
|
|||
unique_together = (
|
||||
('ticket', 'user'),
|
||||
)
|
||||
|
||||
|
||||
ModelTranslation.register(Queue, ('verbose_name',))
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
from orchestra.core.translations import ModelTranslation
|
||||
from orchestra.core.validators import validate_name
|
||||
from orchestra.models.fields import NullableCharField
|
||||
|
||||
|
@ -74,3 +75,5 @@ class Miscellaneous(models.Model):
|
|||
|
||||
|
||||
services.register(Miscellaneous)
|
||||
|
||||
ModelTranslation.register(MiscService, ('verbose_name',))
|
||||
|
|
|
@ -40,7 +40,7 @@ class RouteAdmin(admin.ModelAdmin):
|
|||
|
||||
def display_model(self, route):
|
||||
try:
|
||||
return escape(route.backend_class().model)
|
||||
return escape(route.backend_class.model)
|
||||
except KeyError:
|
||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||
display_model.short_description = _("model")
|
||||
|
@ -48,7 +48,7 @@ class RouteAdmin(admin.ModelAdmin):
|
|||
|
||||
def display_actions(self, route):
|
||||
try:
|
||||
return '<br>'.join(route.backend_class().get_actions())
|
||||
return '<br>'.join(route.backend_class.get_actions())
|
||||
except KeyError:
|
||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||
display_actions.short_description = _("actions")
|
||||
|
|
|
@ -186,11 +186,9 @@ class Route(models.Model):
|
|||
def __unicode__(self):
|
||||
return "%s@%s" % (self.backend, self.host)
|
||||
|
||||
# def clean(self):
|
||||
# backend, method = self.get_backend_class(), self.get_method_class()
|
||||
# if not backend.type in method.types:
|
||||
# msg = _("%s backend is not compatible with %s method")
|
||||
# raise ValidationError(msg % (self.backend, self.method)
|
||||
@property
|
||||
def backend_class(self):
|
||||
return ServiceBackend.get_backend(self.backend)
|
||||
|
||||
@classmethod
|
||||
def get_servers(cls, operation, **kwargs):
|
||||
|
@ -215,6 +213,22 @@ class Route(models.Model):
|
|||
servers.append(route.host)
|
||||
return servers
|
||||
|
||||
def clean(self):
|
||||
if not self.match:
|
||||
self.match = 'True'
|
||||
if self.backend:
|
||||
backend_model = self.backend_class.model
|
||||
try:
|
||||
obj = backend_model.objects.all()[0]
|
||||
except IndexError:
|
||||
return
|
||||
try:
|
||||
bool(self.matches(obj))
|
||||
except Exception, exception:
|
||||
name = type(exception).__name__
|
||||
message = exception.message
|
||||
raise ValidationError(': '.join((name, message)))
|
||||
|
||||
def matches(self, instance):
|
||||
safe_locals = {
|
||||
'instance': instance,
|
||||
|
@ -223,15 +237,6 @@ class Route(models.Model):
|
|||
}
|
||||
return eval(self.match, safe_locals)
|
||||
|
||||
def backend_class(self):
|
||||
return ServiceBackend.get_backend(self.backend)
|
||||
|
||||
# def method_class(self):
|
||||
# for method in MethodBackend.get_backends():
|
||||
# if method.get_name() == self.method:
|
||||
# return method
|
||||
# raise ValueError('This method is not registered')
|
||||
|
||||
def enable(self):
|
||||
self.is_active = True
|
||||
self.save()
|
||||
|
|
|
@ -4,7 +4,7 @@ from orchestra.apps.accounts.models import Account
|
|||
from orchestra.core import services
|
||||
|
||||
|
||||
def get_related_objects(origin, max_depth=2):
|
||||
def get_related_object(origin, max_depth=2):
|
||||
"""
|
||||
Introspects origin object and return the first related service object
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ class OrderQuerySet(models.QuerySet):
|
|||
order.old_billed_until = order.billed_until
|
||||
lines = service.handler.generate_bill_lines(orders, account, **options)
|
||||
bill_lines.extend(lines)
|
||||
# TODO make this consistent always returning the same fucking objects
|
||||
# TODO make this consistent always returning the same fucking types
|
||||
if commit:
|
||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||
else:
|
||||
|
@ -257,7 +257,8 @@ class MetricStorage(models.Model):
|
|||
except cls.DoesNotExist:
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
if metric.value != value:
|
||||
threshold = decimal.Decimal(settings.ORDERS_METRIC_THRESHOLD)
|
||||
if metric.value*(1+threshold) > value or metric.value*threshold < value:
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
metric.updated_on = now
|
||||
|
@ -276,7 +277,7 @@ def cancel_orders(sender, **kwargs):
|
|||
for order in Order.objects.by_object(instance).active():
|
||||
order.cancel()
|
||||
elif not hasattr(instance, 'account'):
|
||||
related = helpers.get_related_objects(instance)
|
||||
related = helpers.get_related_object(instance)
|
||||
if related and related != instance:
|
||||
Order.update_orders(related)
|
||||
|
||||
|
@ -287,6 +288,6 @@ def update_orders(sender, **kwargs):
|
|||
if type(instance) in services:
|
||||
Order.update_orders(instance)
|
||||
elif not hasattr(instance, 'account'):
|
||||
related = helpers.get_related_objects(instance)
|
||||
related = helpers.get_related_object(instance)
|
||||
if related and related != instance:
|
||||
Order.update_orders(related)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
# Pluggable backend for bill generation.
|
||||
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
||||
'orchestra.apps.orders.billing.BillsBackend')
|
||||
|
||||
|
||||
# Pluggable service class
|
||||
ORDERS_SERVICE_MODEL = getattr(settings, 'ORDERS_SERVICE_MODEL', 'services.Service')
|
||||
|
||||
|
||||
# Prevent inspecting these apps for service accounting
|
||||
ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
||||
'orders',
|
||||
'admin',
|
||||
|
@ -19,3 +22,8 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
|||
'bills',
|
||||
'services',
|
||||
))
|
||||
|
||||
|
||||
# Only account for significative changes
|
||||
# metric_storage new value: lastvalue*(1+threshold) > currentvalue or lastvalue*threshold < currentvalue
|
||||
ORDERS_METRIC_THRESHOLD = getattr(settings, 'ORDERS_METRIC_THRESHOLD', 0.4)
|
||||
|
|
|
@ -21,7 +21,7 @@ class PaymentSource(models.Model):
|
|||
related_name='paymentsources')
|
||||
method = models.CharField(_("method"), max_length=32,
|
||||
choices=PaymentMethod.get_plugin_choices())
|
||||
data = JSONField(_("data"))
|
||||
data = JSONField(_("data"), default={})
|
||||
is_active = models.BooleanField(_("active"), default=True)
|
||||
|
||||
objects = PaymentSourcesQueryset.as_manager()
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.db.models import Q
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services, accounts
|
||||
from orchestra.core.translations import ModelTranslation
|
||||
from orchestra.core.validators import validate_name
|
||||
from orchestra.models import queryset
|
||||
|
||||
|
@ -89,3 +90,5 @@ class Rate(models.Model):
|
|||
|
||||
accounts.register(ContractedPlan)
|
||||
services.register(ContractedPlan, menu=False)
|
||||
|
||||
ModelTranslation.register(Plan, ('verbose_name',))
|
||||
|
|
|
@ -15,7 +15,7 @@ class PhpListSaaSBackend(ServiceController):
|
|||
default_route_match = "saas.service == 'phplist'"
|
||||
block = True
|
||||
|
||||
def initialize_database(self, saas, server):
|
||||
def _save(self, saas, server):
|
||||
base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
|
||||
admin_link = 'http://%s/admin/' % saas.get_site_domain()
|
||||
admin_content = requests.get(admin_link).content
|
||||
|
@ -25,21 +25,21 @@ class PhpListSaaSBackend(ServiceController):
|
|||
if install:
|
||||
if not hasattr(saas, 'password'):
|
||||
raise RuntimeError("Password is missing")
|
||||
install = install.groups()[0]
|
||||
install_link = admin_link + install[1:]
|
||||
install_path = install.groups()[0]
|
||||
install_link = admin_link + install_path[1:]
|
||||
post = {
|
||||
'adminname': saas.name,
|
||||
'orgname': saas.account.username,
|
||||
'adminemail': saas.account.username,
|
||||
'adminpassword': saas.password,
|
||||
}
|
||||
print json.dumps(post, indent=4)
|
||||
response = requests.post(install_link, data=post)
|
||||
print response.content
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError("Bad status code %i" % response.status_code)
|
||||
elif hasattr(saas, 'password'):
|
||||
raise NotImplementedError
|
||||
else:
|
||||
raise NotImplementedError("Change password not implemented")
|
||||
|
||||
def save(self, saas):
|
||||
self.append(self.initialize_database, saas)
|
||||
if hasattr(saas, 'password'):
|
||||
self.append(self._save, saas)
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.module_loading import autodiscover_modules
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import caches, validators
|
||||
from orchestra.core.translations import ModelTranslation
|
||||
from orchestra.core.validators import validate_name
|
||||
from orchestra.models import queryset
|
||||
|
||||
|
@ -240,3 +241,6 @@ class Service(models.Model):
|
|||
for instance in related_model.objects.all().select_related('account'):
|
||||
updates += order_model.update_orders(instance, service=self, commit=commit)
|
||||
return updates
|
||||
|
||||
|
||||
ModelTranslation.register(Service, ('description',))
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||
from orchestra.apps.plans.models import Plan
|
||||
from orchestra.utils.tests import random_ascii
|
||||
|
||||
from ...models import Service, Plan
|
||||
from ...models import Service
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -19,7 +20,7 @@ class DomainBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.NOTHING,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -25,7 +25,7 @@ class FTPBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.COMPENSATE,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||
from orchestra.apps.plans.models import Plan
|
||||
from orchestra.utils.tests import random_ascii
|
||||
|
||||
from ...models import Service, Plan
|
||||
from ...models import Service
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -19,7 +20,7 @@ class JobBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric='miscellaneous.amount',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.MATCH_PRICE,
|
||||
rate_algorithm='MATCH_PRICE',
|
||||
on_cancel=Service.NOTHING,
|
||||
payment_style=Service.POSTPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -4,10 +4,11 @@ from django.utils import timezone
|
|||
from freezegun import freeze_time
|
||||
|
||||
from orchestra.apps.mailboxes.models import Mailbox
|
||||
from orchestra.apps.plans.models import Plan
|
||||
from orchestra.apps.resources.models import Resource, ResourceData
|
||||
from orchestra.utils.tests import random_ascii
|
||||
|
||||
from ...models import Service, Plan
|
||||
from ...models import Service
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -23,7 +24,7 @@ class MailboxBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.COMPENSATE,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
@ -44,7 +45,7 @@ class MailboxBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric='max((mailbox.resources.disk.allocated or 0) -1, 0)',
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from ...models import Service, Plan, ContractedPlan
|
||||
from orchestra.apps.plans.models import Plan, ContractedPlan
|
||||
|
||||
from ...models import Service
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -16,7 +18,7 @@ class PlanBillingTest(BaseBillingTest):
|
|||
is_fee=True,
|
||||
metric='',
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -5,9 +5,10 @@ from freezegun import freeze_time
|
|||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||
from orchestra.apps.plans.models import Plan
|
||||
from orchestra.apps.resources.models import Resource, ResourceData, MonitorData
|
||||
|
||||
from ...models import Service, Plan
|
||||
from ...models import Service
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -25,7 +26,7 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
|||
is_fee=False,
|
||||
metric=self.TRAFFIC_METRIC,
|
||||
pricing_period=Service.BILLING_PERIOD,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.NOTHING,
|
||||
payment_style=Service.POSTPAY,
|
||||
tax=0,
|
||||
|
@ -107,7 +108,7 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
|||
is_fee=False,
|
||||
metric="miscellaneous.amount",
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.NOTHING,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -41,7 +41,7 @@ class HandlerTests(BaseTestCase):
|
|||
is_fee=False,
|
||||
metric='',
|
||||
pricing_period=Service.NEVER,
|
||||
rate_algorithm=Service.STEP_PRICE,
|
||||
rate_algorithm='STEP_PRICE',
|
||||
on_cancel=Service.DISCOUNT,
|
||||
payment_style=Service.PREPAY,
|
||||
tax=0,
|
||||
|
|
|
@ -208,10 +208,7 @@ class Exim4Traffic(ServiceMonitor):
|
|||
with open(mainlog, 'r') as mainlog:
|
||||
for line in mainlog.readlines():
|
||||
if ' <= ' in line and 'P=local' in line:
|
||||
username = user_regex.search(line)
|
||||
if not username:
|
||||
continue
|
||||
username = username.groups()[0]
|
||||
username = user_regex.search(line).groups()[0]
|
||||
try:
|
||||
sender = users[username]
|
||||
except KeyError:
|
||||
|
@ -299,7 +296,7 @@ class FTPTraffic(ServiceMonitor):
|
|||
users[username] = [ini_date, object_id, 0]
|
||||
|
||||
def monitor(users, end_date, months, vsftplogs):
|
||||
user_regex = re.compile(r'\] \[([^ ]+)\] OK ')
|
||||
user_regex = re.compile(r'\] \[([^ ]+)\] (OK|FAIL) ')
|
||||
bytes_regex = re.compile(r', ([0-9]+) bytes, ')
|
||||
for vsftplog in vsftplogs:
|
||||
try:
|
||||
|
|
|
@ -97,7 +97,7 @@ class Apache2Backend(ServiceController):
|
|||
def delete(self, site):
|
||||
context = self.get_context(site)
|
||||
self.append("a2dissite %(site_unique_name)s.conf && UPDATED=1" % context)
|
||||
self.append("rm -fr %(sites_available)s" % context)
|
||||
self.append("rm -f %(sites_available)s" % context)
|
||||
|
||||
def commit(self):
|
||||
""" reload Apache2 if necessary """
|
||||
|
|
|
@ -11,7 +11,6 @@ from orchestra.utils.functional import cached
|
|||
|
||||
from . import settings
|
||||
from .directives import SiteDirective
|
||||
from .utils import normurlpath
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
|
@ -141,8 +140,8 @@ class Content(models.Model):
|
|||
return self.path
|
||||
|
||||
def clean(self):
|
||||
# TODO do it on the field?
|
||||
self.path = normurlpath(self.path)
|
||||
if not self.path:
|
||||
self.path = '/'
|
||||
|
||||
def get_absolute_url(self):
|
||||
domain = self.website.domains.first()
|
||||
|
|
|
@ -131,9 +131,10 @@ function install_requirements () {
|
|||
libxslt1-dev \
|
||||
wkhtmltopdf \
|
||||
xvfb \
|
||||
ca-certificates"
|
||||
ca-certificates \
|
||||
gettext"
|
||||
|
||||
PIP="django==1.7.1 \
|
||||
PIP="django==1.7.7 \
|
||||
django-celery-email==1.0.4 \
|
||||
django-fluent-dashboard==0.3.5 \
|
||||
https://bitbucket.org/izi/django-admin-tools/get/a0abfffd76a0.zip \
|
||||
|
@ -158,7 +159,8 @@ function install_requirements () {
|
|||
requests \
|
||||
phonenumbers \
|
||||
django-countries \
|
||||
django-localflavor"
|
||||
django-localflavor \
|
||||
pip==6.0.8"
|
||||
|
||||
if $testing; then
|
||||
APT="${APT} \
|
||||
|
|
|
@ -55,6 +55,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
|||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
|
||||
# Path used for database translations files
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
)
|
||||
|
||||
# EMAIL_HOST = 'smtp.yourhost.eu'
|
||||
# EMAIL_PORT = ''
|
||||
# EMAIL_HOST_USER = ''
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
class ModelTranslation(object):
|
||||
"""
|
||||
Collects all model fields that would be translated
|
||||
|
||||
using 'makemessages --domain database' management command
|
||||
"""
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, model, fields):
|
||||
if model in cls._registry:
|
||||
raise ValueError("Model %s already registered." % model.__name__)
|
||||
cls._registry[model] = fields
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
|
||||
from django.core.management.commands import makemessages
|
||||
|
||||
from orchestra.core.translations import ModelTranslation
|
||||
from orchestra.utils.paths import get_site_root
|
||||
|
||||
|
||||
class Command(makemessages.Command):
|
||||
""" Provides database translations support """
|
||||
|
||||
def handle(self, *args, **options):
|
||||
do_database = os.getcwd() == get_site_root()
|
||||
self.generated_database_files = []
|
||||
if do_database:
|
||||
self.project_locale_path = get_site_root()
|
||||
self.generate_database_files()
|
||||
super(Command, self).handle(*args, **options)
|
||||
self.remove_database_files()
|
||||
|
||||
def get_contents(self):
|
||||
for model, fields in ModelTranslation._registry.iteritems():
|
||||
contents = []
|
||||
for field in fields:
|
||||
for content in model.objects.values_list('id', field):
|
||||
pk, value = content
|
||||
contents.append(
|
||||
(pk, u"_(u'%s')" % value)
|
||||
)
|
||||
yield ('_'.join((model._meta.db_table, field)), contents)
|
||||
|
||||
def generate_database_files(self):
|
||||
""" tmp files are generated because of having a nice gettext location """
|
||||
for name, contents in self.get_contents():
|
||||
name = unicode(name)
|
||||
maximum = None
|
||||
content = {}
|
||||
for pk, value in contents:
|
||||
if not maximum or pk > maximum:
|
||||
maximum = pk
|
||||
content[pk] = value
|
||||
tmpcontent = []
|
||||
for ix in xrange(maximum+1):
|
||||
tmpcontent.append(content.get(ix, ''))
|
||||
tmpcontent = u'\n'.join(tmpcontent) + '\n'
|
||||
filepath = os.path.join(self.project_locale_path, 'database_%s.sql.py' % name)
|
||||
self.generated_database_files.append(filepath)
|
||||
with open(filepath, 'w') as tmpfile:
|
||||
tmpfile.write(tmpcontent.encode('utf-8'))
|
||||
|
||||
def remove_database_files(self):
|
||||
for path in self.generated_database_files:
|
||||
os.unlink(path)
|
Loading…
Reference in New Issue