Latest Pangea source code.

This commit is contained in:
Santiago Lamora 2020-03-18 07:49:04 +01:00
parent 49c84f13a8
commit 4606e86aaf
110 changed files with 2399 additions and 481 deletions

View File

@ -9,7 +9,7 @@ class SetPasswordApiMixin(object):
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer) @detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
def set_password(self, request, pk): def set_password(self, request, pk):
obj = self.get_object() obj = self.get_object()
data = request.DATA data = request.data
if isinstance(data, str): if isinstance(data, str):
data = { data = {
'password': data 'password': data

View File

@ -98,7 +98,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
pass pass
else: else:
password = attrs.pop('password', None) password = attrs.pop('password', None)
attrs = super(SetPasswordSerializer, self).validate() attrs = super().validate(attrs)
if password is not None: if password is not None:
attrs['password'] = password attrs['password'] = password
return attrs return attrs

View File

@ -1,7 +1,7 @@
import sys import sys
import textwrap import textwrap
from django.contrib.auth import get_user_model, base_user from django.contrib.auth import get_user_model
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
from django.db import models from django.db import models
@ -19,14 +19,9 @@ def create_initial_superuser(**kwargs):
) )
from ..models import Account from ..models import Account
try: try:
Account.systemusers.field.model.objects.filter(account_id=1).exists() Account.systemusers.field.related.model.objects.filter(account_id=1).exists()
except FieldError: except FieldError:
# avoid creating a systemuser when systemuser table is not ready # avoid creating a systemuser when systemuser table is not ready
Account.save = models.Model.save Account.save = models.Model.save
old_init = base_user.AbstractBaseUser.__init__
def remove_is_staff(*args, **kwargs):
kwargs.pop('is_staff', None)
old_init(*args, **kwargs)
base_user.AbstractBaseUser.__init__ = remove_is_staff
manager = sys.argv[0] manager = sys.argv[0]
execute_from_command_line(argv=[manager, 'createsuperuser']) execute_from_command_line(argv=[manager, 'createsuperuser'])

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import orchestra.contrib.accounts.models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='account',
managers=[
('objects', orchestra.contrib.accounts.models.AccountManager()),
],
),
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='account',
name='username',
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
),
]

View File

