Random fixes

This commit is contained in:
Marc Aymerich 2015-04-20 14:23:10 +00:00
parent 5d95e64965
commit 9ac9f6b933
37 changed files with 4019 additions and 385 deletions

View file

@ -276,8 +276,14 @@ https://code.djangoproject.com/ticket/24576
* force ignore slack billing period overridig when billing * force ignore slack billing period overridig when billing
* fpm reload starts new pools? * fpm reload starts new pools?
* rename resource.monitors to resource.backends ? * rename resource.monitors to resource.backends ?
* abstract model classes enabling overriding? * abstract model classes that enabling overriding, and ORCHESTRA_DATABASE_MODEL settings + orchestra.get_database_model() instead of explicitly importing from orchestra.contrib.databases.models import Database.. (Admin and REST API are fucked then?)
# Ignore superusers & co on billing: list filter doesn't work nor ignore detection # Ignore superusers & co on billing: list filter doesn't work nor ignore detection
# bill.totals make it 100% computed? # bill.totals make it 100% computed?
* joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz - * joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz -
# Link related orders on bill line
# Customize those service.descriptions that are
# replace multichoicefield and jsonfield by ArrayField, HStoreField

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -107,10 +107,15 @@ def admin_link(*args, **kwargs):
if not getattr(obj, 'pk', None): if not getattr(obj, 'pk', None):
return '---' return '---'
url = change_url(obj) url = change_url(obj)
display = kwargs.get('display')
if display:
display = getattr(obj, display, 'merda')
else:
display = obj
extra = '' extra = ''
if kwargs['popup']: if kwargs['popup']:
extra = 'onclick="return showAddAnotherPopup(this);"' extra = 'onclick="return showAddAnotherPopup(this);"'
return '<a href="%s" %s>%s</a>' % (url, extra, obj) return '<a href="%s" %s>%s</a>' % (url, extra, display)
@admin_field @admin_field

View file

@ -35,6 +35,7 @@ MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*' ALLOWED_HOSTS = '*'
# Set this to True to wrap each HTTP request in a transaction on this database. # Set this to True to wrap each HTTP request in a transaction on this database.
# ATOMIC REQUESTS do not wrap middlewares (orchestra.contrib.orchestration.middlewares.OperationsMiddleware) # ATOMIC REQUESTS do not wrap middlewares (orchestra.contrib.orchestration.middlewares.OperationsMiddleware)
ATOMIC_REQUESTS = False ATOMIC_REQUESTS = False
@ -101,6 +102,7 @@ INSTALLED_APPS = (
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'passlib.ext.django', 'passlib.ext.django',
'django_countries',
# Django.contrib # Django.contrib
'django.contrib.auth', 'django.contrib.auth',

View file

@ -40,6 +40,7 @@ DATABASES = {
'PASSWORD': 'orchestra', # Not used with sqlite3. 'PASSWORD': 'orchestra', # Not used with sqlite3.
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3. 'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3.
'CONN_MAX_AGE': 300,
} }
} }

View file

