Added database translations support

This commit is contained in:
Marc Aymerich 2015-03-29 16:10:07 +00:00
parent 124124da6c
commit bcfc453a95
35 changed files with 689 additions and 83 deletions

55
TODO.md
View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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)

View File

@ -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):

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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',))

View File

@ -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',))

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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',))

View File

@ -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)

View File

@ -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',))

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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:

View File

@ -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 """

View File

@ -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()

View File

@ -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} \

View File

View File

@ -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 = ''

View File

@ -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

View File

@ -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)