@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware #from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
#from orchestra.contrib.orchestration import Operation #from orchestra.contrib.orchestration import Operation
from orchestra.core import services from orchestra import core
from orchestra.models.utils import has_db_field from orchestra.models.utils import has_db_field
from orchestra.utils.mail import send_email_template from orchestra.utils.mail import send_email_template
@ -98,7 +98,7 @@ class Account(auth.AbstractBaseUser):
] ]
for rel in related_fields: for rel in related_fields:
source = getattr(rel, 'related_model', rel.model) source = getattr(rel, 'related_model', rel.model)
if source in services and hasattr(source, 'active'): if source in core.services and hasattr(source, 'active'):
for obj in getattr(self, rel.get_accessor_name()).all(): for obj in getattr(self, rel.get_accessor_name()).all():
yield obj yield obj
@ -141,12 +141,25 @@ class Account(auth.AbstractBaseUser):
backend returns True. Thus, a user who has permission from a single backend returns True. Thus, a user who has permission from a single
auth backend is assumed to have permission in general. If an object is auth backend is assumed to have permission in general. If an object is
provided, permissions for this specific object are checked. provided, permissions for this specific object are checked.
applabel.action_modelname
""" """
if not self.is_active:
return False
# Active superusers have all permissions. # Active superusers have all permissions.
if self.is_active and self.is_superuser: if self.is_superuser:
return True return True
# Otherwise we need to check the backends. app, action_model = perm.split('.')
return auth._user_has_perm(self, perm, obj) action, model = action_model.split('_', 1)
service_apps = set(model._meta.app_label for model in core.services.get().keys())
accounting_apps = set(model._meta.app_label for model in core.accounts.get().keys())
import inspect
if ((app in service_apps or (action == 'view' and app in accounting_apps))):
# class-level permissions
if inspect.isclass(obj):
return True
elif obj and getattr(obj, 'account', None) == self:
return True
def has_perms(self, perm_list, obj=None): def has_perms(self, perm_list, obj=None):
""" """
@ -167,7 +180,6 @@ class Account(auth.AbstractBaseUser):
# Active superusers have all permissions. # Active superusers have all permissions.
if self.is_active and self.is_superuser: if self.is_active and self.is_superuser:
return True return True
return auth._user_has_module_perms(self, app_label)
def get_related_passwords(self, db_field=False): def get_related_passwords(self, db_field=False):
related = [ related = [

View File

@ -7,7 +7,7 @@ class AccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = ( fields = (
'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined', 'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined', 'last_login',
'is_active' 'is_active'
) )

View File

@ -20,7 +20,7 @@ from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions from . import settings, actions
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter, from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
PaymentStateListFilter, AmendedListFilter) PaymentStateListFilter, AmendedListFilter)
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, from .models import (Bill, Invoice, AmendmentInvoice, AbonoInvoice, Fee, AmendmentFee, ProForma, BillLine,
BillSubline, BillContact) BillSubline, BillContact)
@ -461,6 +461,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
admin.site.register(Bill, BillAdmin) admin.site.register(Bill, BillAdmin)
admin.site.register(Invoice, BillAdmin) admin.site.register(Invoice, BillAdmin)
admin.site.register(AmendmentInvoice, BillAdmin) admin.site.register(AmendmentInvoice, BillAdmin)
admin.site.register(AbonoInvoice, BillAdmin)
admin.site.register(Fee, BillAdmin) admin.site.register(Fee, BillAdmin)
admin.site.register(AmendmentFee, BillAdmin) admin.site.register(AmendmentFee, BillAdmin)
admin.site.register(ProForma, BillAdmin) admin.site.register(ProForma, BillAdmin)

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-10-29 10:51+0000\n" "POT-Creation-Date: 2019-12-20 11:56+0100\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,33 +18,33 @@ 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:31 #: actions.py:33
msgid "View" msgid "View"
msgstr "Vista" msgstr "Vista"
#: actions.py:42 #: actions.py:45
msgid "Selected bills should be in open state" msgid "Selected bills should be in open state"
msgstr "Les factures seleccionades han d'estar en estat obert" msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:57 #: actions.py:60
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "Les factures seleccionades han estat tancades" msgstr "Les factures seleccionades han estat tancades"
#: actions.py:70 #: actions.py:73
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created" msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>" msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:71 #: actions.py:74
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created" msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>" msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:77 #: actions.py:80
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "Estàs a punt de tancar les següents factures, estàs segur?" msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
#: actions.py:78 #: actions.py:81
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"
@ -52,174 +52,205 @@ msgstr ""
"Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us " "Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us "
"plau selecciona un mètode de pagament per les factures seleccionades" "plau selecciona un mètode de pagament per les factures seleccionades"
#: actions.py:91 #: actions.py:97
msgid "Close" msgid "Close"
msgstr "Tanca" msgstr "Tanca"
#: actions.py:109 #: actions.py:115
msgid "One bill has been sent." msgid "One bill has been sent."
msgstr "S'ha creat una factura" msgstr "S'ha creat una factura"
#: actions.py:110 #: actions.py:116
#, python-format #, python-format
msgid "%i bills have been sent." msgid "%i bills have been sent."
msgstr "S'han enviat %i factures." msgstr "S'han enviat %i factures."
#: actions.py:117 #: actions.py:123
msgid "Resend" msgid "Resend"
msgstr "Reenviat" msgstr "Reenviat"
#: actions.py:137 #: actions.py:146
msgid "Download" msgid "Download"
msgstr "Descarrega" msgstr "Descarrega"
#: actions.py:153 #: actions.py:162
msgid "C.S.D." msgid "C.S.D."
msgstr "" msgstr ""
#: actions.py:155 #: actions.py:164
msgid "Close, send and download bills in one shot." msgid "Close, send and download bills in one shot."
msgstr "" msgstr ""
#: actions.py:216 #: actions.py:225
#, python-format #, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed." msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "%(norders)s ordres i %(nlines)s línies desfetes." msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
#: actions.py:235 #: actions.py:244
msgid "Lines moved" msgid "Lines moved"
msgstr "Línies mogudes" msgstr "Línies mogudes"
#: actions.py:248 #: actions.py:257
msgid "Selected bills should be in closed state" msgid "Selected bills should be in closed state"
msgstr "Les factures seleccionades han d'estar en estat obert" msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:265 #: actions.py:259
#, python-format
msgid "%s can not be amended."
msgstr ""
#: actions.py:279
#, python-format #, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s" msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s" msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s"
#: actions.py:272 #: actions.py:286
#, python-format #, python-format
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%" msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%" msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:288 #: actions.py:303
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated." msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>" msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:289 #: actions.py:304
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated." msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>" msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:292 #: actions.py:307
msgid "Amend" msgid "Amend"
msgstr "" msgstr ""
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11 #: admin.py:80 admin.py:126 admin.py:180 forms.py:11
#: templates/admin/bills/bill/report.html:43 #: templates/admin/bills/bill/report.html:43
#: templates/admin/bills/bill/report.html:70 #: templates/admin/bills/bill/report.html:70
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: admin.py:89 #: admin.py:112
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: admin.py:97 #: admin.py:120
msgid "Subtotal" msgid "Subtotal"
msgstr "Subtotal" msgstr "Subtotal"
#: admin.py:130 #: admin.py:146
#, fuzzy
#| msgid "Total"
msgid "Totals"
msgstr "Total"
#: admin.py:150
msgid "Order"
msgstr ""
#: admin.py:169
msgid "Is open" msgid "Is open"
msgstr "És oberta" msgstr "És oberta"
#: admin.py:135 #: admin.py:175
msgid "Subline" #, fuzzy
#| msgid "Subline"
msgid "Sublines"
msgstr "Sublínia" msgstr "Sublínia"
#: admin.py:167 #: admin.py:221
msgid "No bills selected." msgid "No bills selected."
msgstr "No hi ha factures seleccionades" msgstr "No hi ha factures seleccionades"
#: admin.py:174 #: admin.py:229
#, python-format #, fuzzy, python-format
msgid "Manage %s bill lines." #| msgid "Manage %s bill lines."
msgid "Manage %s bill lines"
msgstr "Gestiona %s línies de factura." msgstr "Gestiona %s línies de factura."
#: admin.py:176 #: admin.py:231
msgid "Bill not in open state." msgid "Bill not in open state."
msgstr "La factura no està en estat obert" msgstr "La factura no està en estat obert"
#: admin.py:179 #: admin.py:234
msgid "Not all bills are in open state." msgid "Not all bills are in open state."
msgstr "No totes les factures estan en estat obert" msgstr "No totes les factures estan en estat obert"
#: admin.py:180 #: admin.py:235
msgid "Manage bill lines of multiple bills." #, fuzzy
#| msgid "Manage bill lines of multiple bills."
msgid "Manage bill lines of multiple bills"
msgstr "Gestiona línies de factura de multiples factures." msgstr "Gestiona línies de factura de multiples factures."
#: admin.py:204 #: admin.py:250
msgid "Dates" #, python-format
msgid "Subtotal %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:209 #: admin.py:251
msgid "Raw" #, python-format
msgstr "Raw" msgid "Taxes %s%% VAT %s &%s;"
msgstr ""
#: admin.py:235 models.py:73 #: admin.py:255 admin.py:381 filters.py:46
msgid "Created" #: templates/bills/microspective.html:123
msgstr "Creada" msgid "total"
msgstr "total"
#: admin.py:236 #: admin.py:275
#, fuzzy msgid "This bill has been amended, this value may not be valid."
#| msgid "Close" msgstr ""
msgid "Closed"
msgstr "Tanca"
#: admin.py:237 #: admin.py:280
#, fuzzy msgid "Payment"
#| msgid "updated on" msgstr "Pagament"
msgid "Updated"
msgstr "actualitzada el"
#: admin.py:246 #: admin.py:304
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "Amends" msgid "Amends"
msgstr "línia rectificada" msgstr "línia rectificada"
#: admin.py:252 #: admin.py:330
msgid "Dates"
msgstr ""
#: admin.py:335
msgid "Raw"
msgstr "Raw"
#: admin.py:358 models.py:75
msgid "Created"
msgstr "Creada"
#: admin.py:359
#, fuzzy
#| msgid "Close"
msgid "Closed"
msgstr "Tanca"
#: admin.py:360
#, fuzzy
#| msgid "updated on"
msgid "Updated"
msgstr "actualitzada el"
#: admin.py:375
msgid "lines" msgid "lines"
msgstr "línies" msgstr "línies"
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118 #: admin.py:389 models.py:108 models.py:501
msgid "total"
msgstr "total"
#: admin.py:265 models.py:104 models.py:460
msgid "type" msgid "type"
msgstr "tipus" msgstr "tipus"
#: admin.py:282
msgid "This bill has been amended, this value may not be valid."
msgstr ""
#: admin.py:287
msgid "Payment"
msgstr "Pagament"
#: filters.py:21 #: filters.py:21
msgid "All" msgid "All"
msgstr "Tot" msgstr "Tot"
#: filters.py:22 models.py:88 #: filters.py:22 models.py:91
msgid "Invoice" msgid "Invoice"
msgstr "Factura" msgstr "Factura"
#: filters.py:23 models.py:90 #: filters.py:23 models.py:93
msgid "Fee" msgid "Fee"
msgstr "Quota de soci" msgstr "Quota de soci"
@ -231,65 +262,67 @@ msgstr "Pro-forma"
msgid "Amendment fee" msgid "Amendment fee"
msgstr "Rectificació de quota de soci" msgstr "Rectificació de quota de soci"
#: filters.py:26 models.py:89 #: filters.py:26 models.py:92
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificativa" msgstr "Factura rectificativa"
#: filters.py:68 #: filters.py:71
msgid "has bill contact" msgid "has bill contact"
msgstr "té contacte de facturació" msgstr "té contacte de facturació"
#: filters.py:73 #: filters.py:76
msgid "Yes" msgid "Yes"
msgstr "Si" msgstr "Si"
#: filters.py:74 #: filters.py:77
msgid "No" msgid "No"
msgstr "No" msgstr "No"
#: filters.py:85 #: filters.py:88
msgid "payment state" msgid "payment state"
msgstr "Pagament" msgstr "Pagament"
#: filters.py:90 models.py:72 #: filters.py:93 models.py:74
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: filters.py:91 models.py:76 #: filters.py:94 models.py:78
msgid "Paid" msgid "Paid"
msgstr "Pagat" msgstr "Pagat"
#: filters.py:92 #: filters.py:95
msgid "Pending" msgid "Pending"
msgstr "Pendent" msgstr "Pendent"
#: filters.py:93 models.py:79 #: filters.py:96 models.py:81
msgid "Bad debt" msgid "Bad debt"
msgstr "Incobrable" msgstr "Incobrable"
#: filters.py:135 #: filters.py:138
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "amended" msgid "amended"
msgstr "línia rectificada" msgstr "línia rectificada"
#: filters.py:140 #: filters.py:143
#, fuzzy #, fuzzy
#| msgid "Due date" #| msgid "Due date"
msgid "Closed amends" msgid "Closed amends"
msgstr "Data de pagament" msgstr "Data de pagament"
#: filters.py:141 #: filters.py:144
msgid "Open or closed amends"
msgstr ""
#: filters.py:142
#, fuzzy #, fuzzy
#| msgid "closed on" #| msgid "Due date"
msgid "No closed amends" msgid "Open amends"
msgstr "tancat el" msgstr "Data de pagament"
#: filters.py:143 #: filters.py:145
#, fuzzy
#| msgid "amended line"
msgid "Any amends"
msgstr "línia rectificada"
#: filters.py:146
msgid "No amends" msgid "No amends"
msgstr "" msgstr ""
@ -309,7 +342,7 @@ msgstr "Tipus"
msgid "Source" msgid "Source"
msgstr "Font" msgstr "Font"
#: helpers.py:10 #: helpers.py:14
msgid "" msgid ""
"{relation} account \"{account}\" does not have a declared invoice contact. " "{relation} account \"{account}\" does not have a declared invoice contact. "
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>" "You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
@ -317,213 +350,235 @@ msgstr ""
"{relation} compte \"{account}\" no te un contacte de facturació. Hauries de " "{relation} compte \"{account}\" no te un contacte de facturació. Hauries de "
"<a href=\"{url}#invoicecontact-group\">proporcionar un</a>" "<a href=\"{url}#invoicecontact-group\">proporcionar un</a>"
#: helpers.py:17 #: helpers.py:21
msgid "Related" msgid "Related"
msgstr "Relacionat" msgstr "Relacionat"
#: helpers.py:24 #: helpers.py:28
msgid "Main" msgid "Main"
msgstr "Principal" msgstr "Principal"
#: models.py:24 models.py:100 #: models.py:26 models.py:104
msgid "account" msgid "account"
msgstr "compte" msgstr "compte"
#: models.py:26 #: models.py:28
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: models.py:27 #: models.py:29
msgid "Account full name will be used when left blank." msgid "Account full name will be used when left blank."
msgstr "S'emprarà el nom complet del compte quan es deixi en blanc." msgstr "S'emprarà el nom complet del compte quan es deixi en blanc."
#: models.py:28 #: models.py:30
msgid "address" msgid "address"
msgstr "adreça" msgstr "adreça"
#: models.py:29 #: models.py:31
msgid "city" msgid "city"
msgstr "ciutat" msgstr "ciutat"
#: models.py:31 #: models.py:33
msgid "zip code" msgid "zip code"
msgstr "codi postal" msgstr "codi postal"
#: models.py:32 #: models.py:34
msgid "Enter a valid zipcode." msgid "Enter a valid zipcode."
msgstr "Introdueix un codi postal vàlid." msgstr "Introdueix un codi postal vàlid."
#: models.py:33 #: models.py:35
msgid "country" msgid "country"
msgstr "país" msgstr "país"
#: models.py:36 templates/admin/bills/bill/report.html:65 #: models.py:38 templates/admin/bills/bill/report.html:65
msgid "VAT number" msgid "VAT number"
msgstr "NIF" msgstr "NIF"
#: models.py:74 #: models.py:76
msgid "Processed" msgid "Processed"
msgstr "" msgstr ""
#: models.py:75 #: models.py:77
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "Amended" msgid "Amended"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:77 #: models.py:79
msgid "Incomplete" msgid "Incomplete"
msgstr "" msgstr ""
#: models.py:78 #: models.py:80
msgid "Executed" msgid "Executed"
msgstr "" msgstr ""
#: models.py:91 #: models.py:94
msgid "Amendment Fee" msgid "Amendment Fee"
msgstr "Rectificació de quota de soci" msgstr "Rectificació de quota de soci"
#: models.py:92 #: models.py:95
#, fuzzy
#| msgid "Invoice"
msgid "Abono Invoice"
msgstr "Abonament"
#: models.py:96
msgid "Pro forma" msgid "Pro forma"
msgstr "Pro forma" msgstr "Pro forma"
#: models.py:99 #: models.py:103
msgid "number" msgid "number"
msgstr "número" msgstr "número"
#: models.py:102 #: models.py:106
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "amend of" msgid "amend of"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:105 #: models.py:109
msgid "created on" msgid "created on"
msgstr "creat el" msgstr "creat el"
#: models.py:106 #: models.py:110
msgid "closed on" msgid "closed on"
msgstr "tancat el" msgstr "tancat el"
#: models.py:107 #: models.py:111
msgid "open" msgid "open"
msgstr "obert" msgstr "obert"
#: models.py:108 #: models.py:112
msgid "sent" msgid "sent"
msgstr "enviat" msgstr "enviat"
#: models.py:109 #: models.py:113
msgid "due on" msgid "due on"
msgstr "es deu" msgstr "es deu"
#: models.py:110 #: models.py:114
msgid "updated on" msgid "updated on"
msgstr "actualitzada el" msgstr "actualitzada el"
#: models.py:112 #: models.py:116
msgid "comments" msgid "comments"
msgstr "comentaris" msgstr "comentaris"
#: models.py:113 #: models.py:117
msgid "HTML" msgid "HTML"
msgstr "HTML" msgstr "HTML"
#: models.py:194 #: models.py:200
#, python-format #, python-format
msgid "Type %s is not an amendment." msgid "Type %s is not an amendment."
msgstr "" msgstr ""
#: models.py:196 #: models.py:202
msgid "Amend of related account doesn't match bill account." msgid "Amend of related account doesn't match bill account."
msgstr "" msgstr ""
#: models.py:198 #: models.py:204
#, fuzzy #, fuzzy
#| msgid "Bill not in open state." #| msgid "Bill not in open state."
msgid "Related invoice is in open state." msgid "Related invoice is in open state."
msgstr "La factura no està en estat obert" msgstr "La factura no està en estat obert"
#: models.py:200 #: models.py:206
msgid "Related invoice is an amendment." msgid "Related invoice is an amendment."
msgstr "" msgstr ""
#: models.py:392 #: models.py:419
msgid "bill" msgid "bill"
msgstr "factura" msgstr "factura"
#: models.py:393 models.py:458 templates/bills/microspective.html:73 #: models.py:420 models.py:499 templates/bills/microspective.html:75
msgid "description" msgid "description"
msgstr "descripció" msgstr "descripció"
#: models.py:394 #: models.py:421
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:395 #: models.py:422
msgid "quantity" msgid "quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:397 #: models.py:424
#, fuzzy #, fuzzy
#| msgid "quantity" #| msgid "quantity"
msgid "Verbose quantity" msgid "Verbose quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:398 templates/admin/bills/bill/report.html:47 #: models.py:425 templates/admin/bills/bill/report.html:47
#: templates/bills/microspective.html:77 #: templates/bills/microspective.html:79
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
msgid "subtotal" msgid "subtotal"
msgstr "subtotal" msgstr "subtotal"
#: models.py:399 #: models.py:426
msgid "tax" msgid "tax"
msgstr "impostos" msgstr "impostos"
#: models.py:400 #: models.py:427
msgid "start" msgid "start"
msgstr "iniciar" msgstr "iniciar"
#: models.py:401 #: models.py:428
msgid "end" msgid "end"
msgstr "finalitzar" msgstr "finalitzar"
#: models.py:403 #: models.py:431
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "Enllaç informatiu de l'ordre" msgstr "Enllaç informatiu de l'ordre"
#: models.py:404 #: models.py:432
msgid "order billed" msgid "order billed"
msgstr "ordre facturada" msgstr "ordre facturada"
#: models.py:405 #: models.py:433
msgid "order billed until" msgid "order billed until"
msgstr "ordre facturada fins a" msgstr "ordre facturada fins a"
#: models.py:406 #: models.py:434
msgid "created" msgid "created"
msgstr "creada" msgstr "creada"
#: models.py:408 #: models.py:436
msgid "amended line" msgid "amended line"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:451 #: models.py:492
msgid "Volume" msgid "Volume"
msgstr "Volum" msgstr "Volum"
#: models.py:452 #: models.py:493
msgid "Compensation" msgid "Compensation"
msgstr "Compensació" msgstr "Compensació"
#: models.py:453 #: models.py:494
msgid "Other" msgid "Other"
msgstr "Altre" msgstr "Altre"
#: models.py:457 #: models.py:498
msgid "bill line" msgid "bill line"
msgstr "línia de factura" msgstr "línia de factura"
#: templates/admin/bills/bill/change_list.html:9
#, fuzzy
#| msgid "lines"
msgid "Lines"
msgstr "línies"
#: templates/admin/bills/bill/change_list.html:15
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/admin/bills/bill/close_send_download_bills.html:57
msgid "Yes, I'm sure"
msgstr ""
#: templates/admin/bills/bill/report.html:42 #: templates/admin/bills/bill/report.html:42
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
@ -531,19 +586,19 @@ msgstr ""
#: templates/admin/bills/bill/report.html:47 #: templates/admin/bills/bill/report.html:47
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/admin/bills/bill/report.html:69 #: templates/admin/bills/bill/report.html:69
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "VAT" msgid "VAT"
msgstr "IVA" msgstr "IVA"
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "taxes" msgid "taxes"
msgstr "impostos" msgstr "impostos"
#: templates/admin/bills/bill/report.html:56 #: templates/admin/bills/bill/report.html:56
#: templates/admin/bills/billline/report.html:60 #: templates/admin/bills/billline/report.html:60
#: templates/bills/microspective.html:53 #: templates/bills/microspective.html:54
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
@ -561,8 +616,20 @@ msgstr "Data de pagament"
msgid "Base" msgid "Base"
msgstr "" msgstr ""
#: templates/admin/bills/billline/change_list.html:6
msgid "Home"
msgstr ""
#: templates/admin/bills/billline/change_list.html:8
msgid "Bills"
msgstr ""
#: templates/admin/bills/billline/change_list.html:9
msgid "Multiple bills"
msgstr ""
#: templates/admin/bills/billline/report.html:42 #: templates/admin/bills/billline/report.html:42
msgid "Services" msgid "Service"
msgstr "" msgstr ""
#: templates/admin/bills/billline/report.html:43 #: templates/admin/bills/billline/report.html:43
@ -587,27 +654,21 @@ msgstr "quantitat"
msgid "Profit" msgid "Profit"
msgstr "" msgstr ""
#: templates/admin/bills/change_list.html:9 #: templates/bills/microspective-fee.html:115
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/bills/microspective-fee.html:107
msgid "Due date" msgid "Due date"
msgstr "Data de pagament" msgstr "Data de pagament"
#: templates/bills/microspective-fee.html:108 #: templates/bills/microspective-fee.html:116
#, python-format #, python-format
msgid "On %(bank_account)s" msgid "On %(bank_account)s"
msgstr "Al %(bank_account)s" msgstr "Al %(bank_account)s"
#: templates/bills/microspective-fee.html:114 #: templates/bills/microspective-fee.html:122
#, python-format #, python-format
msgid "From %(ini)s to %(end)s" msgid "From %(ini)s to %(end)s"
msgstr "De %(ini)s a %(end)s" msgstr "De %(ini)s a %(end)s"
#: templates/bills/microspective-fee.html:121 #: templates/bills/microspective-fee.html:144
msgid "" msgid ""
"\n" "\n"
"<strong>With your membership</strong> you are supporting ...\n" "<strong>With your membership</strong> you are supporting ...\n"
@ -615,36 +676,36 @@ msgstr ""
"\n" "\n"
"<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n" "<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n"
#: templates/bills/microspective.html:49 #: templates/bills/microspective.html:50
msgid "DUE DATE" msgid "DUE DATE"
msgstr "VENCIMENT" msgstr "VENCIMENT"
#: templates/bills/microspective.html:57 #: templates/bills/microspective.html:58
#, python-format #, python-format
msgid "%(bill_type)s DATE" msgid "%(bill_type)s DATE"
msgstr "DATA %(bill_type)s" msgstr "DATA %(bill_type)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:76
msgid "period" msgid "period"
msgstr "període" msgstr "període"
#: templates/bills/microspective.html:75 #: templates/bills/microspective.html:77
msgid "hrs/qty" msgid "hrs/qty"
msgstr "hrs/qnt" msgstr "hrs/qnt"
#: templates/bills/microspective.html:76 #: templates/bills/microspective.html:78
msgid "rate/price" msgid "rate/price"
msgstr "tarifa/preu" msgstr "tarifa/preu"
#: templates/bills/microspective.html:131 #: templates/bills/microspective.html:137
msgid "COMMENTS" msgid "COMMENTS"
msgstr "COMENTARIS" msgstr "COMENTARIS"
#: templates/bills/microspective.html:138 #: templates/bills/microspective.html:145
msgid "PAYMENT" msgid "PAYMENT"
msgstr "PAGAMENT" msgstr "PAGAMENT"
#: templates/bills/microspective.html:142 #: templates/bills/microspective.html:149
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -658,11 +719,11 @@ msgstr ""
"Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el " "Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el "
"teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és" "teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és"
#: templates/bills/microspective.html:151 #: templates/bills/microspective.html:160
msgid "QUESTIONS" msgid "QUESTIONS"
msgstr "PREGUNTES" msgstr "PREGUNTES"
#: templates/bills/microspective.html:152 #: templates/bills/microspective.html:161
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -679,5 +740,10 @@ msgstr ""
"ràpidament possible.\n" "ràpidament possible.\n"
" " " "
#, fuzzy
#~| msgid "closed on"
#~ msgid "No closed amends"
#~ msgstr "tancat el"
#~ msgid "positive price" #~ msgid "positive price"
#~ msgstr "preu positiu" #~ msgstr "preu positiu"

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-10-29 10:51+0000\n" "POT-Creation-Date: 2019-12-20 11:56+0100\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,33 +18,33 @@ 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:31 #: actions.py:33
msgid "View" msgid "View"
msgstr "Vista" msgstr "Vista"
#: actions.py:42 #: actions.py:45
msgid "Selected bills should be in open state" msgid "Selected bills should be in open state"
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:57 #: actions.py:60
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "Las facturas seleccionadas han sido cerradas" msgstr "Las facturas seleccionadas han sido cerradas"
#: actions.py:70 #: actions.py:73
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created" msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>" msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:71 #: actions.py:74
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created" msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>" msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:77 #: actions.py:80
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?" msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
#: actions.py:78 #: actions.py:81
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"
@ -52,174 +52,199 @@ msgstr ""
"Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor " "Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor "
"seleciona un metodo de pago para las facturas seleccionadas" "seleciona un metodo de pago para las facturas seleccionadas"
#: actions.py:91 #: actions.py:97
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
#: actions.py:109 #: actions.py:115
msgid "One bill has been sent." msgid "One bill has been sent."
msgstr "Se ha enviado una factura" msgstr "Se ha enviado una factura"
#: actions.py:110 #: actions.py:116
#, python-format #, python-format
msgid "%i bills have been sent." msgid "%i bills have been sent."
msgstr "" msgstr ""
#: actions.py:117 #: actions.py:123
msgid "Resend" msgid "Resend"
msgstr "" msgstr ""
#: actions.py:137 #: actions.py:146
msgid "Download" msgid "Download"
msgstr "Descarga" msgstr "Descarga"
#: actions.py:153 #: actions.py:162
msgid "C.S.D." msgid "C.S.D."
msgstr "" msgstr ""
#: actions.py:155 #: actions.py:164
msgid "Close, send and download bills in one shot." msgid "Close, send and download bills in one shot."
msgstr "" msgstr ""
#: actions.py:216 #: actions.py:225
#, python-format #, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed." msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "" msgstr ""
#: actions.py:235 #: actions.py:244
msgid "Lines moved" msgid "Lines moved"
msgstr "" msgstr ""
#: actions.py:248 #: actions.py:257
msgid "Selected bills should be in closed state" msgid "Selected bills should be in closed state"
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:265 #: actions.py:259
#, python-format
msgid "%s can not be amended."
msgstr ""
#: actions.py:279
#, python-format #, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s" msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s" msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s"
#: actions.py:272 #: actions.py:286
#, python-format #, python-format
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%" msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%" msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:288 #: actions.py:303
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated." msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>" msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:289 #: actions.py:304
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated." msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>" msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:292 #: actions.py:307
msgid "Amend" msgid "Amend"
msgstr "" msgstr ""
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11 #: admin.py:80 admin.py:126 admin.py:180 forms.py:11
#: templates/admin/bills/bill/report.html:43 #: templates/admin/bills/bill/report.html:43
#: templates/admin/bills/bill/report.html:70 #: templates/admin/bills/bill/report.html:70
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: admin.py:89 #: admin.py:112
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: admin.py:97 #: admin.py:120
msgid "Subtotal" msgid "Subtotal"
msgstr "" msgstr ""
#: admin.py:130 #: admin.py:146
msgid "Totals"
msgstr ""
#: admin.py:150
msgid "Order"
msgstr ""
#: admin.py:169
msgid "Is open" msgid "Is open"
msgstr "" msgstr ""
#: admin.py:135 #: admin.py:175
msgid "Subline" msgid "Sublines"
msgstr "" msgstr ""
#: admin.py:167 #: admin.py:221
msgid "No bills selected." msgid "No bills selected."
msgstr "" msgstr ""
#: admin.py:174 #: admin.py:229
#, python-format #, fuzzy, python-format
msgid "Manage %s bill lines." #| msgid "bill line"
msgstr "" msgid "Manage %s bill lines"
msgstr "linea de factura"
#: admin.py:176 #: admin.py:231
msgid "Bill not in open state." msgid "Bill not in open state."
msgstr "" msgstr ""
#: admin.py:179 #: admin.py:234
msgid "Not all bills are in open state." msgid "Not all bills are in open state."
msgstr "" msgstr ""
#: admin.py:180 #: admin.py:235
msgid "Manage bill lines of multiple bills." msgid "Manage bill lines of multiple bills"
msgstr "" msgstr ""
#: admin.py:204 #: admin.py:250
msgid "Dates" #, python-format
msgid "Subtotal %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:209 #: admin.py:251
msgid "Raw" #, python-format
msgid "Taxes %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:235 models.py:73 #: admin.py:255 admin.py:381 filters.py:46
msgid "Created" #: templates/bills/microspective.html:123
msgid "total"
msgstr "" msgstr ""
#: admin.py:236 #: admin.py:275
#, fuzzy msgid "This bill has been amended, this value may not be valid."
#| msgid "Close" msgstr ""
msgid "Closed"
msgstr "Cerrar"
#: admin.py:237 #: admin.py:280
#, fuzzy msgid "Payment"
#| msgid "updated on" msgstr "Pago"
msgid "Updated"
msgstr "actualizada en"
#: admin.py:246 #: admin.py:304
#, fuzzy #, fuzzy
#| msgid "Amended" #| msgid "Amended"
msgid "Amends" msgid "Amends"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: admin.py:252 #: admin.py:330
msgid "Dates"
msgstr ""
#: admin.py:335
msgid "Raw"
msgstr ""
#: admin.py:358 models.py:75
msgid "Created"
msgstr ""
#: admin.py:359
#, fuzzy
#| msgid "Close"
msgid "Closed"
msgstr "Cerrar"
#: admin.py:360
#, fuzzy
#| msgid "updated on"
msgid "Updated"
msgstr "actualizada en"
#: admin.py:375
msgid "lines" msgid "lines"
msgstr "" msgstr ""
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118 #: admin.py:389 models.py:108 models.py:501
msgid "total"
msgstr ""
#: admin.py:265 models.py:104 models.py:460
msgid "type" msgid "type"
msgstr "" msgstr ""
#: admin.py:282
msgid "This bill has been amended, this value may not be valid."
msgstr ""
#: admin.py:287
msgid "Payment"
msgstr "Pago"
#: filters.py:21 #: filters.py:21
msgid "All" msgid "All"
msgstr "" msgstr ""
#: filters.py:22 models.py:88 #: filters.py:22 models.py:91
msgid "Invoice" msgid "Invoice"
msgstr "Factura" msgstr "Factura"
#: filters.py:23 models.py:90 #: filters.py:23 models.py:93
msgid "Fee" msgid "Fee"
msgstr "Cuota de socio" msgstr "Cuota de socio"
@ -231,65 +256,67 @@ msgstr ""
msgid "Amendment fee" msgid "Amendment fee"
msgstr "Cuota rectificativa" msgstr "Cuota rectificativa"
#: filters.py:26 models.py:89 #: filters.py:26 models.py:92
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificativa" msgstr "Factura rectificativa"
#: filters.py:68 #: filters.py:71
msgid "has bill contact" msgid "has bill contact"
msgstr "" msgstr ""
#: filters.py:73 #: filters.py:76
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: filters.py:74 #: filters.py:77
msgid "No" msgid "No"
msgstr "" msgstr ""
#: filters.py:85 #: filters.py:88
msgid "payment state" msgid "payment state"
msgstr "Pago" msgstr "Pago"
#: filters.py:90 models.py:72 #: filters.py:93 models.py:74
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: filters.py:91 models.py:76 #: filters.py:94 models.py:78
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: filters.py:92 #: filters.py:95
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
#: filters.py:93 models.py:79 #: filters.py:96 models.py:81
msgid "Bad debt" msgid "Bad debt"
msgstr "" msgstr ""
#: filters.py:135 #: filters.py:138
#, fuzzy #, fuzzy
#| msgid "Amended" #| msgid "Amended"
msgid "amended" msgid "amended"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: filters.py:140 #: filters.py:143
#, fuzzy #, fuzzy
#| msgid "Due date" #| msgid "Due date"
msgid "Closed amends" msgid "Closed amends"
msgstr "Fecha de pago" msgstr "Fecha de pago"
#: filters.py:141 #: filters.py:144
msgid "Open or closed amends"
msgstr ""
#: filters.py:142
#, fuzzy #, fuzzy
#| msgid "closed on" #| msgid "Due date"
msgid "No closed amends" msgid "Open amends"
msgstr "cerrada en" msgstr "Fecha de pago"
#: filters.py:143 #: filters.py:145
#, fuzzy
#| msgid "Amended"
msgid "Any amends"
msgstr "Quota rectificativa"
#: filters.py:146
msgid "No amends" msgid "No amends"
msgstr "" msgstr ""
@ -309,213 +336,233 @@ msgstr ""
msgid "Source" msgid "Source"
msgstr "" msgstr ""
#: helpers.py:10 #: helpers.py:14
msgid "" msgid ""
"{relation} account \"{account}\" does not have a declared invoice contact. " "{relation} account \"{account}\" does not have a declared invoice contact. "
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>" "You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
msgstr "" msgstr ""
#: helpers.py:17 #: helpers.py:21
msgid "Related" msgid "Related"
msgstr "" msgstr ""
#: helpers.py:24 #: helpers.py:28
msgid "Main" msgid "Main"
msgstr "" msgstr ""
#: models.py:24 models.py:100 #: models.py:26 models.py:104
msgid "account" msgid "account"
msgstr "" msgstr ""
#: models.py:26 #: models.py:28
msgid "name" msgid "name"
msgstr "" msgstr ""
#: models.py:27 #: models.py:29
msgid "Account full name will be used when left blank." msgid "Account full name will be used when left blank."
msgstr "" msgstr ""
#: models.py:28 #: models.py:30
msgid "address" msgid "address"
msgstr "" msgstr ""
#: models.py:29 #: models.py:31
msgid "city" msgid "city"
msgstr "" msgstr ""
#: models.py:31 #: models.py:33
msgid "zip code" msgid "zip code"
msgstr "" msgstr ""
#: models.py:32 #: models.py:34
msgid "Enter a valid zipcode." msgid "Enter a valid zipcode."
msgstr "" msgstr ""
#: models.py:33 #: models.py:35
msgid "country" msgid "country"
msgstr "" msgstr ""
#: models.py:36 templates/admin/bills/bill/report.html:65 #: models.py:38 templates/admin/bills/bill/report.html:65
msgid "VAT number" msgid "VAT number"
msgstr "" msgstr ""
#: models.py:74 #: models.py:76
msgid "Processed" msgid "Processed"
msgstr "" msgstr ""
#: models.py:75 #: models.py:77
msgid "Amended" msgid "Amended"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: models.py:77 #: models.py:79
msgid "Incomplete" msgid "Incomplete"
msgstr "" msgstr ""
#: models.py:78 #: models.py:80
msgid "Executed" msgid "Executed"
msgstr "" msgstr ""
#: models.py:91 #: models.py:94
msgid "Amendment Fee" msgid "Amendment Fee"
msgstr "" msgstr ""
#: models.py:92 #: models.py:95
#, fuzzy
#| msgid "Invoice"
msgid "Abono Invoice"
msgstr "Abono"
#: models.py:96
msgid "Pro forma" msgid "Pro forma"
msgstr "" msgstr ""
#: models.py:99 #: models.py:103
msgid "number" msgid "number"
msgstr "número" msgstr "número"
#: models.py:102 #: models.py:106
msgid "amend of" msgid "amend of"
msgstr "rectificación de" msgstr "rectificación de"
#: models.py:105 #: models.py:109
msgid "created on" msgid "created on"
msgstr "creado en" msgstr "creado en"
#: models.py:106 #: models.py:110
msgid "closed on" msgid "closed on"
msgstr "cerrada en" msgstr "cerrada en"
#: models.py:107 #: models.py:111
msgid "open" msgid "open"
msgstr "abierta" msgstr "abierta"
#: models.py:108 #: models.py:112
msgid "sent" msgid "sent"
msgstr "enviada" msgstr "enviada"
#: models.py:109 #: models.py:113
msgid "due on" msgid "due on"
msgstr "vencimiento" msgstr "vencimiento"
#: models.py:110 #: models.py:114
msgid "updated on" msgid "updated on"
msgstr "actualizada en" msgstr "actualizada en"
#: models.py:112 #: models.py:116
msgid "comments" msgid "comments"
msgstr "comentarios" msgstr "comentarios"
#: models.py:113 #: models.py:117
msgid "HTML" msgid "HTML"
msgstr "HTML" msgstr "HTML"
#: models.py:194 #: models.py:200
#, python-format #, python-format
msgid "Type %s is not an amendment." msgid "Type %s is not an amendment."
msgstr "" msgstr ""
#: models.py:196 #: models.py:202
msgid "Amend of related account doesn't match bill account." msgid "Amend of related account doesn't match bill account."
msgstr "" msgstr ""
#: models.py:198 #: models.py:204
#, fuzzy #, fuzzy
#| msgid "Selected bills should be in open state" #| msgid "Selected bills should be in open state"
msgid "Related invoice is in open state." msgid "Related invoice is in open state."
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: models.py:200 #: models.py:206
msgid "Related invoice is an amendment." msgid "Related invoice is an amendment."
msgstr "" msgstr ""
#: models.py:392 #: models.py:419
msgid "bill" msgid "bill"
msgstr "factura" msgstr "factura"
#: models.py:393 models.py:458 templates/bills/microspective.html:73 #: models.py:420 models.py:499 templates/bills/microspective.html:75
msgid "description" msgid "description"
msgstr "descripción" msgstr "descripción"
#: models.py:394 #: models.py:421
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:395 #: models.py:422
msgid "quantity" msgid "quantity"
msgstr "cantidad" msgstr "cantidad"
#: models.py:397 #: models.py:424
msgid "Verbose quantity" msgid "Verbose quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: models.py:398 templates/admin/bills/bill/report.html:47 #: models.py:425 templates/admin/bills/bill/report.html:47
#: templates/bills/microspective.html:77 #: templates/bills/microspective.html:79
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
msgid "subtotal" msgid "subtotal"
msgstr "subtotal" msgstr "subtotal"
#: models.py:399 #: models.py:426
msgid "tax" msgid "tax"
msgstr "impuesto" msgstr "impuesto"
#: models.py:400 #: models.py:427
msgid "start" msgid "start"
msgstr "inicio" msgstr "inicio"
#: models.py:401 #: models.py:428
msgid "end" msgid "end"
msgstr "fín" msgstr "fín"
#: models.py:403 #: models.py:431
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "" msgstr ""
#: models.py:404 #: models.py:432
msgid "order billed" msgid "order billed"
msgstr "" msgstr ""
#: models.py:405 #: models.py:433
msgid "order billed until" msgid "order billed until"
msgstr "" msgstr ""
#: models.py:406 #: models.py:434
msgid "created" msgid "created"
msgstr "creado" msgstr "creado"
#: models.py:408 #: models.py:436
msgid "amended line" msgid "amended line"
msgstr "linea rectificativa" msgstr "linea rectificativa"
#: models.py:451 #: models.py:492
msgid "Volume" msgid "Volume"
msgstr "Volumen" msgstr "Volumen"
#: models.py:452 #: models.py:493
msgid "Compensation" msgid "Compensation"
msgstr "Compensación" msgstr "Compensación"
#: models.py:453 #: models.py:494
msgid "Other" msgid "Other"
msgstr "Otro" msgstr "Otro"
#: models.py:457 #: models.py:498
msgid "bill line" msgid "bill line"
msgstr "linea de factura" msgstr "linea de factura"
#: templates/admin/bills/bill/change_list.html:9
msgid "Lines"
msgstr ""
#: templates/admin/bills/bill/change_list.html:15
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/admin/bills/bill/close_send_download_bills.html:57
msgid "Yes, I'm sure"
msgstr ""
#: templates/admin/bills/bill/report.html:42 #: templates/admin/bills/bill/report.html:42
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
@ -523,19 +570,19 @@ msgstr ""
#: templates/admin/bills/bill/report.html:47 #: templates/admin/bills/bill/report.html:47
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/admin/bills/bill/report.html:69 #: templates/admin/bills/bill/report.html:69
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "VAT" msgid "VAT"
msgstr "IVA" msgstr "IVA"
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "taxes" msgid "taxes"
msgstr "impuestos" msgstr "impuestos"
#: templates/admin/bills/bill/report.html:56 #: templates/admin/bills/bill/report.html:56
#: templates/admin/bills/billline/report.html:60 #: templates/admin/bills/billline/report.html:60
#: templates/bills/microspective.html:53 #: templates/bills/microspective.html:54
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
@ -553,8 +600,20 @@ msgstr "Fecha de pago"
msgid "Base" msgid "Base"
msgstr "Base" msgstr "Base"
#: templates/admin/bills/billline/change_list.html:6
msgid "Home"
msgstr ""
#: templates/admin/bills/billline/change_list.html:8
msgid "Bills"
msgstr ""
#: templates/admin/bills/billline/change_list.html:9
msgid "Multiple bills"
msgstr ""
#: templates/admin/bills/billline/report.html:42 #: templates/admin/bills/billline/report.html:42
msgid "Services" msgid "Service"
msgstr "" msgstr ""
#: templates/admin/bills/billline/report.html:43 #: templates/admin/bills/billline/report.html:43
@ -579,62 +638,56 @@ msgstr "cantidad"
msgid "Profit" msgid "Profit"
msgstr "" msgstr ""
#: templates/admin/bills/change_list.html:9 #: templates/bills/microspective-fee.html:115
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/bills/microspective-fee.html:107
msgid "Due date" msgid "Due date"
msgstr "Fecha de pago" msgstr "Fecha de pago"
#: templates/bills/microspective-fee.html:108 #: templates/bills/microspective-fee.html:116
#, python-format #, python-format
msgid "On %(bank_account)s" msgid "On %(bank_account)s"
msgstr "En %(bank_account)s" msgstr "En %(bank_account)s"
#: templates/bills/microspective-fee.html:114 #: templates/bills/microspective-fee.html:122
#, python-format #, python-format
msgid "From %(ini)s to %(end)s" msgid "From %(ini)s to %(end)s"
msgstr "Desde %(ini)s hasta %(end)s" msgstr "Desde %(ini)s hasta %(end)s"
#: templates/bills/microspective-fee.html:121 #: templates/bills/microspective-fee.html:144
msgid "" msgid ""
"\n" "\n"
"<strong>With your membership</strong> you are supporting ...\n" "<strong>With your membership</strong> you are supporting ...\n"
msgstr "" msgstr ""
#: templates/bills/microspective.html:49 #: templates/bills/microspective.html:50
msgid "DUE DATE" msgid "DUE DATE"
msgstr "VENCIMIENTO" msgstr "VENCIMIENTO"
#: templates/bills/microspective.html:57 #: templates/bills/microspective.html:58
#, python-format #, python-format
msgid "%(bill_type)s DATE" msgid "%(bill_type)s DATE"
msgstr "FECHA %(bill_type)s" msgstr "FECHA %(bill_type)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:76
msgid "period" msgid "period"
msgstr "periodo" msgstr "periodo"
#: templates/bills/microspective.html:75 #: templates/bills/microspective.html:77
msgid "hrs/qty" msgid "hrs/qty"
msgstr "hrs/cant" msgstr "hrs/cant"
#: templates/bills/microspective.html:76 #: templates/bills/microspective.html:78
msgid "rate/price" msgid "rate/price"
msgstr "tarifa/precio" msgstr "tarifa/precio"
#: templates/bills/microspective.html:131 #: templates/bills/microspective.html:137
msgid "COMMENTS" msgid "COMMENTS"
msgstr "COMENTARIOS" msgstr "COMENTARIOS"
#: templates/bills/microspective.html:138 #: templates/bills/microspective.html:145
msgid "PAYMENT" msgid "PAYMENT"
msgstr "PAGO" msgstr "PAGO"
#: templates/bills/microspective.html:142 #: templates/bills/microspective.html:149
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -648,11 +701,11 @@ msgstr ""
"Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu " "Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu "
"nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es" "nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es"
#: templates/bills/microspective.html:151 #: templates/bills/microspective.html:160
msgid "QUESTIONS" msgid "QUESTIONS"
msgstr "PREGUNTAS" msgstr "PREGUNTAS"
#: templates/bills/microspective.html:152 #: templates/bills/microspective.html:161
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -668,3 +721,8 @@ msgstr ""
" contacta con nosotros en %(email)s. Te responderemos lo más " " contacta con nosotros en %(email)s. Te responderemos lo más "
"rapidamente posible.\n" "rapidamente posible.\n"
" " " "
#, fuzzy
#~| msgid "closed on"
#~ msgid "No closed amends"
#~ msgstr "cerrada en"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -86,11 +86,13 @@ class Bill(models.Model):
FEE = 'FEE' FEE = 'FEE'
AMENDMENTFEE = 'AMENDMENTFEE' AMENDMENTFEE = 'AMENDMENTFEE'
PROFORMA = 'PROFORMA' PROFORMA = 'PROFORMA'
ABONOINVOICE = 'ABONOINVOICE'
TYPES = ( TYPES = (
(INVOICE, _("Invoice")), (INVOICE, _("Invoice")),
(AMENDMENTINVOICE, _("Amendment invoice")), (AMENDMENTINVOICE, _("Amendment invoice")),
(FEE, _("Fee")), (FEE, _("Fee")),
(AMENDMENTFEE, _("Amendment Fee")), (AMENDMENTFEE, _("Amendment Fee")),
(ABONOINVOICE, _("Abono Invoice")),
(PROFORMA, _("Pro forma")), (PROFORMA, _("Pro forma")),
) )
AMEND_MAP = { AMEND_MAP = {
@ -392,6 +394,11 @@ class AmendmentInvoice(Bill):
proxy = True proxy = True
class AbonoInvoice(Bill):
class Meta:
proxy = True
class Fee(Bill): class Fee(Bill):
class Meta: class Meta:
proxy = True proxy = True

View File

@ -18,6 +18,9 @@ BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = Setting('BILLS_AMENDMENT_INVOICE_NUMBER_
'A' 'A'
) )
BILLS_ABONOINVOICE_NUMBER_PREFIX = Setting('BILLS_ABONOINVOICE_NUMBER_PREFIX',
'AB'
)
BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX', BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX',
'F' 'F'

View File

@ -282,3 +282,17 @@ a:hover {
#questions { #questions {
margin-bottom: 0px; margin-bottom: 0px;
} }
#watermark {
color: #d0d0d0;
font-size: 100pt;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
position: absolute;
width: 100%;
height: 100%;
margin: 0;
z-index: -1;
max-width: 593px;
}