@ -11,7 +11,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_date, insertattr from orchestra.admin.utils import admin_date, insertattr, admin_link
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple from orchestra.forms.widgets import paddingCheckboxSelectMultiple
@ -29,8 +29,13 @@ PAYMENT_STATE_COLORS = {
class BillLineInline(admin.TabularInline): class BillLineInline(admin.TabularInline):
model = BillLine model = BillLine
fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'display_total') fields = (
readonly_fields = ('display_total',) 'description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
'subtotal', 'display_total',
)
readonly_fields = ('display_total', 'order_link')
order_link = admin_link('order', display='pk')
def display_total(self, line): def display_total(self, line):
total = line.get_total() total = line.get_total()
@ -46,9 +51,9 @@ class BillLineInline(admin.TabularInline):
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """
if db_field.name == 'description': if db_field.name == 'description':
kwargs['widget'] = forms.TextInput(attrs={'size':'110'}) kwargs['widget'] = forms.TextInput(attrs={'size':'50'})
else: elif db_field.name not in ('start_on', 'end_on'):
kwargs['widget'] = forms.TextInput(attrs={'size':'13'}) kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs) return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
def get_queryset(self, request): def get_queryset(self, request):
@ -61,7 +66,8 @@ class ClosedBillLineInline(BillLineInline):
# https://code.djangoproject.com/ticket/9025 # https://code.djangoproject.com/ticket/9025
fields = ( fields = (
'display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total' 'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
'display_subtotal', 'display_total'
) )
readonly_fields = fields readonly_fields = fields
@ -157,10 +163,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines") num_lines.short_description = _("lines")
def display_total(self, bill): def display_total(self, bill):
return "%s &%s;" % (round(bill.totals, 2), settings.BILLS_CURRENCY.lower()) return "%s &%s;" % (round(bill.computed_total or 0, 2), settings.BILLS_CURRENCY.lower())
display_total.allow_tags = True display_total.allow_tags = True
display_total.short_description = _("total") display_total.short_description = _("total")
display_total.admin_order_field = 'totals' display_total.admin_order_field = 'computed_total'
def type_link(self, bill): def type_link(self, bill):
bill_type = bill.type.lower() bill_type = bill.type.lower()
@ -235,7 +241,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(BillAdmin, self).get_queryset(request) qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate( qs = qs.annotate(
models.Count('lines'), models.Count('lines'),
totals=Sum( computed_total=Sum(
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100) (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
), ),
) )

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-03-29 20:21+0000\n" "POT-Creation-Date: 2015-04-20 11:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,112 +18,112 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:35 #: actions.py:36
msgid "Download" msgid "Download"
msgstr "Descarrega" msgstr "Descarrega"
#: actions.py:45 #: actions.py:46
msgid "View" msgid "View"
msgstr "Vista" msgstr "Vista"
#: actions.py:53 #: actions.py:54
msgid "Selected bills should be in open state" msgid "Selected bills should be in open state"
msgstr "" msgstr ""
#: actions.py:71 #: actions.py:72
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "" msgstr ""
#: actions.py:80
#, python-format
msgid "<a href=\"%s\">One related transaction</a> has been created"
msgstr ""
#: actions.py:81 #: actions.py:81
#, python-format #, python-format
msgid "<a href=\"%s\">One related transaction</a> has been created"
msgstr ""
#: actions.py:82
#, python-format
msgid "<a href=\"%s\">%i related transactions</a> have been created" msgid "<a href=\"%s\">%i related transactions</a> have been created"
msgstr "" msgstr ""
#: actions.py:87 #: actions.py:88
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "" msgstr ""
#: actions.py:88 #: actions.py:89
msgid "" msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a " "Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills" "payment source for the selected bills"
msgstr "" msgstr ""
#: actions.py:101 #: actions.py:102
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: actions.py:112 #: actions.py:113
msgid "Resend" msgid "Resend"
msgstr "" msgstr ""
#: actions.py:129 models.py:309 #: actions.py:130 models.py:312
msgid "Not enough information stored for undoing" msgid "Not enough information stored for undoing"
msgstr "" msgstr ""
#: actions.py:132 models.py:311 #: actions.py:133 models.py:314
msgid "Dates don't match" msgid "Dates don't match"
msgstr "" msgstr ""
#: actions.py:147 #: actions.py:148
msgid "Can not move lines which are not in open state." msgid "Can not move lines which are not in open state."
msgstr "" msgstr ""
#: actions.py:152 #: actions.py:153
msgid "Can not move lines from different accounts" msgid "Can not move lines from different accounts"
msgstr "" msgstr ""
#: actions.py:160 #: actions.py:161
msgid "Target account different than lines account." msgid "Target account different than lines account."
msgstr "" msgstr ""
#: actions.py:167 #: actions.py:168
msgid "Lines moved" msgid "Lines moved"
msgstr "" msgstr ""
#: admin.py:41 forms.py:12 #: admin.py:43 admin.py:86 forms.py:11
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: admin.py:69 #: admin.py:73
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: admin.py:77 #: admin.py:81
msgid "Subtotal" msgid "Subtotal"
msgstr "" msgstr ""
#: admin.py:104 #: admin.py:113
msgid "Manage bill lines of multiple bills." msgid "Manage bill lines of multiple bills."
msgstr "" msgstr ""
#: admin.py:109 #: admin.py:118
#, python-format #, python-format
msgid "Manage %s bill lines." msgid "Manage %s bill lines."
msgstr "" msgstr ""
#: admin.py:129 #: admin.py:138
msgid "Raw" msgid "Raw"
msgstr "" msgstr ""
#: admin.py:147 #: admin.py:157
msgid "lines" msgid "lines"
msgstr "" msgstr ""
#: admin.py:152 templates/bills/microspective.html:107 #: admin.py:162 templates/bills/microspective.html:107
msgid "total" msgid "total"
msgstr "" msgstr ""
#: admin.py:160 models.py:85 models.py:340 #: admin.py:170 models.py:87 models.py:342
msgid "type" msgid "type"
msgstr "tipus" msgstr "tipus"
#: admin.py:177 #: admin.py:187
msgid "Payment" msgid "Payment"
msgstr "Pagament" msgstr "Pagament"
@ -131,15 +131,15 @@ msgstr "Pagament"
msgid "All" msgid "All"
msgstr "Tot" msgstr "Tot"
#: filters.py:18 models.py:75 #: filters.py:18 models.py:77
msgid "Invoice" msgid "Invoice"
msgstr "Factura" msgstr "Factura"
#: filters.py:19 models.py:76 #: filters.py:19 models.py:78
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificativa" msgstr "Factura rectificativa"
#: filters.py:20 models.py:77 #: filters.py:20 models.py:79
msgid "Fee" msgid "Fee"
msgstr "Quota de soci" msgstr "Quota de soci"
@ -167,15 +167,15 @@ msgstr "No"
msgid "Number" msgid "Number"
msgstr "" msgstr ""
#: forms.py:11 #: forms.py:10
msgid "Account" msgid "Account"
msgstr "" msgstr ""
#: forms.py:13 #: forms.py:12
msgid "Type" msgid "Type"
msgstr "" msgstr ""
#: forms.py:15 #: forms.py:13
msgid "Source" msgid "Source"
msgstr "" msgstr ""
@ -193,158 +193,178 @@ msgstr ""
msgid "Main" msgid "Main"
msgstr "" msgstr ""
#: models.py:20 models.py:83 #: models.py:22 models.py:85
msgid "account" msgid "account"
msgstr "" msgstr ""
#: models.py:22 #: models.py:24
msgid "name" msgid "name"
msgstr "" msgstr ""
#: models.py:23 #: models.py:25
msgid "Account full name will be used when left blank." msgid "Account full name will be used when left blank."
msgstr "" msgstr ""
#: models.py:24 #: models.py:26
msgid "address" msgid "address"
msgstr "" msgstr ""
#: models.py:25 #: models.py:27
msgid "city" msgid "city"
msgstr "" msgstr ""
#: models.py:27 #: models.py:29
msgid "zip code" msgid "zip code"
msgstr "" msgstr ""
#: models.py:28 #: models.py:30
msgid "Enter a valid zipcode." msgid "Enter a valid zipcode."
msgstr "" msgstr ""
#: models.py:29 #: models.py:31
msgid "country" msgid "country"
msgstr "" msgstr ""
#: models.py:32 #: models.py:34
msgid "VAT number" msgid "VAT number"
msgstr "NIF" msgstr "NIF"
#: models.py:64 #: models.py:66
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: models.py:65 #: models.py:67
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
#: models.py:66 #: models.py:68
msgid "Bad debt" msgid "Bad debt"
msgstr "" msgstr ""
#: models.py:78 #: models.py:80
msgid "Amendment Fee" msgid "Amendment Fee"
msgstr "" msgstr ""
#: models.py:79 #: models.py:81
msgid "Pro forma" msgid "Pro forma"
msgstr "" msgstr ""
#: models.py:82 #: models.py:84
msgid "number" msgid "number"
msgstr "" msgstr ""
#: models.py:86 #: models.py:88
msgid "created on" msgid "created on"
msgstr "" msgstr ""
#: models.py:87 #: models.py:89
msgid "closed on" msgid "closed on"
msgstr "" msgstr ""
#: models.py:88 #: models.py:90
msgid "open" msgid "open"
msgstr "" msgstr ""
#: models.py:89 #: models.py:91
msgid "sent" msgid "sent"
msgstr "" msgstr ""
#: models.py:90 #: models.py:92
msgid "due on" msgid "due on"
msgstr "" msgstr ""
#: models.py:91 #: models.py:93
msgid "updated on" msgid "updated on"
msgstr "" msgstr ""
#: models.py:93 #: models.py:96
msgid "comments" msgid "comments"
msgstr "" msgstr "comentaris"
#: models.py:94 #: models.py:97
msgid "HTML" msgid "HTML"
msgstr "" msgstr ""
#: models.py:271 #: models.py:273
msgid "bill" msgid "bill"
msgstr "" msgstr ""
#: models.py:272 models.py:337 templates/bills/microspective.html:73 #: models.py:274 models.py:339 templates/bills/microspective.html:73
msgid "description" msgid "description"
msgstr "descripció" msgstr "descripció"
#: models.py:273 #: models.py:275
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:274 #: models.py:276
msgid "quantity" msgid "quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:275 templates/bills/microspective.html:76 #: models.py:277
#, fuzzy
#| msgid "quantity"
msgid "Verbose quantity"
msgstr "quantitat"
#: models.py:278 templates/bills/microspective.html:76
#: templates/bills/microspective.html:100
msgid "subtotal" msgid "subtotal"
msgstr "" msgstr ""
#: models.py:276 #: models.py:279
msgid "tax" msgid "tax"
msgstr "impostos" msgstr "impostos"
#: models.py:282 #: models.py:284
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "" msgstr ""
#: models.py:283 #: models.py:285
msgid "order billed" msgid "order billed"
msgstr "" msgstr ""
#: models.py:284 #: models.py:286
msgid "order billed until" msgid "order billed until"
msgstr "" msgstr ""
#: models.py:285 #: models.py:287
msgid "created" msgid "created"
msgstr "" msgstr ""
#: models.py:287 #: models.py:289
msgid "amended line" msgid "amended line"
msgstr "" msgstr ""
#: models.py:330 #: models.py:332
msgid "Volume" msgid "Volume"
msgstr "" msgstr ""
#: models.py:331 #: models.py:333
msgid "Compensation" msgid "Compensation"
msgstr "" msgstr ""
#: models.py:332 #: models.py:334
msgid "Other" msgid "Other"
msgstr "" msgstr ""
#: models.py:336 #: models.py:338
msgid "bill line" msgid "bill line"
msgstr "" msgstr ""
#: templates/bills/microspective.html:49
msgid "DUE DATE"
msgstr "VENCIMENT"
#: templates/bills/microspective.html:53
msgid "TOTAL"
msgstr "TOTAL"
#: templates/bills/microspective.html:57
#, python-format
msgid "%(bill_type|upper)s DATE "
msgstr "DATA %(bill_type|upper)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:74
msgid "hrs/qty" msgid "hrs/qty"
msgstr "hrs/quant" msgstr "hrs/quant"
@ -360,7 +380,7 @@ msgstr "IVA"
#: templates/bills/microspective.html:103 #: templates/bills/microspective.html:103
msgid "taxes" msgid "taxes"
msgstr "" msgstr "impostos"
#: templates/bills/microspective.html:119 #: templates/bills/microspective.html:119
msgid "COMMENTS" msgid "COMMENTS"
@ -371,15 +391,17 @@ msgid "PAYMENT"
msgstr "PAGAMENT" msgstr "PAGAMENT"
#: templates/bills/microspective.html:129 #: templates/bills/microspective.html:129
#, python-format
msgid "" msgid ""
"\n" "\n"
" You can pay our %(type)s by bank transfer. <br>\n" " You can pay our %(type)s by bank transfer. <br>\n"
" Please make sure to state your name and the " " Please make sure to state your name and the %(type)s "
"%(bill.get_type_display.lower)s number.\n" "number.\n"
" Our bank account number is <br>\n" " Our bank account number is <br>\n"
" " " "
msgstr "" msgstr ""
"\n"
"Pots pagar aquesta %(type)s per transferencia banacaria.<br>Inclou el teu "
"nom i el numero de %(type)s. El nostre compte bancari és"
#: templates/bills/microspective.html:138 #: templates/bills/microspective.html:138
msgid "QUESTIONS" msgid "QUESTIONS"

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,4 @@
import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError, RegexValidator from django.core.validators import ValidationError, RegexValidator
@ -277,9 +278,8 @@ class BillLine(models.Model):
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16) verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2) subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2) tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
# Undo start_on = models.DateField(_("start"))
# initial = models.DateTimeField(null=True) end_on = models.DateField(_("end"), null=True)
# end = models.DateTimeField(null=True)
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True, order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL) 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_on = models.DateField(_("order billed"), null=True, blank=True)
@ -305,6 +305,15 @@ class BillLine(models.Model):
def get_verbose_quantity(self): def get_verbose_quantity(self):
return self.verbose_quantity or self.quantity return self.verbose_quantity or self.quantity
def get_verbose_period(self):
ini = self.start_on.strftime("%b, %Y")
if not self.end_on:
return ini
end = (self.end_on - datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
def undo(self): def undo(self):
# TODO warn user that undoing bills with compensations lead to compensation lost # TODO warn user that undoing bills with compensations lead to compensation lost
for attr in ['order_id', 'order_billed_on', 'order_billed_until']: for attr in ['order_id', 'order_billed_on', 'order_billed_until']:

View file

@ -108,7 +108,7 @@ hr {
</div> </div>
<div id="date" class="column-2"> <div id="date" class="column-2">
From {{ bill.lines.get.description }} From {{ bill.lines.get.get_verbose_period }}
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View file

@ -175,10 +175,14 @@ a:hover {
} }
#lines .column-description { #lines .column-description {
width: 65%; width: 45%;
text-align: left; text-align: left;
} }
#lines .column-period {
width: 20%;
}
#lines .column-quantity { #lines .column-quantity {
width: 10%; width: 10%;
} }

View file

@ -28,8 +28,8 @@
</div> </div>
<div class="contact"> <div class="contact">
<p>{{ seller.address }}<br> <p>{{ seller.address }}<br>
{{ seller.zipcode }} - {{ seller.city }}<br> {{ seller.zipcode }} - {% trans seller.city %}<br>
{{ seller.country }}<br> {% trans seller.get_country_display %}<br>
</p> </p>
<p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br> <p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
<a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br> <a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
@ -40,21 +40,21 @@
{% block summary %} {% block summary %}
<div id="bill-number"> <div id="bill-number">
{{ bill.get_type_display.capitalize }}<br> {% trans bill.get_type_display.capitalize %}<br>
<span class="value">{{ bill.number }}</span><br> <span class="value">{{ bill.number }}</span><br>
</div> </div>
<div id="bill-summary"> <div id="bill-summary">
<hr> <hr>
<div id="due-date"> <div id="due-date">
<span class="title">DUE DATE</span><br> <span class="title">{% trans "DUE DATE" %}</span><br>
<psan class="value">{{ bill.due_on | default:default_due_date | date }}</span> <psan class="value">{{ bill.due_on | default:default_due_date | date }}</span>
</div> </div>
<div id="total"> <div id="total">
<span class="title">TOTAL</span><br> <span class="title">{% trans "TOTAL" %}</span><br>
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span> <psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
</div> </div>
<div id="bill-date"> <div id="bill-date">
<span class="title">{{ bill.get_type_display.upper }} DATE</span><br> <span class="title">{% blocktrans with bill_type=bill.get_type_display %}{{ bill_type|upper }} DATE {% endblocktrans %}</span><br>
<psan class="value">{{ bill.closed_on | default:now | date }}</span> <psan class="value">{{ bill.closed_on | default:now | date }}</span>
</div> </div>
</div> </div>
@ -62,8 +62,8 @@
<span class="name">{{ buyer.get_name }}</span><br> <span class="name">{{ buyer.get_name }}</span><br>
{{ buyer.vat }}<br> {{ buyer.vat }}<br>
{{ buyer.address }}<br> {{ buyer.address }}<br>
{{ buyer.zipcode }} - {{ buyer.city }}<br> {{ buyer.zipcode }} - {% trans buyer.city %}<br>
{{ buyer.country }}<br> {% trans buyer.get_country_display %}<br>
</div> </div>
{% endblock %} {% endblock %}
@ -71,6 +71,7 @@
<div id="lines"> <div id="lines">
<span class="title column-id">id</span> <span class="title column-id">id</span>
<span class="title column-description">{% trans "description" %}</span> <span class="title column-description">{% trans "description" %}</span>
<span class="title column-period">{% trans "period" %}</span>
<span class="title column-quantity">{% trans "hrs/qty" %}</span> <span class="title column-quantity">{% trans "hrs/qty" %}</span>
<span class="title column-rate">{% trans "rate/price" %}</span> <span class="title column-rate">{% trans "rate/price" %}</span>
<span class="title column-subtotal">{% trans "subtotal" %}</span> <span class="title column-subtotal">{% trans "subtotal" %}</span>
@ -79,6 +80,7 @@
{% with sublines=line.sublines.all %} {% with sublines=line.sublines.all %}
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span> <span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span> <span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
<span class="{% if not sublines %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span> <span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span>
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span> <span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span> <span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
@ -86,6 +88,7 @@
{% for subline in sublines %} {% for subline in sublines %}
<span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span> <span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
<span class="{% if forloop.last %}last {% endif %}subline column-period">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span> <span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
@ -97,7 +100,7 @@
<div id="totals"> <div id="totals">
<br>&nbsp;<br> <br>&nbsp;<br>
{% for tax, subtotal in bill.get_subtotals.items %} {% for tax, subtotal in bill.get_subtotals.items %}
<span class="subtotal column-title">subtotal {{ tax }}% {% trans "VAT" %}</span> <span class="subtotal column-title">{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}</span>
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span> <span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
<br> <br>
<span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span> <span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span>
@ -126,9 +129,9 @@
{% if payment.message %} {% if payment.message %}
{{ payment.message | safe }} {{ payment.message | safe }}
{% else %} {% else %}
{% blocktrans with type=bill.get_type_display.lower %} {% blocktrans with type=bill.get_type_display %}
You can pay our {{ type }} by bank transfer. <br> You can pay our {{ type }} by bank transfer. <br>
Please make sure to state your name and the {{ bill.get_type_display.lower}} number. Please make sure to state your name and the {{ type }} number.
Our bank account number is <br> Our bank account number is <br>
{% endblocktrans %} {% endblocktrans %}
<strong>{{ seller_info.bank_account }}</strong> <strong>{{ seller_info.bank_account }}</strong>

View file

@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController, replace from orchestra.contrib.orchestration import ServiceController, replace
from orchestra.contrib.orchestration import Operation from orchestra.contrib.orchestration import Operation
from orchestra.utils.python import OrderedSet
from . import settings from . import settings
@ -92,9 +93,9 @@ class Bind9MasterDomainBackend(ServiceController):
return servers return servers
def get_slaves(self, domain): def get_slaves(self, domain):
return set(settings.DOMAINS_SLAVES).union( ips = list(settings.DOMAINS_SLAVES)
set(self.get_servers(domain, Bind9SlaveDomainBackend)) ips += self.get_servers(domain, Bind9SlaveDomainBackend)
) return OrderedSet(ips)
def get_context(self, domain): def get_context(self, domain):
slaves = self.get_slaves(domain) slaves = self.get_slaves(domain)
@ -139,9 +140,9 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi') self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi')
def get_masters(self, domain): def get_masters(self, domain):
return set(settings.DOMAINS_MASTERS).union( ips = list(settings.DOMAINS_MASTERS)
set(self.get_servers(domain, Bind9MasterDomainBackend)) ips += self.get_servers(domain, Bind9MasterDomainBackend)
) return OrderedSet(ips)
def get_context(self, domain): def get_context(self, domain):
context = { context = {

View file

@ -164,17 +164,25 @@ class Domain(models.Model):
value=' '.join(soa) value=' '.join(soa)
)) ))
is_host = self.is_top or not types or Record.A in types or Record.AAAA in types is_host = self.is_top or not types or Record.A in types or Record.AAAA in types
if Record.MX not in types and is_host: if is_host:
for mx in settings.DOMAINS_DEFAULT_MX: if Record.MX not in types:
for mx in settings.DOMAINS_DEFAULT_MX:
records.append(AttrDict(
type=Record.MX,
value=mx
))
default_a = settings.DOMAINS_DEFAULT_A
if default_a and Record.A not in types:
records.append(AttrDict( records.append(AttrDict(
type=Record.MX, type=Record.A,
value=mx value=default_a
))
default_aaaa = settings.DOMAINS_DEFAULT_AAAA
if default_aaaa and Record.AAAA not in types:
records.append(AttrDict(
type=Record.AAAA,
value=default_aaaa
)) ))
if (Record.A not in types and Record.AAAA not in types) and is_host:
records.append(AttrDict(
type=Record.A,
value=settings.DOMAINS_DEFAULT_A
))
result = '' result = ''
for record in records: for record in records:
name = '{name}.{spaces}'.format( name = '{name}.{spaces}'.format(

View file

@ -69,6 +69,11 @@ DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A',
) )
DOMAINS_DEFAULT_AAAA = getattr(settings, 'DOMAINS_DEFAULT_AAAA',
''
)
DOMAINS_DEFAULT_MX = getattr(settings, 'DOMAINS_DEFAULT_MX', ( DOMAINS_DEFAULT_MX = getattr(settings, 'DOMAINS_DEFAULT_MX', (
'10 mail.{}.'.format(ORCHESTRA_BASE_DOMAIN), '10 mail.{}.'.format(ORCHESTRA_BASE_DOMAIN),
'10 mail2.{}.'.format(ORCHESTRA_BASE_DOMAIN), '10 mail2.{}.'.format(ORCHESTRA_BASE_DOMAIN),

View file

@ -6,7 +6,7 @@ from django.db.models.loading import get_model
from orchestra.contrib.orchestration import manager, Operation from orchestra.contrib.orchestration import manager, Operation
from orchestra.contrib.orchestration.models import Server from orchestra.contrib.orchestration.models import Server
from orchestra.contrib.orchestration.backends import ServiceBackend from orchestra.contrib.orchestration.backends import ServiceBackend
from orchestra.utils.python import import_class from orchestra.utils.python import import_class, OrderedSet
class Command(BaseCommand): class Command(BaseCommand):
@ -34,7 +34,7 @@ class Command(BaseCommand):
list_backends = options.get('list_backends') list_backends = options.get('list_backends')
if list_backends: if list_backends:
for backend in ServiceBackend.get_backends(): for backend in ServiceBackend.get_backends():
print(str(backend).split("'")[1]) self.stdout.write(str(backend).split("'")[1])
return return
model = get_model(*options['model'].split('.')) model = get_model(*options['model'].split('.'))
action = options.get('action') action = options.get('action')
@ -49,9 +49,9 @@ class Command(BaseCommand):
comps = iter(comp.split('=')) comps = iter(comp.split('='))
for arg in comps: for arg in comps:
kwargs[arg] = next(comps).strip().rstrip(',') kwargs[arg] = next(comps).strip().rstrip(',')
operations = [] operations = OrderedSet()
operations = set()
route_cache = {} route_cache = {}
queryset = model.objects.filter(**kwargs).order_by('id')
if servers: if servers:
server_objects = [] server_objects = []
# Get and create missing Servers # Get and create missing Servers
@ -62,12 +62,12 @@ class Command(BaseCommand):
server = Server.objects.create(name=server, address=server) server = Server.objects.create(name=server, address=server)
server_objects.append(server) server_objects.append(server)
# Generate operations for the given backend # Generate operations for the given backend
for instance in model.objects.filter(**kwargs): for instance in queryset:
for backend in backends: for backend in backends:
backend = import_class(backend) backend = import_class(backend)
operations.add(Operation(backend, instance, action, servers=server_objects)) operations.add(Operation(backend, instance, action, servers=server_objects))
else: else:
for instance in model.objects.filter(**kwargs): for instance in queryset:
manager.collect(instance, action, operations=operations, route_cache=route_cache) manager.collect(instance, action, operations=operations, route_cache=route_cache)
scripts, block = manager.generate(operations) scripts, block = manager.generate(operations)
servers = [] servers = []
@ -76,11 +76,10 @@ class Command(BaseCommand):
server, __ = key server, __ = key
backend, operations = value backend, operations = value
servers.append(server.name) servers.append(server.name)
sys.stdout.write('# Execute on %s\n' % server.name) self.stdout.write('# Execute on %s' % server.name)
for method, commands in backend.scripts: for method, commands in backend.scripts:
script = '\n'.join(commands) + '\n' script = '\n'.join(commands)
script = script.encode('ascii', errors='replace') self.stdout.write(script)
sys.stdout.write(script.decode('ascii'))
if interactive: if interactive:
context = { context = {
'servers': ', '.join(servers), 'servers': ', '.join(servers),
@ -97,7 +96,7 @@ class Command(BaseCommand):
if not dry: if not dry:
logs = manager.execute(scripts, block=block) logs = manager.execute(scripts, block=block)
for log in logs: for log in logs:
print(log.stdout.encode('utf8', errors='replace')) self.stdout.write(log.stdout)
sys.stderr.write(log.stderr.encode('utf8', errors='replace')) self.stderr.write(log.stderr)
for log in logs: for log in logs:
print(log.backend, log.state) self.stdout.write(' '.join((log.backend, log.state)))

View file

@ -6,7 +6,7 @@ from collections import OrderedDict
from django import db from django import db
from django.core.mail import mail_admins from django.core.mail import mail_admins
from orchestra.utils.python import import_class from orchestra.utils.python import import_class, OrderedSet
from . import settings, Operation from . import settings, Operation
from .backends import ServiceBackend from .backends import ServiceBackend
@ -138,7 +138,7 @@ def execute(scripts, block=False, async=False):
def collect(instance, action, **kwargs): def collect(instance, action, **kwargs):
""" collect operations """ """ collect operations """
operations = kwargs.get('operations', set()) operations = kwargs.get('operations', OrderedSet())
route_cache = kwargs.get('route_cache', {}) route_cache = kwargs.get('route_cache', {})
for backend_cls in ServiceBackend.get_backends(): for backend_cls in ServiceBackend.get_backends():
# Check if there exists a related instance to be executed for this backend and action # Check if there exists a related instance to be executed for this backend and action

View file

@ -1,3 +1,4 @@
from datetime import timedelta
from os import path from os import path
from django.conf import settings from django.conf import settings
@ -28,3 +29,8 @@ ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PA
ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION', ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION',
False False
) )
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
timedelta(days=40)
)

View file

@ -0,0 +1,11 @@
from celery.task.schedules import crontab
from celery.decorators import periodic_task
from django.utils import timezone
from .models import BackendLog
@periodic_task(run_every=crontab(hour=7, minute=30, day_of_week=1))
def backend_logs_cleanup(run_every=run_every):
epoch = timezone.now()-settings.ORCHESTRATION_BACKEND_CLEANUP_DELTA
BackendLog.objects.filter(created_at__lt=epoch).delete()

View file

@ -12,6 +12,9 @@ class BillsBackend(object):
create_new = options.get('new_open', False) create_new = options.get('new_open', False)
proforma = options.get('proforma', False) proforma = options.get('proforma', False)
for line in lines: for line in lines:
quantity = line.metric*line.size
if quantity == 0:
continue
service = line.order.service service = line.order.service
# Create bill if needed # Create bill if needed
if bill is None or service.is_fee: if bill is None or service.is_fee:
@ -33,36 +36,32 @@ class BillsBackend(object):
bill = Invoice.objects.create(account=account, is_open=True) bill = Invoice.objects.create(account=account, is_open=True)
bills.append(bill) bills.append(bill)
# Create bill line # Create bill line
quantity = line.metric*line.size billine = bill.lines.create(
if quantity != 0: rate=service.nominal_price,
billine = bill.lines.create( quantity=line.metric*line.size,
rate=service.nominal_price, verbose_quantity=self.get_verbose_quantity(line),
quantity=line.metric*line.size, subtotal=line.subtotal,
verbose_quantity=self.get_verbose_quantity(line), tax=service.tax,
subtotal=line.subtotal, description=self.get_line_description(line),
tax=service.tax, start_on=line.ini,
description=self.get_line_description(line), end_on=line.end if service.billing_period != service.NEVER else None,
order=line.order, order=line.order,
order_billed_on=line.order.old_billed_on, order_billed_on=line.order.old_billed_on,
order_billed_until=line.order.old_billed_until order_billed_until=line.order.old_billed_until
) )
self.create_sublines(billine, line.discounts) self.create_sublines(billine, line.discounts)
return bills return bills
def format_period(self, ini, end): # def format_period(self, ini, end):
ini = ini.strftime("%b, %Y") # ini = ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y") # end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end: # if ini == end:
return ini # return ini
return _("{ini} to {end}").format(ini=ini, end=end) # return _("{ini} to {end}").format(ini=ini, end=end)
def get_line_description(self, line): def get_line_description(self, line):
service = line.order.service service = line.order.service
if service.is_fee:
return self.format_period(line.ini, line.end)
description = line.order.description description = line.order.description
if service.billing_period != service.NEVER:
description += " %s" % self.format_period(line.ini, line.end)
return description return description
def get_verbose_quantity(self, line): def get_verbose_quantity(self, line):

View file

@ -31,18 +31,15 @@ class BilledOrderListFilter(SimpleListFilter):
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('to_date', _("To date")), ('yes', _("Billed")),
('full', _("Full period")), ('no', _("Not billed")),
('not', _("Not billed")),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'to_date': if self.value() == 'yes':
return queryset.filter(billed_until__isnull=False, return queryset.filter(billed_until__isnull=False,
billed_until__gte=timezone.now()) billed_until__gte=timezone.now())
elif self.value() == 'full': elif self.value() == 'no':
raise NotImplementedError
elif self.value() == 'not':
return queryset.filter( return queryset.filter(
Q(billed_until__isnull=True) | Q(billed_until__isnull=True) |
Q(billed_until__lt=timezone.now()) Q(billed_until__lt=timezone.now())

View file

@ -44,6 +44,9 @@ class OrderQuerySet(models.QuerySet):
bills += bill_backend.create_bills(account, bill_lines, **options) bills += bill_backend.create_bills(account, bill_lines, **options)
else: else:
bills += [(account, bill_lines)] bills += [(account, bill_lines)]
# TODO remove if commit and always return unique elemenets (set()) when the other todo is fixed
if commit:
return list(set(bills))
return bills return bills
def givers(self, ini, end): def givers(self, ini, end):
@ -173,6 +176,8 @@ class Order(models.Model):
def update(self): def update(self):
instance = self.content_object instance = self.content_object
if instance is None:
return
handler = self.service.handler handler = self.service.handler
metric = '' metric = ''
if handler.metric: if handler.metric:

View file

@ -25,7 +25,7 @@ class GitLabSaaSBackend(ServiceController):
def validate_response(self, response, *status_codes): def validate_response(self, response, *status_codes):
if response.status_code not in status_codes: if response.status_code not in status_codes:
raise RuntimeError("[%i] %s" % (response.status_code, response.content)) raise RuntimeError("[%i] %s" % (response.status_code, response.content))
return json.loads(response.content) return json.loads(response.content.decode('utf8'))
def authenticate(self): def authenticate(self):
login_url = self.get_base_url() + '/session' login_url = self.get_base_url() + '/session'
@ -61,7 +61,7 @@ class GitLabSaaSBackend(ServiceController):
user_url = self.get_user_url(saas) user_url = self.get_user_url(saas)
response = requests.get(user_url, headers=self.headers) response = requests.get(user_url, headers=self.headers)
user = self.validate_response(response, 200) user = self.validate_response(response, 200)
user = json.loads(response.content) user = json.loads(response.content.decode('utf8'))
user['password'] = saas.password user['password'] = saas.password
response = requests.put(user_url, data=user, headers=self.headers) response = requests.put(user_url, data=user, headers=self.headers)
user = self.validate_response(response, 200) user = self.validate_response(response, 200)
@ -92,7 +92,8 @@ class GitLabSaaSBackend(ServiceController):
username = saas.name username = saas.name
email = saas.data['email'] email = saas.data['email']
users_url = self.get_base_url() + '/users/' users_url = self.get_base_url() + '/users/'
users = json.loads(requests.get(users_url, headers=self.headers).content) response = requests.get(users_url, headers=self.headers)
users = json.loads(response.content.decode('utf8'))
for user in users: for user in users:
if user['username'] == username: if user['username'] == username:
print('ValidationError: user-exists') print('ValidationError: user-exists')

View file

@ -14,7 +14,7 @@ class PhpListSaaSBackend(ServiceController):
def _save(self, saas, server): def _save(self, saas, server):
admin_link = 'http://%s/admin/' % saas.get_site_domain() admin_link = 'http://%s/admin/' % saas.get_site_domain()
admin_content = requests.get(admin_link).content admin_content = requests.get(admin_link).content.decode('utf8')
if admin_content.startswith('Cannot connect to Database'): if admin_content.startswith('Cannot connect to Database'):
raise RuntimeError("Database is not yet configured") raise RuntimeError("Database is not yet configured")
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content) install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
@ -30,7 +30,7 @@ class PhpListSaaSBackend(ServiceController):
'adminpassword': saas.password, 'adminpassword': saas.password,
} }
response = requests.post(install_link, data=post) response = requests.post(install_link, data=post)
print(response.content) print(response.content.decode('utf8'))
if response.status_code != 200: if response.status_code != 200:
raise RuntimeError("Bad status code %i" % response.status_code) raise RuntimeError("Bad status code %i" % response.status_code)
else: else:

View file

@ -31,7 +31,7 @@ class WordpressMuBackend(ServiceController):
def validate_response(self, response): def validate_response(self, response):
if response.status_code != 200: if response.status_code != 200:
errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', response.content) errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', response.content.decode('utf8'))
raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
def get_id(self, session, webapp): def get_id(self, session, webapp):
@ -41,7 +41,7 @@ class WordpressMuBackend(ServiceController):
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+' '<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
'class="edit">%s</a>' % webapp.name 'class="edit">%s</a>' % webapp.name
) )
content = session.get(search).content content = session.get(search).content.decode('utf8')
# Get id # Get id
ids = regex.search(content) ids = regex.search(content)
if not ids: if not ids:
@ -64,7 +64,7 @@ class WordpressMuBackend(ServiceController):
except RuntimeError: except RuntimeError:
url = self.get_base_url() url = self.get_base_url()
url += '/wp-admin/network/site-new.php' url += '/wp-admin/network/site-new.php'
content = session.get(url).content content = session.get(url).content.decode('utf8')
wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"') wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"')
wpnonce = wpnonce.search(content).groups()[0] wpnonce = wpnonce.search(content).groups()[0]
@ -94,7 +94,7 @@ class WordpressMuBackend(ServiceController):
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog' delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
delete += '&id=%d&_wpnonce=%s' % (id, wpnonce) delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
content = session.get(delete).content content = session.get(delete).content.decode('utf8')
wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"') wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"')
wpnonce = wpnonce.search(content).groups()[0] wpnonce = wpnonce.search(content).groups()[0]
data = { data = {

View file

@ -11,12 +11,12 @@ from .. import settings
class GitLabForm(SoftwareServiceForm): class GitLabForm(SoftwareServiceForm):
email = forms.EmailField(label=_("Email"), email = forms.EmailField(label=_("Email"),
help_text=_("Initial email address, changes on the GitLab server are not reflected here.")) help_text=_("Initial email address, changes on the GitLab server are not reflected here."))
class GitLaChangebForm(GitLabForm): class GitLaChangebForm(GitLabForm):
user_id = forms.IntegerField(label=("User ID"), widget=widgets.ShowTextWidget, user_id = forms.IntegerField(label=("User ID"), widget=widgets.ShowTextWidget,
help_text=_("ID of this user on the GitLab server, the only attribute that not changes.")) help_text=_("ID of this user on the GitLab server, the only attribute that not changes."))
class GitLabSerializer(serializers.Serializer): class GitLabSerializer(serializers.Serializer):

View file

@ -106,6 +106,8 @@ class SoftwareService(plugins.Plugin):
except IndexError: except IndexError:
pass pass
else: else:
if log.state != log.SUCCESS:
raise ValidationError(_("Validate creation execution has failed."))
errors = {} errors = {}
if 'user-exists' in log.stdout: if 'user-exists' in log.stdout:
errors['name'] = _("User with this username already exists.") errors['name'] = _("User with this username already exists.")

View file

@ -23,8 +23,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
Relax and enjoy the journey. Relax and enjoy the journey.
""" """
_VOLUME = 'VOLUME' _VOLUME = 'volume'
_COMPENSATION = 'COMPENSATION' _COMPENSATION = 'compensation'
model = None model = None
@ -42,29 +42,27 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
def validate_content_type(self, service): def validate_content_type(self, service):
pass pass
def validate_match(self, service): def validate_expression(self, service, method):
if not service.match:
service.match = 'True'
try: try:
obj = service.content_type.model_class().objects.all()[0] obj = service.content_type.model_class().objects.all()[0]
except IndexError: except IndexError:
return return
try: try:
bool(self.matches(obj)) bool(getattr(self, method)(obj))
except Exception as exception: except Exception as exception:
name = type(exception).__name__ name = type(exception).__name__
raise ValidationError(': '.join((name, str(exception)))) raise ValidationError(': '.join((name, str(exception))))
def validate_match(self, service):
if not service.match:
service.match = 'True'
self.validate_expression(service, 'matches')
def validate_metric(self, service): def validate_metric(self, service):
try: self.validate_expression(service, 'get_metric')
obj = service.content_type.model_class().objects.all()[0]
except IndexError: def validate_order_description(self, service):
return self.validate_expression(service, 'get_order_description')
try:
bool(self.get_metric(obj))
except Exception as exception:
name = type(exception).__name__
raise ValidationError(': '.join((name, str(exception))))
def get_content_type(self): def get_content_type(self):
if not self.model: if not self.model:
@ -72,15 +70,26 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
app_label, model = self.model.split('.') app_label, model = self.model.split('.')
return ContentType.objects.get_by_natural_key(app_label, model.lower()) return ContentType.objects.get_by_natural_key(app_label, model.lower())
def get_expression_context(self, instance):
return {
'instance': instance,
'obj': instance,
'ugettext': ugettext,
'handler': self,
'service': self.service,
instance._meta.model_name: instance,
'math': math,
'logsteps': lambda n, size=1: \
round(n/(decimal.Decimal(size*10**int(math.log10(max(n, 1))))))*size*10**int(math.log10(max(n, 1))),
'log10': math.log10,
'Decimal': decimal.Decimal,
}
def matches(self, instance): def matches(self, instance):
if not self.match: if not self.match:
# Blank expressions always evaluate True # Blank expressions always evaluate True
return True return True
safe_locals = { safe_locals = self.get_expression_context(instance)
'instance': instance,
'obj': instance,
instance._meta.model_name: instance,
}
return eval(self.match, safe_locals) return eval(self.match, safe_locals)
def get_ignore_delta(self): def get_ignore_delta(self):
@ -113,27 +122,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
def get_metric(self, instance): def get_metric(self, instance):
if self.metric: if self.metric:
safe_locals = { safe_locals = self.get_expression_context(instance)
instance._meta.model_name: instance,
'instance': instance,
'math': math,
'logsteps': lambda n, size=1: \
round(n/(decimal.Decimal(size*10**int(math.log10(max(n, 1))))))*size*10**int(math.log10(max(n, 1))),
'log10': math.log10,
'Decimal': decimal.Decimal,
}
try: try:
return eval(self.metric, safe_locals) return eval(self.metric, safe_locals)
except Exception as error: except Exception as error:
raise type(error)("%s on '%s'" %(error, self.service)) raise type(error)("%s on '%s'" %(error, self.service))
def get_order_description(self, instance): def get_order_description(self, instance):
safe_locals = { safe_locals = self.get_expression_context(instance)
'instance': instance,
'obj': instance,
'ugettext': ugettext,
instance._meta.model_name: instance,
}
account = getattr(instance, 'account', instance) account = getattr(instance, 'account', instance)
with translation.override(account.language): with translation.override(account.language):
if not self.order_description: if not self.order_description:

View file

@ -91,7 +91,7 @@ class Service(models.Model):
help_text=_( help_text=_(
"Python <a href='https://docs.python.org/2/library/functions.html#eval'>expression</a> " "Python <a href='https://docs.python.org/2/library/functions.html#eval'>expression</a> "
"used for generating the description for the bill lines of this services.<br>" "used for generating the description for the bill lines of this services.<br>"
"Defaults to <tt>'%s: %s' % (handler.description, instance)</tt>" "Defaults to <tt>'%s: %s' % (ugettext(handler.description), instance)</tt>"
)) ))
ignore_period = models.CharField(_("ignore period"), max_length=16, blank=True, ignore_period = models.CharField(_("ignore period"), max_length=16, blank=True,
help_text=_("Period in which orders will be ignored if cancelled. " help_text=_("Period in which orders will be ignored if cancelled. "
@ -180,6 +180,7 @@ class Service(models.Model):
'content_type': (self.handler.validate_content_type, self), 'content_type': (self.handler.validate_content_type, self),
'match': (self.handler.validate_match, self), 'match': (self.handler.validate_match, self),
'metric': (self.handler.validate_metric, self), 'metric': (self.handler.validate_metric, self),
'order_description': (self.handler.validate_order_description, self),
}) })
def get_pricing_period(self): def get_pricing_period(self):
@ -238,7 +239,10 @@ class Service(models.Model):
order_model = get_model(settings.SERVICES_ORDER_MODEL) order_model = get_model(settings.SERVICES_ORDER_MODEL)
related_model = self.content_type.model_class() related_model = self.content_type.model_class()
updates = [] updates = []
for instance in related_model.objects.select_related('account').all(): queryset = related_model.objects.all()
if related_model._meta.model_name != 'account':
queryset = queryset.select_related('account').all()
for instance in queryset:
updates += order_model.update_orders(instance, service=self, commit=commit) updates += order_model.update_orders(instance, service=self, commit=commit)
return updates return updates

View file

@ -118,8 +118,18 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
) )
super(PHPBackend, self).commit() super(PHPBackend, self).commit()
def get_options(self, webapp):
kwargs = {}
if self.MERGE:
kwargs = {
'webapp__account': webapp.account,
'webapp__type': webapp.type,
'webapp__data__contains': '"php_version":"%s"' % webapp.data['php_version'],
}
return webapp.get_options(**kwargs)
def get_fpm_config(self, webapp, context): def get_fpm_config(self, webapp, context):
options = webapp.get_options(merge=self.MERGE) options = self.get_options(webapp)
context.update({ context.update({
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE), 'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN), 'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
@ -168,7 +178,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
exec %(php_binary_path)s %(php_init_vars)s""") % context exec %(php_binary_path)s %(php_init_vars)s""") % context
def get_fcgid_cmd_options(self, webapp, context): def get_fcgid_cmd_options(self, webapp, context):
options = webapp.get_options(merge=self.MERGE) options = self.get_options(webapp)
maps = { maps = {
'MaxProcesses': options.get('processes', None), 'MaxProcesses': options.get('processes', None),
'IOTimeout': options.get('timeout', None), 'IOTimeout': options.get('timeout', None),

View file

@ -10,7 +10,12 @@ from . import WebAppServiceMixin
from .. import settings from .. import settings
class PythonBackend(WebAppServiceMixin, ServiceController): class uWSGIPythonBackend(WebAppServiceMixin, ServiceController):
"""
Emperor mode
http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
"""
verbose_name = _("Python uWSGI") verbose_name = _("Python uWSGI")
default_route_match = "webapp.type.endswith('python')" default_route_match = "webapp.type.endswith('python')"
@ -26,26 +31,14 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
self.delete_webapp_dir(context) self.delete_webapp_dir(context)
def save_uwsgi(self, webapp, context): def save_uwsgi(self, webapp, context):
self.append(textwrap.dedent("""\ self.append("echo '%(uwsgi_config)s' > %(vassal_path)s" % context)
uwsgi_config='%(uwsgi_config)s'
{
echo -e "${uwsgi_config}" | diff -N -I'^\s*;;' %(uwsgi_path)s -
} || {
echo -e "${uwsgi_config}" > %(uwsgi_path)s
UPDATED_UWSGI=1
}
ln -s %(uwsgi_path)s %(uwsgi_enabled)s
""") % context
)
def delete_uwsgi(self, webapp, context): def delete_uwsgi(self, webapp, context):
self.append("rm -f %(uwsgi_path)s" % context) self.append("rm -f %(vassal_path)s" % context)
self.append("rm -f %(uwsgi_enabled)s" % context)
def get_uwsgi_ini(self, context): def get_uwsgi_ini(self, context):
# TODO switch to this http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
# TODO http://uwsgi-docs.readthedocs.org/en/latest/Changelog-1.9.1.html#on-demand-vassals
return textwrap.dedent("""\ return textwrap.dedent("""\
# %(banner)s
[uwsgi] [uwsgi]
plugins = python{python_version_number} plugins = python{python_version_number}
chdir = {app_path} chdir = {app_path}
@ -70,10 +63,8 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
context.update({ context.update({
'uwsgi_ini': self.get_uwsgi_ini(context), 'uwsgi_ini': self.get_uwsgi_ini(context),
'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR, 'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR,
'uwsgi_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR, 'vassal_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
'apps-available/%s.ini'% context['app_name']), 'vassals/%s' % context['app_name']),
'uwsgi_enabled': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
'apps-enabled/%s.ini'% context['app_name']),
}) })
return context return context

View file

@ -1,123 +0,0 @@
import re
import requests
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController
from .. import settings
class WordpressMuBackend(ServiceController):
verbose_name = _("Wordpress multisite")
model = 'saas.SaaS'
default_route_match = "saas.service == 'wordpress-mu'"
@property
def script(self):
return self.cmds
def login(self, session):
base_url = self.get_base_url()
login_url = base_url + '/wp-login.php'
login_data = {
'log': 'admin',
'pwd': settings.WEBSITES_WORDPRESSMU_ADMIN_PASSWORD,
'redirect_to': '/wp-admin/'
}
response = session.post(login_url, data=login_data)
if response.url != base_url + '/wp-admin/':
raise IOError("Failure login to remote application")
def get_base_url(self):
base_url = settings.WEBSITES_WORDPRESSMU_BASE_URL
return base_url.rstrip('/')
def validate_response(self, response):
if response.status_code != 200:
errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', response.content)
raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
def get_id(self, session, webapp):
search = self.get_base_url()
search += '/wp-admin/network/sites.php?s=%s&action=blogs' % webapp.name
regex = re.compile(
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
'class="edit">%s</a>' % webapp.name
)
content = session.get(search).content
# Get id
ids = regex.search(content)
if not ids:
raise RuntimeError("Blog '%s' not found" % webapp.name)
ids = ids.groups()
if len(ids) > 1:
raise ValueError("Multiple matches")
# Get wpnonce
wpnonce = re.search(r'<span class="delete">(.*)</span>', content).groups()[0]
wpnonce = re.search(r'_wpnonce=([^"]*)"', wpnonce).groups()[0]
return int(ids[0]), wpnonce
def create_blog(self, webapp, server):
session = requests.Session()
self.login(session)
# Check if blog already exists
try:
self.get_id(session, webapp)
except RuntimeError:
url = self.get_base_url()
url += '/wp-admin/network/site-new.php'
content = session.get(url).content
wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"')
wpnonce = wpnonce.search(content).groups()[0]
url += '?action=add-site'
data = {
'blog[domain]': webapp.name,
'blog[title]': webapp.name,
'blog[email]': webapp.account.email,
'_wpnonce_add-blog': wpnonce,
}
# Validate response
response = session.post(url, data=data)
self.validate_response(response)
def delete_blog(self, webapp, server):
session = requests.Session()
self.login(session)
try:
id, wpnonce = self.get_id(session, webapp)
except RuntimeError:
pass
else:
delete = self.get_base_url()
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
content = session.get(delete).content
wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"')
wpnonce = wpnonce.search(content).groups()[0]
data = {
'action': 'deleteblog',
'id': id,
'_wpnonce': wpnonce,
'_wp_http_referer': '/wp-admin/network/sites.php',
}
delete = self.get_base_url()
delete += '/wp-admin/network/sites.php?action=deleteblog'
response = session.post(delete, data=data)
self.validate_response(response)
def save(self, webapp):
if webapp.type != 'wordpress-mu':
return
self.append(self.create_blog, webapp)
def delete(self, webapp):
if webapp.type != 'wordpress-mu':
return
self.append(self.delete_blog, webapp)

View file

@ -57,17 +57,19 @@ class WebApp(models.Model):
self.data = apptype.clean_data() self.data = apptype.clean_data()
@cached @cached
def get_options(self, merge=False): def get_options(self, **kwargs):
if merge: options = OrderedDict()
options = OrderedDict() if not kwargs:
qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type) kwargs = {
for name, value in qs.values_list('name', 'value').order_by('name'): 'webapp_id': self.pk,
if name in options: }
options[name] = max(options[name], value) qs = WebAppOption.objects.filter(**kwargs)
else: for name, value in qs.values_list('name', 'value').order_by('name'):
options[name] = value if name in options:
return options options[name] = max(options[name], value)
return OrderedDict(self.options.values_list('name', 'value').order_by('name')) else:
options[name] = value
return options
def get_directive(self): def get_directive(self):
return self.type_instance.get_directive() return self.type_instance.get_directive()

View file

@ -9,4 +9,4 @@ class Command(BaseCommand):
def handle(self, *filenames, **options): def handle(self, *filenames, **options):
flake = run('flake8 {%s,%s} | grep -v "W293\|E501"' % (get_orchestra_dir(), get_site_dir())) flake = run('flake8 {%s,%s} | grep -v "W293\|E501"' % (get_orchestra_dir(), get_site_dir()))
print(flake.stdout) self.stdout.write(flake.stdout)

View file

@ -7,5 +7,5 @@ def html_to_pdf(html):
'PATH=$PATH:/usr/local/bin/\n' 'PATH=$PATH:/usr/local/bin/\n'
'xvfb-run -a -s "-screen 0 640x4800x16" ' 'xvfb-run -a -s "-screen 0 640x4800x16" '
'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -', 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
stdin=html.encode('utf-8'), force_unicode=False stdin=html.encode('utf-8')
).stdout ).stdout

View file

@ -45,15 +45,15 @@ def read_async(fd):
return '' return ''
def runiterator(command, display=False, error_codes=[0], silent=False, stdin='', force_unicode=True): def runiterator(command, display=False, error_codes=[0], silent=False, stdin=''):
""" Subprocess wrapper for running commands concurrently """ """ Subprocess wrapper for running commands concurrently """
if display: if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
p = subprocess.Popen(command, shell=True, executable='/bin/bash', p = subprocess.Popen(command, shell=True, executable='/bin/bash',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
p.stdin.write(bytes(stdin, 'utf-8')) p.stdin.write(stdin)
p.stdin.close() p.stdin.close()
yield yield
@ -62,16 +62,18 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
# Async reading of stdout and sterr # Async reading of stdout and sterr
while True: while True:
stdout = '' stdout = b''
stderr = '' stderr = b''
# Get complete unicode chunks # Get complete unicode chunks
select.select([p.stdout, p.stderr], [], []) select.select([p.stdout, p.stderr], [], [])
stdoutPiece = read_async(p.stdout) stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr) stderrPiece = read_async(p.stderr)
stdout += (stdoutPiece or b'').decode('utf8', errors='replace') stdout += (stdoutPiece or b'')
stderr += (stderrPiece or b'').decode('utf8', errors='replace') #.decode('ascii'), errors='replace')
stderr += (stderrPiece or b'')
#.decode('ascii'), errors='replace')
if display and stdout: if display and stdout:
sys.stdout.write(stdout) sys.stdout.write(stdout)
@ -89,14 +91,14 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
raise StopIteration raise StopIteration
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True): def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False):
iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode) iterator = runiterator(command, display, error_codes, silent, stdin)
next(iterator) next(iterator)
if async: if async:
return iterator return iterator
stdout = '' stdout = b''
stderr = '' stderr = b''
for state in iterator: for state in iterator:
stdout += state.stdout stdout += state.stdout
stderr += state.stderr stderr += state.stderr