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
* fpm reload starts new pools?
* 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
# 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 -
# 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):
return '---'
url = change_url(obj)
display = kwargs.get('display')
if display:
display = getattr(obj, display, 'merda')
else:
display = obj
extra = ''
if kwargs['popup']:
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

View File

@ -35,6 +35,7 @@ MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*'
# 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 = False
@ -101,6 +102,7 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'passlib.ext.django',
'django_countries',
# Django.contrib
'django.contrib.auth',

View File

@ -40,6 +40,7 @@ DATABASES = {
'PASSWORD': 'orchestra', # 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.
'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 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.forms.widgets import paddingCheckboxSelectMultiple
@ -29,8 +29,13 @@ PAYMENT_STATE_COLORS = {
class BillLineInline(admin.TabularInline):
model = BillLine
fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'display_total')
readonly_fields = ('display_total',)
fields = (
'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):
total = line.get_total()
@ -46,9 +51,9 @@ class BillLineInline(admin.TabularInline):
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'description':
kwargs['widget'] = forms.TextInput(attrs={'size':'110'})
else:
kwargs['widget'] = forms.TextInput(attrs={'size':'13'})
kwargs['widget'] = forms.TextInput(attrs={'size':'50'})
elif db_field.name not in ('start_on', 'end_on'):
kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
def get_queryset(self, request):
@ -61,7 +66,8 @@ class ClosedBillLineInline(BillLineInline):
# https://code.djangoproject.com/ticket/9025
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
@ -157,10 +163,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines")
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.short_description = _("total")
display_total.admin_order_field = 'totals'
display_total.admin_order_field = 'computed_total'
def type_link(self, bill):
bill_type = bill.type.lower()
@ -235,7 +241,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(
models.Count('lines'),
totals=Sum(
computed_total=Sum(
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
),
)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,112 +18,112 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:35
#: actions.py:36
msgid "Download"
msgstr "Descarrega"
#: actions.py:45
#: actions.py:46
msgid "View"
msgstr "Vista"
#: actions.py:53
#: actions.py:54
msgid "Selected bills should be in open state"
msgstr ""
#: actions.py:71
#: actions.py:72
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\">One related transaction</a> has been created"
msgstr ""
#: actions.py:82
#, python-format
msgid "<a href=\"%s\">%i related transactions</a> have been created"
msgstr ""
#: actions.py:87
#: actions.py:88
msgid "Are you sure about closing the following bills?"
msgstr ""
#: actions.py:88
#: actions.py:89
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
#: actions.py:102
msgid "Close"
msgstr ""
#: actions.py:112
#: actions.py:113
msgid "Resend"
msgstr ""
#: actions.py:129 models.py:309
#: actions.py:130 models.py:312
msgid "Not enough information stored for undoing"
msgstr ""
#: actions.py:132 models.py:311
#: actions.py:133 models.py:314
msgid "Dates don't match"
msgstr ""
#: actions.py:147
#: actions.py:148
msgid "Can not move lines which are not in open state."
msgstr ""
#: actions.py:152
#: actions.py:153
msgid "Can not move lines from different accounts"
msgstr ""
#: actions.py:160
#: actions.py:161
msgid "Target account different than lines account."
msgstr ""
#: actions.py:167
#: actions.py:168
msgid "Lines moved"
msgstr ""
#: admin.py:41 forms.py:12
#: admin.py:43 admin.py:86 forms.py:11
msgid "Total"
msgstr ""
#: admin.py:69
#: admin.py:73
msgid "Description"
msgstr "Descripció"
#: admin.py:77
#: admin.py:81
msgid "Subtotal"
msgstr ""
#: admin.py:104
#: admin.py:113
msgid "Manage bill lines of multiple bills."
msgstr ""
#: admin.py:109
#: admin.py:118
#, python-format
msgid "Manage %s bill lines."
msgstr ""
#: admin.py:129
#: admin.py:138
msgid "Raw"
msgstr ""
#: admin.py:147
#: admin.py:157
msgid "lines"
msgstr ""
#: admin.py:152 templates/bills/microspective.html:107
#: admin.py:162 templates/bills/microspective.html:107
msgid "total"
msgstr ""
#: admin.py:160 models.py:85 models.py:340
#: admin.py:170 models.py:87 models.py:342
msgid "type"
msgstr "tipus"
#: admin.py:177
#: admin.py:187
msgid "Payment"
msgstr "Pagament"
@ -131,15 +131,15 @@ msgstr "Pagament"
msgid "All"
msgstr "Tot"
#: filters.py:18 models.py:75
#: filters.py:18 models.py:77
msgid "Invoice"
msgstr "Factura"
#: filters.py:19 models.py:76
#: filters.py:19 models.py:78
msgid "Amendment invoice"
msgstr "Factura rectificativa"
#: filters.py:20 models.py:77
#: filters.py:20 models.py:79
msgid "Fee"
msgstr "Quota de soci"
@ -167,15 +167,15 @@ msgstr "No"
msgid "Number"
msgstr ""
#: forms.py:11
#: forms.py:10
msgid "Account"
msgstr ""
#: forms.py:13
#: forms.py:12
msgid "Type"
msgstr ""
#: forms.py:15
#: forms.py:13
msgid "Source"
msgstr ""
@ -193,158 +193,178 @@ msgstr ""
msgid "Main"
msgstr ""
#: models.py:20 models.py:83
#: models.py:22 models.py:85
msgid "account"
msgstr ""
#: models.py:22
#: models.py:24
msgid "name"
msgstr ""
#: models.py:23
#: models.py:25
msgid "Account full name will be used when left blank."
msgstr ""
#: models.py:24
#: models.py:26
msgid "address"
msgstr ""
#: models.py:25
#: models.py:27
msgid "city"
msgstr ""
#: models.py:27
#: models.py:29
msgid "zip code"
msgstr ""
#: models.py:28
#: models.py:30
msgid "Enter a valid zipcode."
msgstr ""
#: models.py:29
#: models.py:31
msgid "country"
msgstr ""
#: models.py:32
#: models.py:34
msgid "VAT number"
msgstr "NIF"
#: models.py:64
#: models.py:66
msgid "Paid"
msgstr ""
#: models.py:65
#: models.py:67
msgid "Pending"
msgstr ""
#: models.py:66
#: models.py:68
msgid "Bad debt"
msgstr ""
#: models.py:78
#: models.py:80
msgid "Amendment Fee"
msgstr ""
#: models.py:79
#: models.py:81
msgid "Pro forma"
msgstr ""
#: models.py:82
#: models.py:84
msgid "number"
msgstr ""
#: models.py:86
#: models.py:88
msgid "created on"
msgstr ""
#: models.py:87
#: models.py:89
msgid "closed on"
msgstr ""
#: models.py:88
#: models.py:90
msgid "open"
msgstr ""
#: models.py:89
#: models.py:91
msgid "sent"
msgstr ""
#: models.py:90
#: models.py:92
msgid "due on"
msgstr ""
#: models.py:91
#: models.py:93
msgid "updated on"
msgstr ""
#: models.py:93
#: models.py:96
msgid "comments"
msgstr ""
msgstr "comentaris"
#: models.py:94
#: models.py:97
msgid "HTML"
msgstr ""
#: models.py:271
#: models.py:273
msgid "bill"
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"
msgstr "descripció"
#: models.py:273
#: models.py:275
msgid "rate"
msgstr "tarifa"
#: models.py:274
#: models.py:276
msgid "quantity"
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"
msgstr ""
#: models.py:276
#: models.py:279
msgid "tax"
msgstr "impostos"
#: models.py:282
#: models.py:284
msgid "Informative link back to the order"
msgstr ""
#: models.py:283
#: models.py:285
msgid "order billed"
msgstr ""
#: models.py:284
#: models.py:286
msgid "order billed until"
msgstr ""
#: models.py:285
#: models.py:287
msgid "created"
msgstr ""
#: models.py:287
#: models.py:289
msgid "amended line"
msgstr ""
#: models.py:330
#: models.py:332
msgid "Volume"
msgstr ""
#: models.py:331
#: models.py:333
msgid "Compensation"
msgstr ""
#: models.py:332
#: models.py:334
msgid "Other"
msgstr ""
#: models.py:336
#: models.py:338
msgid "bill line"
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
msgid "hrs/qty"
msgstr "hrs/quant"
@ -360,7 +380,7 @@ msgstr "IVA"
#: templates/bills/microspective.html:103
msgid "taxes"
msgstr ""
msgstr "impostos"
#: templates/bills/microspective.html:119
msgid "COMMENTS"
@ -371,15 +391,17 @@ msgid "PAYMENT"
msgstr "PAGAMENT"
#: templates/bills/microspective.html:129
#, python-format
msgid ""
"\n"
" You can pay our %(type)s by bank transfer. <br>\n"
" Please make sure to state your name and the "
"%(bill.get_type_display.lower)s number.\n"
" Please make sure to state your name and the %(type)s "
"number.\n"
" Our bank account number is <br>\n"
" "
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
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 django.core.validators import ValidationError, RegexValidator
@ -277,9 +278,8 @@ class BillLine(models.Model):
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
# Undo
# initial = models.DateTimeField(null=True)
# end = models.DateTimeField(null=True)
start_on = models.DateField(_("start"))
end_on = models.DateField(_("end"), null=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)
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
@ -305,6 +305,15 @@ class BillLine(models.Model):
def get_verbose_quantity(self):
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):
# TODO warn user that undoing bills with compensations lead to compensation lost
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:

View File

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

View File

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

View File

@ -28,8 +28,8 @@
</div>
<div class="contact">
<p>{{ seller.address }}<br>
{{ seller.zipcode }} - {{ seller.city }}<br>
{{ seller.country }}<br>
{{ seller.zipcode }} - {% trans seller.city %}<br>
{% trans seller.get_country_display %}<br>
</p>
<p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
<a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
@ -40,21 +40,21 @@
{% block summary %}
<div id="bill-number">
{{ bill.get_type_display.capitalize }}<br>
{% trans bill.get_type_display.capitalize %}<br>
<span class="value">{{ bill.number }}</span><br>
</div>
<div id="bill-summary">
<hr>
<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>
</div>
<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>
</div>
<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>
</div>
</div>
@ -62,8 +62,8 @@
<span class="name">{{ buyer.get_name }}</span><br>
{{ buyer.vat }}<br>
{{ buyer.address }}<br>
{{ buyer.zipcode }} - {{ buyer.city }}<br>
{{ buyer.country }}<br>
{{ buyer.zipcode }} - {% trans buyer.city %}<br>
{% trans buyer.get_country_display %}<br>
</div>
{% endblock %}
@ -71,6 +71,7 @@
<div id="lines">
<span class="title column-id">id</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-rate">{% trans "rate/price" %}</span>
<span class="title column-subtotal">{% trans "subtotal" %}</span>
@ -79,6 +80,7 @@
{% 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-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-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>
@ -86,6 +88,7 @@
{% 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-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-rate">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
@ -97,7 +100,7 @@
<div id="totals">
<br>&nbsp;<br>
{% 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>
<br>
<span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span>
@ -126,9 +129,9 @@
{% if payment.message %}
{{ payment.message | safe }}
{% 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>
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>
{% endblocktrans %}
<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 Operation
from orchestra.utils.python import OrderedSet
from . import settings
@ -92,9 +93,9 @@ class Bind9MasterDomainBackend(ServiceController):
return servers
def get_slaves(self, domain):
return set(settings.DOMAINS_SLAVES).union(
set(self.get_servers(domain, Bind9SlaveDomainBackend))
)
ips = list(settings.DOMAINS_SLAVES)
ips += self.get_servers(domain, Bind9SlaveDomainBackend)
return OrderedSet(ips)
def get_context(self, 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')
def get_masters(self, domain):
return set(settings.DOMAINS_MASTERS).union(
set(self.get_servers(domain, Bind9MasterDomainBackend))
)
ips = list(settings.DOMAINS_MASTERS)
ips += self.get_servers(domain, Bind9MasterDomainBackend)
return OrderedSet(ips)
def get_context(self, domain):
context = {

View File

@ -164,17 +164,25 @@ class Domain(models.Model):
value=' '.join(soa)
))
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:
for mx in settings.DOMAINS_DEFAULT_MX:
if is_host:
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(
type=Record.MX,
value=mx
type=Record.A,
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 = ''
for record in records:
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', (
'10 mail.{}.'.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.models import Server
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):
@ -34,7 +34,7 @@ class Command(BaseCommand):
list_backends = options.get('list_backends')
if list_backends:
for backend in ServiceBackend.get_backends():
print(str(backend).split("'")[1])
self.stdout.write(str(backend).split("'")[1])
return
model = get_model(*options['model'].split('.'))
action = options.get('action')
@ -49,9 +49,9 @@ class Command(BaseCommand):
comps = iter(comp.split('='))
for arg in comps:
kwargs[arg] = next(comps).strip().rstrip(',')
operations = []
operations = set()
operations = OrderedSet()
route_cache = {}
queryset = model.objects.filter(**kwargs).order_by('id')
if servers:
server_objects = []
# Get and create missing Servers
@ -62,12 +62,12 @@ class Command(BaseCommand):
server = Server.objects.create(name=server, address=server)
server_objects.append(server)
# Generate operations for the given backend
for instance in model.objects.filter(**kwargs):
for instance in queryset:
for backend in backends:
backend = import_class(backend)
operations.add(Operation(backend, instance, action, servers=server_objects))
else:
for instance in model.objects.filter(**kwargs):
for instance in queryset:
manager.collect(instance, action, operations=operations, route_cache=route_cache)
scripts, block = manager.generate(operations)
servers = []
@ -76,11 +76,10 @@ class Command(BaseCommand):
server, __ = key
backend, operations = value
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:
script = '\n'.join(commands) + '\n'
script = script.encode('ascii', errors='replace')
sys.stdout.write(script.decode('ascii'))
script = '\n'.join(commands)
self.stdout.write(script)
if interactive:
context = {
'servers': ', '.join(servers),
@ -97,7 +96,7 @@ class Command(BaseCommand):
if not dry:
logs = manager.execute(scripts, block=block)
for log in logs:
print(log.stdout.encode('utf8', errors='replace'))
sys.stderr.write(log.stderr.encode('utf8', errors='replace'))
self.stdout.write(log.stdout)
self.stderr.write(log.stderr)
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.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 .backends import ServiceBackend
@ -138,7 +138,7 @@ def execute(scripts, block=False, async=False):
def collect(instance, action, **kwargs):
""" collect operations """
operations = kwargs.get('operations', set())
operations = kwargs.get('operations', OrderedSet())
route_cache = kwargs.get('route_cache', {})
for backend_cls in ServiceBackend.get_backends():
# 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 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',
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)
proforma = options.get('proforma', False)
for line in lines:
quantity = line.metric*line.size
if quantity == 0:
continue
service = line.order.service
# Create bill if needed
if bill is None or service.is_fee:
@ -33,36 +36,32 @@ class BillsBackend(object):
bill = Invoice.objects.create(account=account, is_open=True)
bills.append(bill)
# Create bill line
quantity = line.metric*line.size
if quantity != 0:
billine = bill.lines.create(
rate=service.nominal_price,
quantity=line.metric*line.size,
verbose_quantity=self.get_verbose_quantity(line),
subtotal=line.subtotal,
tax=service.tax,
description=self.get_line_description(line),
order=line.order,
order_billed_on=line.order.old_billed_on,
order_billed_until=line.order.old_billed_until
)
self.create_sublines(billine, line.discounts)
billine = bill.lines.create(
rate=service.nominal_price,
quantity=line.metric*line.size,
verbose_quantity=self.get_verbose_quantity(line),
subtotal=line.subtotal,
tax=service.tax,
description=self.get_line_description(line),
start_on=line.ini,
end_on=line.end if service.billing_period != service.NEVER else None,
order=line.order,
order_billed_on=line.order.old_billed_on,
order_billed_until=line.order.old_billed_until
)
self.create_sublines(billine, line.discounts)
return bills
def format_period(self, ini, end):
ini = ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
# def format_period(self, ini, end):
# ini = ini.strftime("%b, %Y")
# end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
# if ini == end:
# return ini
# return _("{ini} to {end}").format(ini=ini, end=end)
def get_line_description(self, line):
service = line.order.service
if service.is_fee:
return self.format_period(line.ini, line.end)
description = line.order.description
if service.billing_period != service.NEVER:
description += " %s" % self.format_period(line.ini, line.end)
return description
def get_verbose_quantity(self, line):

View File

@ -31,18 +31,15 @@ class BilledOrderListFilter(SimpleListFilter):
def lookups(self, request, model_admin):
return (
('to_date', _("To date")),
('full', _("Full period")),
('not', _("Not billed")),
('yes', _("Billed")),
('no', _("Not billed")),
)
def queryset(self, request, queryset):
if self.value() == 'to_date':
if self.value() == 'yes':
return queryset.filter(billed_until__isnull=False,
billed_until__gte=timezone.now())
elif self.value() == 'full':
raise NotImplementedError
elif self.value() == 'not':
elif self.value() == 'no':
return queryset.filter(
Q(billed_until__isnull=True) |
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)
else:
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
def givers(self, ini, end):
@ -173,6 +176,8 @@ class Order(models.Model):
def update(self):
instance = self.content_object
if instance is None:
return
handler = self.service.handler
metric = ''
if handler.metric:

View File

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

View File

@ -14,7 +14,7 @@ class PhpListSaaSBackend(ServiceController):
def _save(self, saas, server):
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'):
raise RuntimeError("Database is not yet configured")
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
@ -30,7 +30,7 @@ class PhpListSaaSBackend(ServiceController):
'adminpassword': saas.password,
}
response = requests.post(install_link, data=post)
print(response.content)
print(response.content.decode('utf8'))
if response.status_code != 200:
raise RuntimeError("Bad status code %i" % response.status_code)
else:

View File

@ -31,7 +31,7 @@ class WordpressMuBackend(ServiceController):
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)
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)
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+'
'class="edit">%s</a>' % webapp.name
)
content = session.get(search).content
content = session.get(search).content.decode('utf8')
# Get id
ids = regex.search(content)
if not ids:
@ -64,7 +64,7 @@ class WordpressMuBackend(ServiceController):
except RuntimeError:
url = self.get_base_url()
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 = wpnonce.search(content).groups()[0]
@ -94,7 +94,7 @@ class WordpressMuBackend(ServiceController):
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
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 = wpnonce.search(content).groups()[0]
data = {

View File

@ -11,12 +11,12 @@ from .. import settings
class GitLabForm(SoftwareServiceForm):
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):
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):

View File

@ -106,6 +106,8 @@ class SoftwareService(plugins.Plugin):
except IndexError:
pass
else:
if log.state != log.SUCCESS:
raise ValidationError(_("Validate creation execution has failed."))
errors = {}
if 'user-exists' in log.stdout:
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.
"""
_VOLUME = 'VOLUME'
_COMPENSATION = 'COMPENSATION'
_VOLUME = 'volume'
_COMPENSATION = 'compensation'
model = None
@ -42,29 +42,27 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
def validate_content_type(self, service):
pass
def validate_match(self, service):
if not service.match:
service.match = 'True'
def validate_expression(self, service, method):
try:
obj = service.content_type.model_class().objects.all()[0]
except IndexError:
return
try:
bool(self.matches(obj))
bool(getattr(self, method)(obj))
except Exception as exception:
name = type(exception).__name__
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):
try:
obj = service.content_type.model_class().objects.all()[0]
except IndexError:
return
try:
bool(self.get_metric(obj))
except Exception as exception:
name = type(exception).__name__
raise ValidationError(': '.join((name, str(exception))))
self.validate_expression(service, 'get_metric')
def validate_order_description(self, service):
self.validate_expression(service, 'get_order_description')
def get_content_type(self):
if not self.model:
@ -72,15 +70,26 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
app_label, model = self.model.split('.')
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):
if not self.match:
# Blank expressions always evaluate True
return True
safe_locals = {
'instance': instance,
'obj': instance,
instance._meta.model_name: instance,
}
safe_locals = self.get_expression_context(instance)
return eval(self.match, safe_locals)
def get_ignore_delta(self):
@ -113,27 +122,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
def get_metric(self, instance):
if self.metric:
safe_locals = {
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,
}
safe_locals = self.get_expression_context(instance)
try:
return eval(self.metric, safe_locals)
except Exception as error:
raise type(error)("%s on '%s'" %(error, self.service))
def get_order_description(self, instance):
safe_locals = {
'instance': instance,
'obj': instance,
'ugettext': ugettext,
instance._meta.model_name: instance,
}
safe_locals = self.get_expression_context(instance)
account = getattr(instance, 'account', instance)
with translation.override(account.language):
if not self.order_description:

View File

@ -91,7 +91,7 @@ class Service(models.Model):
help_text=_(
"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>"
"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,
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),
'match': (self.handler.validate_match, self),
'metric': (self.handler.validate_metric, self),
'order_description': (self.handler.validate_order_description, self),
})
def get_pricing_period(self):
@ -238,7 +239,10 @@ class Service(models.Model):
order_model = get_model(settings.SERVICES_ORDER_MODEL)
related_model = self.content_type.model_class()
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)
return updates

View File

@ -118,8 +118,18 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
)
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):
options = webapp.get_options(merge=self.MERGE)
options = self.get_options(webapp)
context.update({
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
'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
def get_fcgid_cmd_options(self, webapp, context):
options = webapp.get_options(merge=self.MERGE)
options = self.get_options(webapp)
maps = {
'MaxProcesses': options.get('processes', None),
'IOTimeout': options.get('timeout', None),

View File

@ -10,7 +10,12 @@ from . import WebAppServiceMixin
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")
default_route_match = "webapp.type.endswith('python')"
@ -26,26 +31,14 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
self.delete_webapp_dir(context)
def save_uwsgi(self, webapp, context):
self.append(textwrap.dedent("""\
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
)
self.append("echo '%(uwsgi_config)s' > %(vassal_path)s" % context)
def delete_uwsgi(self, webapp, context):
self.append("rm -f %(uwsgi_path)s" % context)
self.append("rm -f %(uwsgi_enabled)s" % context)
self.append("rm -f %(vassal_path)s" % 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("""\
# %(banner)s
[uwsgi]
plugins = python{python_version_number}
chdir = {app_path}
@ -70,10 +63,8 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
context.update({
'uwsgi_ini': self.get_uwsgi_ini(context),
'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR,
'uwsgi_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
'apps-available/%s.ini'% context['app_name']),
'uwsgi_enabled': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
'apps-enabled/%s.ini'% context['app_name']),
'vassal_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
'vassals/%s' % context['app_name']),
})
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()
@cached
def get_options(self, merge=False):
if merge:
options = OrderedDict()
qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type)
for name, value in qs.values_list('name', 'value').order_by('name'):
if name in options:
options[name] = max(options[name], value)
else:
options[name] = value
return options
return OrderedDict(self.options.values_list('name', 'value').order_by('name'))
def get_options(self, **kwargs):
options = OrderedDict()
if not kwargs:
kwargs = {
'webapp_id': self.pk,
}
qs = WebAppOption.objects.filter(**kwargs)
for name, value in qs.values_list('name', 'value').order_by('name'):
if name in options:
options[name] = max(options[name], value)
else:
options[name] = value
return options
def get_directive(self):
return self.type_instance.get_directive()

View File

@ -9,4 +9,4 @@ class Command(BaseCommand):
def handle(self, *filenames, **options):
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'
'xvfb-run -a -s "-screen 0 640x4800x16" '
'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

View File

@ -45,15 +45,15 @@ def read_async(fd):
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 """
if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
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()
yield
@ -62,16 +62,18 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
# Async reading of stdout and sterr
while True:
stdout = ''
stderr = ''
stdout = b''
stderr = b''
# Get complete unicode chunks
select.select([p.stdout, p.stderr], [], [])
stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr)
stdout += (stdoutPiece or b'').decode('utf8', errors='replace')
stderr += (stderrPiece or b'').decode('utf8', errors='replace')
stdout += (stdoutPiece or b'')
#.decode('ascii'), errors='replace')
stderr += (stderrPiece or b'')
#.decode('ascii'), errors='replace')
if display and stdout:
sys.stdout.write(stdout)
@ -89,14 +91,14 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
raise StopIteration
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True):
iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode)
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False):
iterator = runiterator(command, display, error_codes, silent, stdin)
next(iterator)
if async:
return iterator
stdout = ''
stderr = ''
stdout = b''
stderr = b''
for state in iterator:
stdout += state.stdout
stderr += state.stderr