View File

@ -12,6 +12,12 @@
{% block body %} {% block body %}
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
{% if bill.is_open %}
<!-- TODO DANIEL: falta arreglar el css d'aquesta cosa -->
<div id="watermark">
<p>ESBORRANY - DRAFT - BORRADOR</p>
</div>
{% endif %}
{% block header %} {% block header %}
<div id="logo"> <div id="logo">
{% block logo %} {% block logo %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,10 @@ from .filters import HasUserListFilter, HasDatabaseListFilter
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
from .models import Database, DatabaseUser from .models import Database, DatabaseUser
def save_selected(modeladmin, request, queryset):
for selected in queryset:
selected.save()
save_selected.short_description = "Re-save selected objects"
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'display_users', 'account_link') list_display = ('name', 'type', 'display_users', 'account_link')
@ -22,7 +26,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'name', 'type', 'users', 'display_users'), 'fields': ('account_link', 'name', 'type', 'users', 'display_users', 'comments'),
}), }),
) )
add_fieldsets = ( add_fieldsets = (
@ -44,7 +48,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
filter_horizontal = ['users'] filter_horizontal = ['users']
filter_by_account_fields = ('users',) filter_by_account_fields = ('users',)
list_prefetch_related = ('users',) list_prefetch_related = ('users',)
actions = (list_accounts,) actions = (list_accounts, save_selected)
def display_users(self, db): def display_users(self, db):
links = [] links = []
@ -93,7 +97,7 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
readonly_fields = ('account_link', 'display_databases',) readonly_fields = ('account_link', 'display_databases',)
filter_by_account_fields = ('databases',) filter_by_account_fields = ('databases',)
list_prefetch_related = ('databases',) list_prefetch_related = ('databases',)
actions = (list_accounts,) actions = (list_accounts, save_selected)
def display_databases(self, user): def display_databases(self, user):
links = [] links = []

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0002_auto_20170528_2005'),
]
operations = [
migrations.AddField(
model_name='database',
name='comments',
field=models.TextField(default=''),
),
]

View File

@ -22,6 +22,7 @@ class Database(models.Model):
default=settings.DATABASES_DEFAULT_TYPE) default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='databases') related_name='databases')
comments = models.TextField(default="", blank=True)
class Meta: class Meta:
unique_together = ('name', 'type') unique_together = ('name', 'type')

View File

@ -20,7 +20,7 @@ DATABASES_DEFAULT_TYPE = Setting('DATABASES_DEFAULT_TYPE',
DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST', DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
'localhost', 'localhost',
validators=[validate_hostname], # validators=[validate_hostname],
) )

View File

@ -55,7 +55,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
'structured_name', 'display_is_top', 'display_websites', 'display_addresses', 'account_link' 'structured_name', 'display_is_top', 'display_websites', 'display_addresses', 'account_link'
) )
add_fields = ('name', 'account') add_fields = ('name', 'account')
fields = ('name', 'account_link', 'display_websites', 'display_addresses') fields = ('name', 'account_link', 'display_websites', 'display_addresses', 'dns2136_address_match_list')
readonly_fields = ( readonly_fields = (
'account_link', 'top_link', 'display_websites', 'display_addresses', 'implicit_records' 'account_link', 'top_link', 'display_websites', 'display_addresses', 'implicit_records'
) )

View File

@ -102,7 +102,7 @@ class Bind9MasterDomainController(ServiceController):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
# Apply changes # Apply changes
if [[ $UPDATED == 1 ]]; then if [[ $UPDATED == 1 ]]; then
service bind9 reload rm /etc/bind/master/*jnl || true; service bind9 restart
fi""") fi""")
) )
@ -158,6 +158,7 @@ class Bind9MasterDomainController(ServiceController):
'slaves': '; '.join(slaves) or 'none', 'slaves': '; '.join(slaves) or 'none',
'also_notify': '; '.join(slaves) + ';' if slaves else '', 'also_notify': '; '.join(slaves) + ';' if slaves else '',
'conf_path': self.CONF_PATH, 'conf_path': self.CONF_PATH,
'dns2136_address_match_list': domain.dns2136_address_match_list
} }
context['conf'] = textwrap.dedent("""\ context['conf'] = textwrap.dedent("""\
zone "%(name)s" { zone "%(name)s" {
@ -166,6 +167,7 @@ class Bind9MasterDomainController(ServiceController):
file "%(zone_path)s"; file "%(zone_path)s";
allow-transfer { %(slaves)s; }; allow-transfer { %(slaves)s; };
also-notify { %(also_notify)s }; also-notify { %(also_notify)s };
allow-update { %(dns2136_address_match_list)s };
notify yes; notify yes;
};""") % context };""") % context
return context return context

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
dependencies = [
('domains', '0005_auto_20160219_1034'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='min_ttl',
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>30m</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
),
migrations.AlterField(
model_name='record',
name='ttl',
field=models.CharField(blank=True, help_text='Record TTL, defaults to 30m', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
),
migrations.AlterField(
model_name='record',
name='type',
field=models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-08-05 09:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0006_auto_20170528_2011'),
]
operations = [
migrations.AlterField(
model_name='record',
name='value',
field=models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-09-20 07:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0007_auto_20190805_1134'),
]
operations = [
migrations.AddField(
model_name='domain',
name='dns2136_address_match_list',
field=models.CharField(blank=True, default='none;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:17
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0008_domain_dns2136_address_match_list'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='dns2136_address_match_list',
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
),
]

View File

@ -65,6 +65,10 @@ class Domain(models.Model):
"zone file. This value is supplied in query responses to inform other " "zone file. This value is supplied in query responses to inform other "
"servers how long they should keep the data in cache. " "servers how long they should keep the data in cache. "
"The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL) "The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL)
dns2136_address_match_list = models.CharField(max_length=80, default=settings.DOMAINS_DEFAULT_DNS2136,
blank=True,
help_text="A bind-9 'address_match_list' that will be granted permission to perform "
"dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.")
objects = DomainQuerySet.as_manager() objects = DomainQuerySet.as_manager()
@ -319,7 +323,8 @@ class Record(models.Model):
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval]) validators=[validators.validate_zone_interval])
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES) type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
value = models.CharField(_("value"), max_length=256, # max_length bumped from 256 to 1024 (arbitrary) on August 2019.
value = models.CharField(_("value"), max_length=1024,
help_text=_("MX, NS and CNAME records sould end with a dot.")) help_text=_("MX, NS and CNAME records sould end with a dot."))
def __str__(self): def __str__(self):

View File

@ -122,3 +122,6 @@ DOMAINS_MASTERS = Setting('DOMAINS_MASTERS',
validators=[lambda masters: list(map(validate_ip_address, masters))], validators=[lambda masters: list(map(validate_ip_address, masters))],
help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()." help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()."
) )
#TODO remove pangea-specific default
DOMAINS_DEFAULT_DNS2136 = "key pangea.key;"

View File

@ -60,7 +60,7 @@ def validate_zone_label(value):
if not value.endswith('.'): if not value.endswith('.'):
msg = _("Use a fully expanded domain name ending with a dot.") msg = _("Use a fully expanded domain name ending with a dot.")
raise ValidationError(msg) raise ValidationError(msg)
if len(value) > 63: if len(value) > 254:
raise ValidationError(_("Labels must be 63 characters or less.")) raise ValidationError(_("Labels must be 63 characters or less."))

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('issues', '0003_auto_20160320_1127'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
),
migrations.AlterField(
model_name='ticket',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
),
migrations.AlterField(
model_name='ticket',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
),
]

View File

@ -10,7 +10,7 @@ from .serializers import ListSerializer
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet): class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
queryset = List.objects.all() queryset = List.objects.all()
serializer_class = ListSerializer serializer_class = ListSerializer
filter_fields = ('name',) filter_fields = ('name', 'address_domain')
router.register(r'lists', ListViewSet) router.register(r'lists', ListViewSet)

View File

@ -48,20 +48,14 @@ class MailmanVirtualDomainController(ServiceController):
def save(self, mail_list): def save(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.include_virtual_alias_domain(context) #self.include_virtual_alias_domain(context)
def delete(self, mail_list): def delete(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.exclude_virtual_alias_domain(context) #self.exclude_virtual_alias_domain(context)
def commit(self): def commit(self):
context = self.get_context_files() context = self.get_context_files()
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
service postfix reload
fi""") % context
)
super(MailmanVirtualDomainController, self).commit() super(MailmanVirtualDomainController, self).commit()
def get_context_files(self): def get_context_files(self):
@ -107,7 +101,7 @@ class MailmanController(MailmanVirtualDomainController):
for suffix in self.address_suffixes: for suffix in self.address_suffixes:
context['suffix'] = suffix context['suffix'] = suffix
# Because mailman doesn't properly handle lists aliases we need two virtual aliases # Because mailman doesn't properly handle lists aliases we need two virtual aliases
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context) aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s@grups.pangea.org" % context)
if context['address_name'] != context['name']: if context['address_name'] != context['name']:
# And another with the original list name; Mailman generates links with it # And another with the original list name; Mailman generates links with it
aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context) aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
@ -116,84 +110,21 @@ class MailmanController(MailmanVirtualDomainController):
def save(self, mail_list): def save(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
# Create list # Create list
self.append(textwrap.dedent(""" cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/save.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
# Create list %(name)s if not mail_list.active:
[[ ! -e '%(mailman_root)s/lists/%(name)s' ]] && { cmd += ' --inactive'
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s' self.append(cmd)
}""") % context)
# Custom domain
if mail_list.address:
context.update({
'aliases': self.get_virtual_aliases(context),
'num_entries': 2 if context['address_name'] != context['name'] else 1,
})
self.append(textwrap.dedent("""\
# Create list alias for custom domain
aliases='%(aliases)s'
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
echo "${aliases}" >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1
else
existing=$({ grep -E '^\s*(%(address_name)s|%(name)s)@%(address_domain)s\s\s*%(name)s\s*$' %(virtual_alias)s || test $? -lt 2; }|wc -l)
if [[ $existing -ne %(num_entries)s ]]; then
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
echo "${aliases}" >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1
fi
fi
echo "require_explicit_destination = 0" | \\
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s
echo "host_name = '%(address_domain)s'" | \\
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s""") % context
)
else:
self.append(textwrap.dedent("""\
# Cleanup possible ex-custom domain
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
fi""") % context
)
# Update
if context['password'] is not None:
self.append(textwrap.dedent("""\
# Re-set password
%(mailman_root)s/bin/change_pw --listname="%(name)s" --password="%(password)s"\
""") % context
)
self.include_virtual_alias_domain(context)
if mail_list.active:
self.append('chmod 775 %(mailman_root)s/lists/%(name)s' % context)
else:
self.append('chmod 000 %(mailman_root)s/lists/%(name)s' % context)
def delete(self, mail_list): def delete(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.exclude_virtual_alias_domain(context) # Delete list
self.append(textwrap.dedent(""" cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/delete.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
# Remove list %(name)s if not mail_list.active:
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\ cmd += ' --inactive'
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s self.append(cmd)
# Non-existent list archives produce exit code 1
exit_code=0
rmlist -a %(name)s || exit_code=$?
if [[ $exit_code != 0 && $exit_code != 1 ]]; then
exit $exit_code
fi""") % context
)
def commit(self): def commit(self):
context = self.get_context_files() pass
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS == 1 ]]; then
postmap %(virtual_alias)s
fi
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
service postfix reload
fi
exit $exit_code""") % context
)
def get_context_files(self): def get_context_files(self):
return { return {

View File

@ -31,7 +31,7 @@ class ListSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
class Meta: class Meta:
model = List model = List
fields = ('url', 'id', 'name', 'password', 'address_name', 'address_domain', 'admin_email') fields = ('url', 'id', 'name', 'password', 'address_name', 'address_domain', 'admin_email', 'is_active',)
postonly_fields = ('name', 'password') postonly_fields = ('name', 'password')
def validate_address_domain(self, address_name): def validate_address_domain(self, address_name):

View File

@ -605,3 +605,11 @@ class PostfixMailscannerTraffic(ServiceMonitor):
'last_date': self.get_last_date(mailbox.pk).strftime("%Y-%m-%d %H:%M:%S %Z"), 'last_date': self.get_last_date(mailbox.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
} }
return context return context
class RoundcubeIdentityController(ServiceController):
"""
WARNING: not implemented
"""
verbose_name = _("Roundcube Identity Controller")
model = 'mailboxes.Mailbox'

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import orchestra.contrib.mailboxes.validators
class Migration(migrations.Migration):
dependencies = [
('mailboxes', '0002_auto_20160219_1032'),
]
operations = [
migrations.AlterField(
model_name='mailbox',
name='custom_filtering',
field=models.TextField(blank=True, help_text="Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. This overrides any automatic junk email filtering", validators=[orchestra.contrib.mailboxes.validators.validate_sieve], verbose_name='filtering'),
),
migrations.AlterField(
model_name='mailbox',
name='name',
field=models.CharField(db_index=True, help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid mailbox name.')], verbose_name='name'),
),
]

View File

@ -1,3 +1,4 @@
import logging
import textwrap import textwrap
from functools import partial from functools import partial
@ -9,6 +10,7 @@ from orchestra import plugins
from . import methods from . import methods
logger = logging.getLogger(__name__)
def replace(context, pattern, repl): def replace(context, pattern, repl):
""" applies replace to all context str values """ """ applies replace to all context str values """
@ -108,7 +110,10 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
def get_related(cls, obj): def get_related(cls, obj):
opts = obj._meta opts = obj._meta
model = '%s.%s' % (opts.app_label, opts.object_name) model = '%s.%s' % (opts.app_label, opts.object_name)
logger.debug('Model: {}'.format(model))
for rel_model, field in cls.related_models: for rel_model, field in cls.related_models:
logger.debug('rel_model: {}'.format(rel_model))
logger.debug('field: {}'.format(field))
if rel_model == model: if rel_model == model:
related = obj related = obj
for attribute in field.split('__'): for attribute in field.split('__'):

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('orchestration', '0006_auto_20160219_1110'),
]
operations = [
migrations.AlterField(
model_name='route',
name='backend',
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailScannerSpamRuleController', '[S] MailScanner ruleset'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PangeaProxmoxOVZ', '[S] PangeaProxmoxOVZ'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('PostfixRecipientAccessController', '[S] Postfix recipient access'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
),
migrations.AlterField(
model_name='route',
name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='routes', to='orchestration.Server', verbose_name='host'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-08-05 09:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orchestration', '0007_auto_20170528_2011'),
]
operations = [
migrations.AlterField(
model_name='route',
name='backend',
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailScannerSpamRuleController', '[S] MailScanner ruleset'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PangeaProxmoxOVZ', '[S] PangeaProxmoxOVZ'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('PostfixRecipientAccessController', '[S] Postfix recipient access'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('RoundcubeIdentityController', '[S] Roundcube Identity Controller'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
),
]

View File

@ -193,6 +193,7 @@ def report(modeladmin, request, queryset):
transactions = Transaction.objects.filter(id__in=transactions) transactions = Transaction.objects.filter(id__in=transactions)
states = {} states = {}
total = 0 total = 0
transactions = transactions.order_by('bill__number')
for transaction in transactions: for transaction in transactions:
state = transaction.get_state_display() state = transaction.get_state_display()
try: try:

View File

@ -215,7 +215,9 @@ class SEPADirectDebit(PaymentMethod):
), ),
E.DrctDbtTx( # Direct Debit Transaction E.DrctDbtTx( # Direct Debit Transaction
E.MndtRltdInf( # Mandate Related Info E.MndtRltdInf( # Mandate Related Info
E.MndtId(str(account.id)), # Mandate Id # + 10000 xk vam canviar de sistema per generar aquestes IDs i volem evitar colisions amb els
# numeros usats antigament
E.MndtId(str(transaction.source_id+10000)), # Mandate Id
E.DtOfSgntr( # Date of Signature E.DtOfSgntr( # Date of Signature
account.date_joined.strftime("%Y-%m-%d") account.date_joined.strftime("%Y-%m-%d")
) )

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payments', '0002_auto_20150709_1018'),
]
operations = [
migrations.AlterField(
model_name='transaction',
name='source',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='payments.PaymentSource', verbose_name='source'),
),
]

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import orchestra.models.fields
class Migration(migrations.Migration):
dependencies = [
('resources', '0010_auto_20160219_1108'),
]
operations = [
migrations.AlterField(
model_name='monitordata',
name='monitor',
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic')], db_index=True, max_length=256, verbose_name='monitor'),
),
migrations.AlterField(
model_name='resource',
name='crontab',
field=models.ForeignKey(blank=True, help_text='Crontab for periodic execution. Leave it empty to disable periodic monitoring', null=True, on_delete=django.db.models.deletion.SET_NULL, to='djcelery.CrontabSchedule', verbose_name='crontab'),
),
migrations.AlterField(
model_name='resource',
name='monitors',
field=orchestra.models.fields.MultiSelectField(blank=True, choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic')], help_text='Monitor backends used for monitoring this resource.', max_length=256, verbose_name='monitors'),
),
]

View File

@ -0,0 +1,175 @@
import re
import sys
import textwrap
import time
import xml.etree.ElementTree as ET
from urllib.parse import urlparse
import requests
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController
from orchestra.contrib.resources import ServiceMonitor
from . import ApacheTrafficByName
from .. import settings
class NextCloudAPIMixin(object):
def validate_response(self, response):
request = response.request
context = (request.method, response.url, request.body, response.status_code)
sys.stderr.write("%s %s '%s' HTTP %s\n" % context)
if response.status_code != requests.codes.ok:
raise RuntimeError("%s %s '%s' HTTP %s" % context)
root = ET.fromstring(response.text)
statuscode = root.find("./meta/statuscode").text
if statuscode != '100':
message = root.find("./meta/status").text
request = response.request
context = (request.method, response.url, request.body, statuscode, message)
raise RuntimeError("%s %s '%s' ERROR %s, %s" % context)
def api_call(self, action, url_path, *args, **kwargs):
BASE_URL = settings.SAAS_NEXTCLOUD_API_URL.rstrip('/')
url = '/'.join((BASE_URL, url_path))
response = action(url, headers={'OCS-APIRequest':'true'}, *args, **kwargs)
self.validate_response(response)
return response
def api_get(self, url_path, *args, **kwargs):
return self.api_call(requests.get, url_path, *args, **kwargs)
def api_post(self, url_path, *args, **kwargs):
return self.api_call(requests.post, url_path, *args, **kwargs)
def api_put(self, url_path, *args, **kwargs):
return self.api_call(requests.put, url_path, *args, **kwargs)
def api_delete(self, url_path, *args, **kwargs):
return self.api_call(requests.delete, url_path, *args, **kwargs)
def create(self, saas):
data = {
'userid': saas.name,
'password': saas.password
}
self.api_post('users', data)
def update(self, saas):
"""
key: email|quota|display|password
value: el valor a modificar.
Si es un email, tornarà un error si la direcció no te la "@"
Si es una quota, sembla que algo per l'estil "5G", "100M", etc. funciona. Quota 0 = infinit
"display" es el display name, no crec que el fem servir, és cosmetic
"""
data = {
'key': 'password',
'value': saas.password,
}
self.api_put('users/%s' % saas.name, data)
def get_user(self, saas):
"""
{
'displayname'
'email'
'quota' =>
{
'free' (en Bytes)
'relative' (en tant per cent sense signe %, e.g. 68.17)
'total' (en Bytes)
'used' (en Bytes)
}
}
"""
response = self.api_get('users/%s' % saas.name)
root = ET.fromstring(response.text)
ret = {}
for data in root.find('./data'):
ret[data.tag] = data.text
ret['quota'] = {}
for data in root.find('.data/quota'):
ret['quota'][data.tag] = data.text
return ret
class NextCloudController(NextCloudAPIMixin, ServiceController):
"""
Creates a wordpress site on a WordPress MultiSite installation.
You should point it to the database server
"""
verbose_name = _("nextCloud SaaS")
model = 'saas.SaaS'
default_route_match = "saas.service == 'nextcloud'"
doc_settings = (settings,
('SAAS_NEXTCLOUD_API_URL',)
)
def update_or_create(self, saas, server):
try:
self.api_get('users/%s' % saas.name)
except RuntimeError:
if getattr(saas, 'password'):
self.create(saas)
else:
raise
else:
if getattr(saas, 'password'):
self.update(saas)
def remove(self, saas, server):
self.api_delete('users/%s' % saas.name)
def save(self, saas):
# TODO disable user https://github.com/owncloud/core/issues/12601
self.append(self.update_or_create, saas)
def delete(self, saas):
self.append(self.remove, saas)
class NextcloudTraffic(ApacheTrafficByName):
__doc__ = ApacheTrafficByName.__doc__
verbose_name = _("nextCloud SaaS Traffic")
default_route_match = "saas.service == 'nextcloud'"
doc_settings = (settings,
('SAAS_TRAFFIC_IGNORE_HOSTS', 'SAAS_NEXTCLOUD_LOG_PATH')
)
log_path = settings.SAAS_NEXTCLOUD_LOG_PATH
class NextCloudDiskQuota(NextCloudAPIMixin, ServiceMonitor):
model = 'saas.SaaS'
verbose_name = _("nextCloud SaaS Disk Quota")
default_route_match = "saas.service == 'nextcloud'"
resource = ServiceMonitor.DISK
delete_old_equal_values = True
def monitor(self, user):
context = self.get_context(user)
self.append("echo %(object_id)s $(monitor %(base_home)s)" % context)
def get_context(self, user):
context = {
'object_id': user.pk,
'base_home': user.get_base_home(),
}
return replace(context, "'", '"')
def get_quota(self, saas, server):
try:
user = self.get_user(saas)
except requests.exceptions.ConnectionError:
time.sleep(2)
user = self.get_user(saas)
context = {
'object_id': saas.pk,
'used': int(user['quota'].get('used', 0)),
}
sys.stdout.write('%(object_id)i %(used)i\n' % context)
def monitor(self, saas):
self.append(self.get_quota, saas)

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import orchestra.core.validators
class Migration(migrations.Migration):
dependencies = [
('saas', '0002_auto_20151001_0923'),
]
operations = [
migrations.AlterField(
model_name='saas',
name='custom_url',
field=models.URLField(blank=True, help_text='Optional and alternative URL for accessing this service instance. i.e. <tt>https://wiki.mydomain/doku/</tt><br>A related website will be automatically configured if needed.', verbose_name='custom URL'),
),
migrations.AlterField(
model_name='saas',
name='name',
field=models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./- only.', max_length=64, validators=[orchestra.core.validators.validate_hostname], verbose_name='Name'),
),
migrations.AlterField(
model_name='saas',
name='service',
field=models.CharField(choices=[('bscw', 'BSCW'), ('dokuwiki', 'Dowkuwiki'), ('drupal', 'Drupal'), ('gitlab', 'GitLab'), ('moodle', 'Moodle'), ('wordpress', 'WordPress'), ('nextcloud', 'nextCloud'), ('owncloud', 'ownCloud'), ('phplist', 'phpList')], max_length=32, verbose_name='service'),
),
]

View File

@ -70,7 +70,7 @@ class SaaS(models.Model):
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = True
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def clean(self): def clean(self):

View File

@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.websites.models import Website, WebsiteDirective, Content from orchestra.contrib.websites.models import Website, WebsiteDirective, Content
from orchestra.contrib.websites.validators import validate_domain_protocol from orchestra.contrib.websites.validators import validate_domain_protocol
from orchestra.contrib.orchestration.models import Server
from orchestra.utils.python import AttrDict from orchestra.utils.python import AttrDict
@ -54,7 +55,9 @@ def clean_custom_url(saas):
(url.netloc, account, domain.account), (url.netloc, account, domain.account),
}) })
# Create new website for custom_url # Create new website for custom_url
website = Website(name=url.netloc, protocol=protocol, account=account) # Changed by daniel: hardcode target_server to web.pangea.lan, consider putting it into settings.py
tgt_server = Server.objects.get(name='web.pangea.lan')
website = Website(name=url.netloc, protocol=protocol, account=account, target_server=tgt_server)
full_clean(website) full_clean(website)
try: try:
validate_domain_protocol(website, domain, protocol) validate_domain_protocol(website, domain, protocol)
@ -110,7 +113,8 @@ def create_or_update_directive(saas):
Domain = Website.domains.field.rel.to Domain = Website.domains.field.rel.to
domain = Domain.objects.get(name=url.netloc) domain = Domain.objects.get(name=url.netloc)
# Create new website for custom_url # Create new website for custom_url
website = Website(name=url.netloc, protocol=protocol, account=account) tgt_server = Server.objects.get(name='web.pangea.lan')
website = Website(name=url.netloc, protocol=protocol, account=account, target_server=tgt_server)
website.save() website.save()
website.domains.add(domain) website.domains.add(domain)
# get or create directive # get or create directive

View File

@ -0,0 +1,13 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from .. import settings
from .options import SoftwareService
class NextCloudService(SoftwareService):
name = 'nextcloud'
verbose_name = "nextCloud"
icon = 'orchestra/icons/apps/nextCloud.png'
site_domain = settings.SAAS_NEXTCLOUD_DOMAIN

View File

@ -18,6 +18,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES',
'orchestra.contrib.saas.services.dokuwiki.DokuWikiService', 'orchestra.contrib.saas.services.dokuwiki.DokuWikiService',
'orchestra.contrib.saas.services.drupal.DrupalService', 'orchestra.contrib.saas.services.drupal.DrupalService',
'orchestra.contrib.saas.services.owncloud.OwnCloudService', 'orchestra.contrib.saas.services.owncloud.OwnCloudService',
'orchestra.contrib.saas.services.nextcloud.NextCloudService',
# 'orchestra.contrib.saas.services.seafile.SeaFileService', # 'orchestra.contrib.saas.services.seafile.SeaFileService',
), ),
# lazy loading # lazy loading
@ -235,6 +236,23 @@ SAAS_OWNCLOUD_LOG_PATH = Setting('SAAS_OWNCLOUD_LOG_PATH',
) )
# nextCloud
SAAS_NEXTCLOUD_DOMAIN = Setting('SAAS_NEXTCLOUD_DOMAIN',
'nextcloud.{}'.format(ORCHESTRA_BASE_DOMAIN),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
)
SAAS_NEXTCLOUD_API_URL = Setting('SAAS_NEXTCLOUD_API_URL',
'https://admin:secret@nextcloud.{}/ocs/v1.php/cloud'.format(ORCHESTRA_BASE_DOMAIN),
)
SAAS_NEXTCLOUD_LOG_PATH = Setting('SAAS_NEXTCLOUD_LOG_PATH',
'',
help_text=_('Filesystem path for the webserver access logs.<br>'
'<tt>LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Host}i\"" host</tt>'),
)
# BSCW # BSCW
SAAS_BSCW_DOMAIN = Setting('SAAS_BSCW_DOMAIN', SAAS_BSCW_DOMAIN = Setting('SAAS_BSCW_DOMAIN',

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0005_auto_20160427_1531'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0006_auto_20170528_2005'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0007_auto_20170528_2011'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0008_auto_20170625_1813'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0009_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0010_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0011_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-08-05 09:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0012_auto_20170625_1841'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0013_auto_20190805_1134'),
]
operations = [
migrations.AlterField(
model_name='service',
name='rate_algorithm',
field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.<br>&nbsp;&nbsp;Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).<br>&nbsp;&nbsp;Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.<br>&nbsp;&nbsp;Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'),
),
]

View File

@ -28,6 +28,14 @@ class UNIXUserController(ServiceController):
context = self.get_context(user) context = self.get_context(user)
if not context['user']: if not context['user']:
return return
if not user.active:
self.append(textwrap.dedent("""
#Just disable that user, if it exists
if id %(user)s ; then
usermod %(user)s --password '%(password)s'
fi
""") % context)
return
# TODO userd add will fail if %(user)s group already exists # TODO userd add will fail if %(user)s group already exists
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
# Update/create user state for %(user)s # Update/create user state for %(user)s
@ -61,7 +69,8 @@ class UNIXUserController(ServiceController):
if context['home'] != context['base_home']: if context['home'] != context['base_home']:
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
# Set extra permissions: %(user)s home is inside %(mainuser)s home # Set extra permissions: %(user)s home is inside %(mainuser)s home
if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then if true; then
# if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then
# Account group as the owner # Account group as the owner
chown %(mainuser)s:%(mainuser)s '%(home)s' chown %(mainuser)s:%(mainuser)s '%(home)s'
chmod g+s '%(home)s' chmod g+s '%(home)s'

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import orchestra.core.validators
class Migration(migrations.Migration):
dependencies = [
('systemusers', '0002_auto_20150429_1413'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='shell',
field=models.CharField(choices=[('/dev/null', 'No shell, FTP only'), ('/bin/rssh', 'No shell, SFTP/RSYNC only'), ('/usr/bin/git-shell', 'No shell, GIT only'), ('/bin/bash', '/bin/bash')], default='/dev/null', max_length=32, verbose_name='shell'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[orchestra.core.validators.validate_username], verbose_name='username'),
),
]

View File

@ -88,7 +88,7 @@ class SystemUser(models.Model):
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = True
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def get_description(self): def get_description(self):

View File

@ -133,3 +133,22 @@ class ProxmoxOpenVZTraffic(ServiceMonitor):
'object_id': vps.id, 'object_id': vps.id,
'hostname': vps.hostname, 'hostname': vps.hostname,
} }
class LxcController(ServiceController):
model = 'vps.VPS'
RESOURCES = (
('memory', 'mem'),
('disk', 'disk'),
('vcpu', 'vcpu')
)
def prepare(self):
super(LxcController, self).prepare()
def save(self, vps):
# TODO create the container
pass

View File

@ -0,0 +1,135 @@
import decimal
import textwrap
from orchestra.contrib.orchestration import ServiceController
from orchestra.contrib.resources import ServiceMonitor
from . import settings
class ProxmoxOVZ(ServiceController):
model = 'vps.VPS'
RESOURCES = (
('memory', 'mem'),
('swap', 'swap'),
('disk', 'disk')
)
GET_PROXMOX_INFO = textwrap.dedent("""
function get_vz_info () {
hostname=$1
version=$(pveversion | cut -d '/' -f2 | cut -d'.' -f1)
if [[ $version -lt 2 ]]; then
conf=$(grep "CID\\|:$hostname:" /var/lib/pve-manager/vzlist | grep -B1 ":$hostname:")
CID=$(echo "$conf" | head -n1 | cut -d':' -f2)
CTID=$(echo "$conf" | tail -n1 | cut -d':' -f1)
node=$(pveca -l | grep "^\\s*$CID\\s*:" | awk {'print $3'})
else
conf=$(grep -r "HOSTNAME=\\"$hostname\\"" /etc/pve/nodes/*/openvz/*.conf)
node=$(echo "${conf}" | cut -d"/" -f5)
CTID=$(echo "${conf}" | cut -d"/" -f7 | cut -d"\\." -f1)
fi
echo $CTID $node
}""")
def prepare(self):
super(ProxmoxOVZ, self).prepare()
self.append(self.GET_PROXMOX_INFO)
def get_vzset_args(self, context):
args = list(settings.VPS_DEFAULT_VZSET_ARGS)
for resource, arg_name in self.RESOURCES:
try:
allocation = context[resource]
except KeyError:
pass
else:
args.append('--%s %i' % (arg_name, allocation))
return ' '.join(args)
def run_ssh_commands(self, ssh_commands):
commands = '\n '.join(ssh_commands)
self.append(textwrap.dedent("""\
cat << EOF | ssh root@${info[1]}
%s
EOF""") % commands
)
def save(self, vps):
# TODO create the container
context = self.get_context(vps)
self.append(textwrap.dedent("""
info=( $(get_vz_info %(hostname)s) )
echo "Managing ${info[@]}"\
""") % context
)
ssh_commands = []
vzset_args = self.get_vzset_args(context)
if vzset_args:
context['vzset_args'] = vzset_args
ssh_commands.append("pvectl vzset ${info[0]} %(vzset_args)s" % context)
if hasattr(vps, 'password'):
context['password'] = vps.password.replace('$', '\\$')
ssh_commands.append(textwrap.dedent("""\
echo 'root:%(password)s' \\
| chroot /var/lib/vz/private/${info[0]} chpasswd -e""") % context
)
self.run_ssh_commands(ssh_commands)
def get_context(self, vps):
context = {
'hostname': vps.hostname,
}
for resource, __ in self.RESOURCES:
try:
allocation = getattr(vps.resources, resource).allocated
except AttributeError:
pass
else:
context[resource] = allocation
return context
class ProxmoxOpenVZTraffic(ServiceMonitor):
model = 'vps.VPS'
resource = ServiceMonitor.TRAFFIC
monthly_sum_old_values = True
GET_PROXMOX_INFO = ProxmoxOVZ.GET_PROXMOX_INFO
def prepare(self):
super(ProxmoxOpenVZTraffic, self).prepare()
self.append(self.GET_PROXMOX_INFO)
self.append(textwrap.dedent("""
function monitor () {
object_id=$1
hostname=$2
info=( $(get_vz_info $hostname) )
cat << EOF | ssh root@${info[1]}
vzctl exec ${info[0]} cat /proc/net/dev \\
| grep venet0 \\
| tr ':' ' ' \\
| awk '{sum=\\$2+\\$10} END {printf ("$object_id %0.0f\\n", sum)}'
EOF
}
""")
)
def process(self, line):
""" diff with last stored state """
object_id, value, state = super(ProxmoxOpenVZTraffic, self).process(line)
value = decimal.Decimal(value)
last = self.get_last_data(object_id)
if not last or last.state > value:
return object_id, value, value
return object_id, value-last.state, value
def monitor(self, vps):
""" Get OpenVZ container traffic on a Proxmox cluster """
context = self.get_context(vps)
self.append('monitor %(object_id)s %(hostname)s' % context)
def get_context(self, vps):
return {
'object_id': vps.id,
'hostname': vps.hostname,
}

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vps', '0003_vps_is_active'),
]
operations = [
migrations.AlterField(
model_name='vps',
name='template',
field=models.CharField(choices=[('debian7', 'Debian 7 - Wheezy'), ('placeholder', 'LXC placeholder')], default='placeholder', help_text='Initial template.', max_length=64, verbose_name='template'),
),
migrations.AlterField(
model_name='vps',
name='type',
field=models.CharField(choices=[('openvz', 'OpenVZ container'), ('lxc', 'LXC container')], default='lxc', max_length=64, verbose_name='type'),
),
]

View File

@ -4,13 +4,14 @@ from orchestra.contrib.settings import Setting
VPS_TYPES = Setting('VPS_TYPES', VPS_TYPES = Setting('VPS_TYPES',
( (
('openvz', 'OpenVZ container'), ('openvz', 'OpenVZ container'),
('lxc', 'LXC container')
), ),
validators=[Setting.validate_choices] validators=[Setting.validate_choices]
) )
VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE', VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE',
'openvz', 'lxc',
choices=VPS_TYPES choices=VPS_TYPES
) )
@ -18,13 +19,14 @@ VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE',
VPS_TEMPLATES = Setting('VPS_TEMPLATES', VPS_TEMPLATES = Setting('VPS_TEMPLATES',
( (
('debian7', 'Debian 7 - Wheezy'), ('debian7', 'Debian 7 - Wheezy'),
('placeholder', 'LXC placeholder')
), ),
validators=[Setting.validate_choices] validators=[Setting.validate_choices]
) )
VPS_DEFAULT_TEMPLATE = Setting('VPS_DEFAULT_TEMPLATE', VPS_DEFAULT_TEMPLATE = Setting('VPS_DEFAULT_TEMPLATE',
'debian7', 'placeholder',
choices=VPS_TEMPLATES choices=VPS_TEMPLATES
) )

View File

@ -32,6 +32,7 @@ class PHPController(WebAppServiceMixin, ServiceController):
)) ))
def save(self, webapp): def save(self, webapp):
self.delete_old_config(webapp)
context = self.get_context(webapp) context = self.get_context(webapp)
self.create_webapp_dir(context) self.create_webapp_dir(context)
if webapp.type_instance.is_fpm: if webapp.type_instance.is_fpm:
@ -40,11 +41,32 @@ class PHPController(WebAppServiceMixin, ServiceController):
self.save_fcgid(webapp, context) self.save_fcgid(webapp, context)
else: else:
raise TypeError("Unknown PHP execution type") raise TypeError("Unknown PHP execution type")
self.append("# Clean non-used PHP FCGID wrappers and FPM pools") # LEGACY CLEANUP FUNCTIONS. TODO REMOVE WHEN SURE NOT NEEDED.
self.delete_fcgid(webapp, context, preserve=True) # self.delete_fcgid(webapp, context, preserve=True)
self.delete_fpm(webapp, context, preserve=True) # self.delete_fpm(webapp, context, preserve=True)
self.set_under_construction(context) self.set_under_construction(context)
def delete_config(self,webapp):
context = self.get_context(webapp)
to_delete = []
if webapp.type_instance.is_fpm:
to_delete.append(settings.WEBAPPS_PHPFPM_POOL_PATH % context)
to_delete.append(settings.WEBAPPS_FPM_LISTEN % context)
elif webapp.type_instance.is_fcgid:
to_delete.append(settings.WEBAPPS_FCGID_WRAPPER_PATH % context)
to_delete.append(settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context)
for item in to_delete:
self.append('rm -f "{}"'.format(item))
def delete_old_config(self,webapp):
# Check if we loaded the old version of the webapp. If so, we're updating
# rather than creating, so we must make sure the old config files are removed.
if hasattr(webapp, '_old_self'):
self.append("# Clean old configuration files")
self.delete_config(webapp._old_self)
else:
self.append("# No old config files to delete")
def save_fpm(self, webapp, context): def save_fpm(self, webapp, context):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
# Generate FPM configuration # Generate FPM configuration
@ -99,10 +121,11 @@ class PHPController(WebAppServiceMixin, ServiceController):
def delete(self, webapp): def delete(self, webapp):
context = self.get_context(webapp) context = self.get_context(webapp)
if webapp.type_instance.is_fpm: self.delete_old_config(webapp)
self.delete_fpm(webapp, context) # if webapp.type_instance.is_fpm:
elif webapp.type_instance.is_fcgid: # self.delete_fpm(webapp, context)
self.delete_fcgid(webapp, context) # elif webapp.type_instance.is_fcgid:
# self.delete_fcgid(webapp, context)
self.delete_webapp_dir(context) self.delete_webapp_dir(context)
def has_sibilings(self, webapp, context): def has_sibilings(self, webapp, context):
@ -205,7 +228,7 @@ class PHPController(WebAppServiceMixin, ServiceController):
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
fpm_config = Template(textwrap.dedent("""\ fpm_config = Template(textwrap.dedent("""\
;; {{ banner }} ;; {{ banner }}
[{{ user }}] [{{ user }}-{{app_name}}]
user = {{ user }} user = {{ user }}
group = {{ group }} group = {{ group }}

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapps', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='webapp',
name='type',
field=models.CharField(choices=[('moodle-php', 'Moodle'), ('php', 'PHP'), ('python', 'Python'), ('static', 'Static'), ('symbolic-link', 'Symbolic link'), ('webalizer', 'Webalizer'), ('wordpress-php', 'WordPress')], max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='webappoption',
name='name',
field=models.CharField(choices=[(None, '-------'), ('FileSystem', [('public-root', 'Public root')]), ('Process', [('timeout', 'Process timeout'), ('processes', 'Number of processes')]), ('PHP', [('enable_functions', 'Enable functions'), ('disable_functions', 'Disable functions'), ('allow_url_include', 'Allow URL include'), ('allow_url_fopen', 'Allow URL fopen'), ('auto_append_file', 'Auto append file'), ('auto_prepend_file', 'Auto prepend file'), ('date.timezone', 'date.timezone'), ('default_socket_timeout', 'Default socket timeout'), ('display_errors', 'Display errors'), ('extension', 'Extension'), ('include_path', 'Include path'), ('magic_quotes_gpc', 'Magic quotes GPC'), ('magic_quotes_runtime', 'Magic quotes runtime'), ('magic_quotes_sybase', 'Magic quotes sybase'), ('max_input_time', 'Max input time'), ('max_input_vars', 'Max input vars'), ('memory_limit', 'Memory limit'), ('mysql.connect_timeout', 'Mysql connect timeout'), ('output_buffering', 'Output buffering'), ('register_globals', 'Register globals'), ('post_max_size', 'Post max size'), ('sendmail_path', 'Sendmail path'), ('session.bug_compat_warn', 'Session bug compat warning'), ('session.auto_start', 'Session auto start'), ('safe_mode', 'Safe mode'), ('suhosin.post.max_vars', 'Suhosin POST max vars'), ('suhosin.get.max_vars', 'Suhosin GET max vars'), ('suhosin.request.max_vars', 'Suhosin request max vars'), ('suhosin.session.encrypt', 'Suhosin session encrypt'), ('suhosin.simulation', 'Suhosin simulation'), ('suhosin.executor.include.whitelist', 'Suhosin executor include whitelist'), ('upload_max_filesize', 'Upload max filesize'), ('upload_tmp_dir', 'Upload tmp dir'), ('zend_extension', 'Zend extension')])], max_length=128, verbose_name='name'),
),
]

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-07-04 08:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('orchestration', '0007_auto_20170528_2011'),
('webapps', '0002_auto_20170528_2011'),
]
operations = [
migrations.AddField(
model_name='webapp',
name='target_server',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to='orchestration.Server', verbose_name='Target Server'),
preserve_default=False,
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:17
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapps', '0003_webapp_target_server'),
]
operations = [
migrations.AddField(
model_name='webapp',
name='comments',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapps', '0004_webapp_comments'),
]
operations = [
migrations.AlterField(
model_name='webapp',
name='comments',
field=models.TextField(default=''),
),
]

View File

@ -23,6 +23,9 @@ class WebApp(models.Model):
related_name='webapps') related_name='webapps')
data = JSONField(_("data"), blank=True, default={}, data = JSONField(_("data"), blank=True, default={},
help_text=_("Extra information dependent of each service.")) help_text=_("Extra information dependent of each service."))
target_server = models.ForeignKey('orchestration.Server', verbose_name=_("Target Server"),
related_name='webapps')
comments = models.TextField(default="", blank=True)
# CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them # CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them
databases = VirtualDatabaseRelation('databases.Database') databases = VirtualDatabaseRelation('databases.Database')

View File

@ -102,8 +102,8 @@ class Processes(AppOption):
# FCGID MaxProcesses # FCGID MaxProcesses
# FPM pm.max_children # FPM pm.max_children
verbose_name = _("Number of processes") verbose_name = _("Number of processes")
help_text = _("Maximum number of children that can be alive at the same time (a number between 0 and 9).") help_text = _("Maximum number of children that can be alive at the same time (a number between 0 and 99).")
regex = r'^[0-9]{1,2}$' regex = r'^[0-9]{1,3}$'
group = AppOption.PROCESS group = AppOption.PROCESS

View File

@ -30,7 +30,7 @@ class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
class Meta: class Meta:
model = WebApp model = WebApp
fields = ('url', 'id', 'name', 'type', 'options', 'data') fields = ('url', 'id', 'name', 'type', 'options', 'data',)
postonly_fields = ('name', 'type') postonly_fields = ('name', 'type')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -99,6 +99,7 @@ WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', (
('5.3-cgi', 'PHP 5.3 FCGID'), ('5.3-cgi', 'PHP 5.3 FCGID'),
('5.2-cgi', 'PHP 5.2 FCGID'), ('5.2-cgi', 'PHP 5.2 FCGID'),
('4-cgi', 'PHP 4 FCGID'), ('4-cgi', 'PHP 4 FCGID'),
('7-fpm', 'PHP 7 FPM')
), ),
help_text="Execution modle choose by ending -fpm or -cgi.", help_text="Execution modle choose by ending -fpm or -cgi.",
validators=[Setting.validate_choices] validators=[Setting.validate_choices]

View File

@ -3,19 +3,22 @@ from django.dispatch import receiver
from .models import WebApp from .models import WebApp
# Admin bulk deletion doesn't call model.delete() # Admin bulk deletion doesn't call model.delete()
# So, signals are used instead of model method overriding # So, signals are used instead of model method overriding
@receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') @receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save')
def type_save(sender, *args, **kwargs): def type_save(sender, *args, **kwargs):
instance = kwargs['instance'] instance = kwargs['instance']
# Since a webapp might need to cleanup its old config files, the data
# from the OLD VERSION of the webapp is needed.
if instance.pk:
instance._old_self = type(instance).objects.get(id=instance.pk)
instance.type_instance.save() instance.type_instance.save()
@receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete') @receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete')
def type_delete(sender, *args, **kwargs): def type_delete(sender, *args, **kwargs):
instance = kwargs['instance'] instance = kwargs['instance']
instance._old_self = type(instance).objects.get(id=instance.pk)
try: try:
instance.type_instance.delete() instance.type_instance.delete()
except KeyError: except KeyError:

View File

@ -69,7 +69,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'name', 'protocol', 'domains', 'is_active'), 'fields': ('account_link', 'name', 'protocol', 'target_server', 'domains', 'is_active', 'comments'),
}), }),
) )
form = WebsiteAdminForm form = WebsiteAdminForm

View File

@ -58,7 +58,8 @@ class Apache2Controller(ServiceController):
context.update({ context.update({
'port': self.HTTPS_PORT if ssl else self.HTTP_PORT, 'port': self.HTTPS_PORT if ssl else self.HTTP_PORT,
'vhost_set_fcgid': False, 'vhost_set_fcgid': False,
'server_alias_lines': ' \\\n '.join(context['server_alias']) 'server_alias_lines': ' \\\n '.join(context['server_alias']),
'suexec_needed': site.target_server == 'web.pangea.lan'
}) })
context['extra_conf'] = self.get_extra_conf(site, context, ssl) context['extra_conf'] = self.get_extra_conf(site, context, ssl)
return Template(textwrap.dedent("""\ return Template(textwrap.dedent("""\
@ -71,7 +72,8 @@ class Apache2Controller(ServiceController):
CustomLog {{ access_log }} common{% endif %}\ CustomLog {{ access_log }} common{% endif %}\
{% if error_log %} {% if error_log %}
ErrorLog {{ error_log }}{% endif %} ErrorLog {{ error_log }}{% endif %}
SuexecUserGroup {{ user }} {{ group }}\ {% if suexec_needed %}
SuexecUserGroup {{ user }} {{ group }}{% endif %}\
{% for line in extra_conf.splitlines %} {% for line in extra_conf.splitlines %}
{{ line | safe }}{% endfor %} {{ line | safe }}{% endfor %}
</VirtualHost> </VirtualHost>
@ -225,15 +227,18 @@ class Apache2Controller(ServiceController):
target = 'fcgi://%(socket)s%(app_path)s/$1' target = 'fcgi://%(socket)s%(app_path)s/$1'
else: else:
# UNIX socket # UNIX socket
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/' target = 'unix:%(socket)s|fcgi://127.0.0.1/'
if context['location']:
# FIXME unix sockets do not support $1
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1'
context.update({ context.update({
'app_path': os.path.normpath(app_path), 'app_path': os.path.normpath(app_path),
'socket': socket, 'socket': socket,
}) })
directives = "ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target}\n".format(target=target) % context directives = textwrap.dedent("""
<Directory {app_path}>
<FilesMatch "\.php$">
SetHandler "proxy:unix:{socket}|fcgi://127.0.0.1"
</FilesMatch>
</Directory>
""").format(socket=socket, app_path=app_path)
directives += self.get_location_filesystem_map(context) directives += self.get_location_filesystem_map(context)
return [ return [
(context['location'], directives), (context['location'], directives),
@ -286,7 +291,8 @@ class Apache2Controller(ServiceController):
if not (cert and key): if not (cert and key):
cert = [settings.WEBSITES_DEFAULT_SSL_CERT] cert = [settings.WEBSITES_DEFAULT_SSL_CERT]
key = [settings.WEBSITES_DEFAULT_SSL_KEY] key = [settings.WEBSITES_DEFAULT_SSL_KEY]
ca = [settings.WEBSITES_DEFAULT_SSL_CA] # Disabled because since the migration to LE, CA is not required here
#ca = [settings.WEBSITES_DEFAULT_SSL_CA]
if not (cert and key): if not (cert and key):
return [] return []
ssl_config = [ ssl_config = [

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0002_auto_20160219_1036'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')])], db_index=True, max_length=128, verbose_name='name'),
),
migrations.AlterField(
model_name='websitedirective',
name='value',
field=models.CharField(blank=True, max_length=256, verbose_name='value'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0003_auto_20170528_2011'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0004_auto_20170625_1813'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')])], db_index=True, max_length=128, verbose_name='name'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0005_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0006_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')])], db_index=True, max_length=128, verbose_name='name'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-06-25 16:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('websites', '0007_auto_20170625_1840'),
]
operations = [
migrations.AlterField(
model_name='websitedirective',
name='name',
field=models.CharField(choices=[(None, '-------'), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name'),
),
]

Some files were not shown because too many files have changed in this diff Show More