Added backend docstring as helptext
This commit is contained in:
parent
1a78317028
commit
eebaee1097
3
TODO.md
3
TODO.md
|
@ -286,3 +286,6 @@ https://code.djangoproject.com/ticket/24576
|
||||||
# Amend lines???
|
# Amend lines???
|
||||||
|
|
||||||
# Add icon on select contact view
|
# Add icon on select contact view
|
||||||
|
|
||||||
|
# Determine the difference between data serializer used for validation and used for the rest API!
|
||||||
|
# Make PluginApiView that fills metadata and other stuff like modeladmin plugin support
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.utils import model_meta
|
||||||
|
|
||||||
from ..core.validators import validate_password
|
from ..core.validators import validate_password
|
||||||
|
|
||||||
|
@ -10,33 +13,48 @@ class SetPasswordSerializer(serializers.Serializer):
|
||||||
style={'widget': widgets.PasswordInput}, validators=[validate_password])
|
style={'widget': widgets.PasswordInput}, validators=[validate_password])
|
||||||
|
|
||||||
|
|
||||||
#class HyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
|
|
||||||
# def __init__(self, meta):
|
|
||||||
# super(HyperlinkedModelSerializerOptions, self).__init__(meta)
|
|
||||||
# self.postonly_fields = getattr(meta, 'postonly_fields', ())
|
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
""" support for postonly_fields, fields whose value can only be set on post """
|
""" support for postonly_fields, fields whose value can only be set on post """
|
||||||
# _options_class = HyperlinkedModelSerializerOptions
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
""" calls model.clean() """
|
""" calls model.clean() """
|
||||||
attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
|
attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
|
||||||
instance = self.Meta.model(**attrs)
|
validated_data = dict(attrs)
|
||||||
|
ModelClass = self.Meta.model
|
||||||
|
# Remove many-to-many relationships from validated_data.
|
||||||
|
info = model_meta.get_field_info(ModelClass)
|
||||||
|
for field_name, relation_info in info.relations.items():
|
||||||
|
if relation_info.to_many and (field_name in validated_data):
|
||||||
|
validated_data.pop(field_name)
|
||||||
|
if self.instance:
|
||||||
|
# on update: Merge provided fields with instance field
|
||||||
|
instance = copy.deepcopy(self.instance)
|
||||||
|
for key, value in validated_data.items():
|
||||||
|
setattr(instance, key, value)
|
||||||
|
else:
|
||||||
|
instance = ModelClass(**validated_data)
|
||||||
instance.clean()
|
instance.clean()
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
# TODO raise validationError instead of silently removing fields
|
def post_only_cleanning(self, instance, validated_data):
|
||||||
def update(self, instance, validated_data):
|
""" removes postonly_fields from attrs """
|
||||||
""" removes postonly_fields from attrs when not posting """
|
|
||||||
model_attrs = dict(**validated_data)
|
model_attrs = dict(**validated_data)
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
for attr, value in validated_data.items():
|
for attr, value in validated_data.items():
|
||||||
if attr in self.Meta.postonly_fields:
|
if attr in self.Meta.postonly_fields:
|
||||||
model_attrs.pop(attr)
|
model_attrs.pop(attr)
|
||||||
|
return model_attrs
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
""" removes postonly_fields from attrs when not posting """
|
||||||
|
model_attrs = self.post_only_cleanning(instance, validated_data)
|
||||||
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
|
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
|
||||||
|
|
||||||
|
def partial_update(self, instance, validated_data):
|
||||||
|
""" removes postonly_fields from attrs when not posting """
|
||||||
|
model_attrs = self.post_only_cleanning(instance, validated_data)
|
||||||
|
return super(HyperlinkedModelSerializer, self).partial_update(instance, model_attrs)
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
password = serializers.CharField(max_length=128, label=_('Password'),
|
password = serializers.CharField(max_length=128, label=_('Password'),
|
||||||
|
|
|
@ -8,13 +8,13 @@ from .serializers import DatabaseSerializer, DatabaseUserSerializer
|
||||||
|
|
||||||
|
|
||||||
class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Database.objects.all()
|
queryset = Database.objects.prefetch_related('users').all()
|
||||||
serializer_class = DatabaseSerializer
|
serializer_class = DatabaseSerializer
|
||||||
filter_fields = ('name',)
|
filter_fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = DatabaseUser.objects.all()
|
queryset = DatabaseUser.objects.prefetch_related('databases').all()
|
||||||
serializer_class = DatabaseUserSerializer
|
serializer_class = DatabaseUserSerializer
|
||||||
filter_fields = ('username',)
|
filter_fields = ('username',)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,12 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class MySQLBackend(ServiceController):
|
class MySQLBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Simple backend for creating MySQL databases using `CREATE DATABASE` statement.
|
||||||
|
DATABASES_DEFAULT_HOST = %s
|
||||||
|
"""
|
||||||
|
format_docstring = (settings.DATABASES_DEFAULT_HOST,)
|
||||||
|
|
||||||
verbose_name = "MySQL database"
|
verbose_name = "MySQL database"
|
||||||
model = 'databases.Database'
|
model = 'databases.Database'
|
||||||
default_route_match = "database.type == 'mysql'"
|
default_route_match = "database.type == 'mysql'"
|
||||||
|
@ -54,6 +60,11 @@ class MySQLBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class MySQLUserBackend(ServiceController):
|
class MySQLUserBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Simple backend for creating MySQL users using `CREATE USER` statement.
|
||||||
|
DATABASES_DEFAULT_HOST = %s
|
||||||
|
""" % settings.DATABASES_DEFAULT_HOST
|
||||||
|
|
||||||
verbose_name = "MySQL user"
|
verbose_name = "MySQL user"
|
||||||
model = 'databases.DatabaseUser'
|
model = 'databases.DatabaseUser'
|
||||||
default_route_match = "databaseuser.type == 'mysql'"
|
default_route_match = "databaseuser.type == 'mysql'"
|
||||||
|
@ -93,6 +104,10 @@ class MySQLUserBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class MysqlDisk(ServiceMonitor):
|
class MysqlDisk(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
du -bs <database_path>
|
||||||
|
Implements triggers for resource limit exceeded and recovery, disabling insert and create privileges.
|
||||||
|
"""
|
||||||
model = 'databases.Database'
|
model = 'databases.Database'
|
||||||
verbose_name = _("MySQL disk")
|
verbose_name = _("MySQL disk")
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,18 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class Bind9MasterDomainBackend(ServiceController):
|
class Bind9MasterDomainBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Bind9 zone and config generation.
|
||||||
|
It auto-discovers slave Bind9 servers based on your routing configuration or you can use DOMAINS_SLAVES to explicitly configure the slaves.
|
||||||
|
DOMAINS_SLAVES = %s
|
||||||
|
DOMAINS_MASTERS_PATH = '%s'
|
||||||
|
"""
|
||||||
|
|
||||||
|
format_docstring = (
|
||||||
|
str(settings.DOMAINS_SLAVES),
|
||||||
|
settings.DOMAINS_MASTERS_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
verbose_name = _("Bind9 master domain")
|
verbose_name = _("Bind9 master domain")
|
||||||
model = 'domains.Domain'
|
model = 'domains.Domain'
|
||||||
related_models = (
|
related_models = (
|
||||||
|
@ -121,6 +133,12 @@ class Bind9MasterDomainBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
|
class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
|
||||||
|
"""
|
||||||
|
Generate the configuartion for slave servers
|
||||||
|
It auto-discover the master server based on your routing configuration or you can use DOMAINS_MASTERS to explicitly configure the master.
|
||||||
|
DOMAINS_MASTERS = %s
|
||||||
|
""" % str(settings.DOMAINS_MASTERS)
|
||||||
|
|
||||||
verbose_name = _("Bind9 slave domain")
|
verbose_name = _("Bind9 slave domain")
|
||||||
related_models = (
|
related_models = (
|
||||||
('domains.Domain', 'origin'),
|
('domains.Domain', 'origin'),
|
||||||
|
|
|
@ -10,6 +10,19 @@ from .models import List
|
||||||
|
|
||||||
|
|
||||||
class MailmanBackend(ServiceController):
|
class MailmanBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Mailman backend based on `newlist`, it handles custom domains.
|
||||||
|
LISTS_VIRTUAL_ALIAS_PATH = '%s'
|
||||||
|
LISTS_VIRTUAL_ALIAS_DOMAINS_PATH = '%s'
|
||||||
|
LISTS_DEFAULT_DOMAIN = '%s'
|
||||||
|
LISTS_MAILMAN_ROOT_DIR = '%s'
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.LISTS_VIRTUAL_ALIAS_PATH,
|
||||||
|
settings.LISTS_VIRTUAL_ALIAS_DOMAINS_PATH,
|
||||||
|
settings.LISTS_DEFAULT_DOMAIN,
|
||||||
|
settings.LISTS_MAILMAN_ROOT_DIR,
|
||||||
|
)
|
||||||
verbose_name = "Mailman"
|
verbose_name = "Mailman"
|
||||||
model = 'lists.List'
|
model = 'lists.List'
|
||||||
addresses = [
|
addresses = [
|
||||||
|
@ -149,75 +162,15 @@ class MailmanBackend(ServiceController):
|
||||||
return replace(context, "'", '"')
|
return replace(context, "'", '"')
|
||||||
|
|
||||||
|
|
||||||
class MailmanTrafficBash(ServiceMonitor):
|
|
||||||
model = 'lists.List'
|
|
||||||
resource = ServiceMonitor.TRAFFIC
|
|
||||||
verbose_name = _("Mailman traffic (Bash)")
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
super(MailmanTraffic, self).prepare()
|
|
||||||
context = {
|
|
||||||
'mailman_log': '%s{,.1}' % settings.LISTS_MAILMAN_POST_LOG_PATH,
|
|
||||||
'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
||||||
}
|
|
||||||
self.append(textwrap.dedent("""\
|
|
||||||
function monitor () {
|
|
||||||
OBJECT_ID=$1
|
|
||||||
# Dates convertions are done server-side because of timezone discrepancies
|
|
||||||
INI_DATE=$(date "+%%Y%%m%%d%%H%%M%%S" -d "$2")
|
|
||||||
END_DATE=$(date '+%%Y%%m%%d%%H%%M%%S' -d '%(current_date)s')
|
|
||||||
LIST_NAME="$3"
|
|
||||||
MAILMAN_LOG=%(mailman_log)s
|
|
||||||
|
|
||||||
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
|
|
||||||
{
|
|
||||||
{ grep " post to ${LIST_NAME} " ${MAILMAN_LOG} || echo '\\r'; } \\
|
|
||||||
| awk -v ini="${INI_DATE}" -v end="${END_DATE}" -v subs="${SUBSCRIBERS}" '
|
|
||||||
BEGIN {
|
|
||||||
sum = 0
|
|
||||||
months["Jan"] = "01"
|
|
||||||
months["Feb"] = "02"
|
|
||||||
months["Mar"] = "03"
|
|
||||||
months["Apr"] = "04"
|
|
||||||
months["May"] = "05"
|
|
||||||
months["Jun"] = "06"
|
|
||||||
months["Jul"] = "07"
|
|
||||||
months["Aug"] = "08"
|
|
||||||
months["Sep"] = "09"
|
|
||||||
months["Oct"] = "10"
|
|
||||||
months["Nov"] = "11"
|
|
||||||
months["Dec"] = "12"
|
|
||||||
} {
|
|
||||||
# Mar 01 08:29:02 2015
|
|
||||||
month = months[$1]
|
|
||||||
day = $2
|
|
||||||
year = $4
|
|
||||||
split($3, time, ":")
|
|
||||||
line_date = year month day time[1] time[2] time[3]
|
|
||||||
if ( line_date > ini && line_date < end)
|
|
||||||
sum += substr($11, 6, length($11)-6)
|
|
||||||
} END {
|
|
||||||
print sum * subs
|
|
||||||
}' || [[ $? == 1 ]] && true
|
|
||||||
} | xargs echo ${OBJECT_ID}
|
|
||||||
}""") % context)
|
|
||||||
|
|
||||||
def monitor(self, mail_list):
|
|
||||||
context = self.get_context(mail_list)
|
|
||||||
self.append(
|
|
||||||
'monitor %(object_id)i "%(last_date)s" "%(list_name)s"' % context
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context(self, mail_list):
|
|
||||||
context = {
|
|
||||||
'list_name': mail_list.name,
|
|
||||||
'object_id': mail_list.pk,
|
|
||||||
'last_date': self.get_last_date(mail_list.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
||||||
}
|
|
||||||
return replace(context, "'", '"')
|
|
||||||
|
|
||||||
|
|
||||||
class MailmanTraffic(ServiceMonitor):
|
class MailmanTraffic(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
Parses mailman log file looking for email size and multiples it by `list_members` count.
|
||||||
|
LISTS_MAILMAN_POST_LOG_PATH = '%s'
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.LISTS_MAILMAN_POST_LOG_PATH,
|
||||||
|
)
|
||||||
model = 'lists.List'
|
model = 'lists.List'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
verbose_name = _("Mailman traffic")
|
verbose_name = _("Mailman traffic")
|
||||||
|
@ -318,6 +271,9 @@ class MailmanTraffic(ServiceMonitor):
|
||||||
|
|
||||||
|
|
||||||
class MailmanSubscribers(ServiceMonitor):
|
class MailmanSubscribers(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
Monitors number of list subscribers via `list_members`
|
||||||
|
"""
|
||||||
model = 'lists.List'
|
model = 'lists.List'
|
||||||
verbose_name = _("Mailman subscribers")
|
verbose_name = _("Mailman subscribers")
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,12 @@ from .serializers import AddressSerializer, MailboxSerializer
|
||||||
|
|
||||||
|
|
||||||
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Address.objects.all()
|
queryset = Address.objects.select_related('domain').prefetch_related('mailboxes').all()
|
||||||
serializer_class = AddressSerializer
|
serializer_class = AddressSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Mailbox.objects.all()
|
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
||||||
serializer_class = MailboxSerializer
|
serializer_class = MailboxSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,10 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UNIXUserMaildirBackend(ServiceController):
|
class UNIXUserMaildirBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Assumes that all system users on this servers all mail accounts.
|
||||||
|
If you want to have system users AND mailboxes on the same server you should consider using virtual mailboxes
|
||||||
|
"""
|
||||||
verbose_name = _("UNIX maildir user")
|
verbose_name = _("UNIX maildir user")
|
||||||
model = 'mailboxes.Mailbox'
|
model = 'mailboxes.Mailbox'
|
||||||
|
|
||||||
|
@ -74,6 +78,9 @@ class UNIXUserMaildirBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
|
class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
WARNING: This backends is not fully implemented
|
||||||
|
"""
|
||||||
verbose_name = _("Dovecot-Postfix virtualuser")
|
verbose_name = _("Dovecot-Postfix virtualuser")
|
||||||
model = 'mailboxes.Mailbox'
|
model = 'mailboxes.Mailbox'
|
||||||
# TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
|
# TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
|
||||||
|
@ -176,6 +183,17 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class PostfixAddressBackend(ServiceController):
|
class PostfixAddressBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Addresses based on Postfix virtual alias domains.
|
||||||
|
<tt>MAILBOXES_LOCAL_DOMAIN = '%s'
|
||||||
|
MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = '%s'
|
||||||
|
MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = '%s'</tt>
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.MAILBOXES_LOCAL_DOMAIN,
|
||||||
|
settings.MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH,
|
||||||
|
settings.MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH,
|
||||||
|
)
|
||||||
verbose_name = _("Postfix address")
|
verbose_name = _("Postfix address")
|
||||||
model = 'mailboxes.Address'
|
model = 'mailboxes.Address'
|
||||||
related_models = (
|
related_models = (
|
||||||
|
@ -267,6 +285,9 @@ class PostfixAddressBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class AutoresponseBackend(ServiceController):
|
class AutoresponseBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
WARNING: not implemented
|
||||||
|
"""
|
||||||
verbose_name = _("Mail autoresponse")
|
verbose_name = _("Mail autoresponse")
|
||||||
model = 'mailboxes.Autoresponse'
|
model = 'mailboxes.Autoresponse'
|
||||||
|
|
||||||
|
@ -274,9 +295,13 @@ class AutoresponseBackend(ServiceController):
|
||||||
class DovecotMaildirDisk(ServiceMonitor):
|
class DovecotMaildirDisk(ServiceMonitor):
|
||||||
"""
|
"""
|
||||||
Maildir disk usage based on Dovecot maildirsize file
|
Maildir disk usage based on Dovecot maildirsize file
|
||||||
|
|
||||||
http://wiki2.dovecot.org/Quota/Maildir
|
http://wiki2.dovecot.org/Quota/Maildir
|
||||||
|
|
||||||
|
MAILBOXES_MAILDIRSIZE_PATH = '%s'
|
||||||
"""
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.MAILBOXES_MAILDIRSIZE_PATH,
|
||||||
|
)
|
||||||
model = 'mailboxes.Mailbox'
|
model = 'mailboxes.Mailbox'
|
||||||
resource = ServiceMonitor.DISK
|
resource = ServiceMonitor.DISK
|
||||||
verbose_name = _("Dovecot Maildir size")
|
verbose_name = _("Dovecot Maildir size")
|
||||||
|
@ -304,9 +329,13 @@ class DovecotMaildirDisk(ServiceMonitor):
|
||||||
|
|
||||||
class PostfixMailscannerTraffic(ServiceMonitor):
|
class PostfixMailscannerTraffic(ServiceMonitor):
|
||||||
"""
|
"""
|
||||||
A high-performance log parser
|
A high-performance log parser.
|
||||||
Reads the mail.log file only once, for all users
|
Reads the mail.log file only once, for all users.
|
||||||
|
MAILBOXES_MAIL_LOG_PATH = '%s'
|
||||||
"""
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.MAILBOXES_MAIL_LOG_PATH,
|
||||||
|
)
|
||||||
model = 'mailboxes.Mailbox'
|
model = 'mailboxes.Mailbox'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
verbose_name = _("Postfix-Mailscanner traffic")
|
verbose_name = _("Postfix-Mailscanner traffic")
|
||||||
|
@ -460,8 +489,3 @@ 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 replace(context, "'", '"')
|
return replace(context, "'", '"')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,10 @@ class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address
|
model = Address
|
||||||
fields = ('url', 'name', 'domain', 'forward')
|
fields = ('url', 'name', 'domain', 'forward')
|
||||||
|
#
|
||||||
def from_native(self, data, files=None):
|
# def from_native(self, data, files=None):
|
||||||
queryset = self.opts.model.objects.filter(account=self.account)
|
# queryset = self.opts.model.objects.filter(account=self.account)
|
||||||
return get_object_or_404(queryset, name=data['name'])
|
# return get_object_or_404(queryset, name=data['name'])
|
||||||
|
|
||||||
|
|
||||||
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
||||||
|
|
|
@ -25,9 +25,7 @@ class Operation():
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
# instance should maintain any dynamic attribute until backend execution
|
# instance should maintain any dynamic attribute until backend execution
|
||||||
# deep copy is prefered over copy otherwise objects will share same atributes (queryset cache)
|
# deep copy is prefered over copy otherwise objects will share same atributes (queryset cache)
|
||||||
print('aa', getattr(instance, 'password', 'NOOOO'), id(instance))
|
|
||||||
self.instance = copy.deepcopy(instance)
|
self.instance = copy.deepcopy(instance)
|
||||||
print('aa', getattr(self.instance, 'password', 'NOOOO'), id(self.instance))
|
|
||||||
self.action = action
|
self.action = action
|
||||||
self.servers = servers
|
self.servers = servers
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from orchestra.admin.html import monospace_format
|
from orchestra.admin.html import monospace_format
|
||||||
from orchestra.admin.utils import admin_link, admin_date, admin_colored
|
from orchestra.admin.utils import admin_link, admin_date, admin_colored
|
||||||
|
|
||||||
from . import settings
|
from . import settings, helpers
|
||||||
from .backends import ServiceBackend
|
from .backends import ServiceBackend
|
||||||
from .models import Server, Route, BackendLog, BackendOperation
|
from .models import Server, Route, BackendLog, BackendOperation
|
||||||
from .widgets import RouteBackendSelect
|
from .widgets import RouteBackendSelect
|
||||||
|
@ -31,10 +31,7 @@ class RouteAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('host', 'is_active', 'backend')
|
list_filter = ('host', 'is_active', 'backend')
|
||||||
ordering = ('backend',)
|
ordering = ('backend',)
|
||||||
|
|
||||||
BACKEND_HELP_TEXT = {
|
BACKEND_HELP_TEXT = helpers.get_backends_help_text(ServiceBackend.get_backends())
|
||||||
backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model
|
|
||||||
for backend, __ in ServiceBackend.get_choices()
|
|
||||||
}
|
|
||||||
DEFAULT_MATCH = {
|
DEFAULT_MATCH = {
|
||||||
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
|
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
default_route_match = 'True'
|
default_route_match = 'True'
|
||||||
# Force the backend manager to block in multiple backend executions executing them synchronously
|
# Force the backend manager to block in multiple backend executions executing them synchronously
|
||||||
block = False
|
block = False
|
||||||
|
format_docstring = ()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return type(self).__name__
|
return type(self).__name__
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.mail import mail_admins
|
from django.core.mail import mail_admins
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
@ -6,6 +8,36 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def get_backends_help_text(backends):
|
||||||
|
help_texts = {}
|
||||||
|
for backend in backends:
|
||||||
|
context = {
|
||||||
|
'model': backend.model,
|
||||||
|
'related_models': str(backend.related_models),
|
||||||
|
'script_executable': backend.script_executable,
|
||||||
|
'script_method': str(backend.script_method),
|
||||||
|
'function_method': str(backend.script_method),
|
||||||
|
'actions': ', '.join(backend.actions),
|
||||||
|
}
|
||||||
|
help_text = textwrap.dedent("""
|
||||||
|
- Model: '%(model)s'<br>
|
||||||
|
- Related models: %(related_models)s<br>
|
||||||
|
- Script executable: %(script_executable)s<br>
|
||||||
|
- Script method: %(script_method)s<br>
|
||||||
|
- Function method: %(function_method)s<br>
|
||||||
|
- Actions: %(actions)s<br>"""
|
||||||
|
) % context
|
||||||
|
docstring = backend.__doc__
|
||||||
|
if docstring:
|
||||||
|
try:
|
||||||
|
docstring = (docstring % backend.format_docstring).strip().splitlines()
|
||||||
|
except TypeError as e:
|
||||||
|
raise TypeError(str(backend) + str(e))
|
||||||
|
help_text += '<br>' + '<br>'.join(docstring)
|
||||||
|
help_texts[backend.get_name()] = help_text
|
||||||
|
return help_texts
|
||||||
|
|
||||||
|
|
||||||
def send_report(method, args, log):
|
def send_report(method, args, log):
|
||||||
server = args[0]
|
server = args[0]
|
||||||
backend = method.__self__.__class__.__name__
|
backend = method.__self__.__class__.__name__
|
||||||
|
|
|
@ -32,5 +32,5 @@ ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECU
|
||||||
|
|
||||||
|
|
||||||
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
|
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
|
||||||
timedelta(days=40)
|
timedelta(days=15)
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,6 +10,16 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class UNIXUserBackend(ServiceController):
|
class UNIXUserBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Basic UNIX system user/group support based on `useradd`, `usermod`, `userdel` and `groupdel`.
|
||||||
|
<tt>SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = '%s'
|
||||||
|
SYSTEMUSERS_MOVE_ON_DELETE_PATH = '%s'</tt>
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS,
|
||||||
|
settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
verbose_name = _("UNIX user")
|
verbose_name = _("UNIX user")
|
||||||
model = 'systemusers.SystemUser'
|
model = 'systemusers.SystemUser'
|
||||||
actions = ('save', 'delete', 'grant_permission')
|
actions = ('save', 'delete', 'grant_permission')
|
||||||
|
@ -84,6 +94,9 @@ class UNIXUserBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class UNIXUserDisk(ServiceMonitor):
|
class UNIXUserDisk(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
`du -bs <home>`
|
||||||
|
"""
|
||||||
model = 'systemusers.SystemUser'
|
model = 'systemusers.SystemUser'
|
||||||
resource = ServiceMonitor.DISK
|
resource = ServiceMonitor.DISK
|
||||||
verbose_name = _('UNIX user disk')
|
verbose_name = _('UNIX user disk')
|
||||||
|
@ -109,6 +122,14 @@ class UNIXUserDisk(ServiceMonitor):
|
||||||
|
|
||||||
|
|
||||||
class Exim4Traffic(ServiceMonitor):
|
class Exim4Traffic(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
Exim4 mainlog parser for mails sent on the webserver by system users (e.g. via PHP mail())
|
||||||
|
SYSTEMUSERS_MAIL_LOG_PATH = '%s'
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.SYSTEMUSERS_MAIL_LOG_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
model = 'systemusers.SystemUser'
|
model = 'systemusers.SystemUser'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
verbose_name = _("Exim4 traffic")
|
verbose_name = _("Exim4 traffic")
|
||||||
|
@ -188,6 +209,13 @@ class Exim4Traffic(ServiceMonitor):
|
||||||
|
|
||||||
|
|
||||||
class VsFTPdTraffic(ServiceMonitor):
|
class VsFTPdTraffic(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
vsFTPd log parser.
|
||||||
|
SYSTEMUSERS_FTP_LOG_PATH = '%s'
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.SYSTEMUSERS_FTP_LOG_PATH,
|
||||||
|
)
|
||||||
model = 'systemusers.SystemUser'
|
model = 'systemusers.SystemUser'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
verbose_name = _('VsFTPd traffic')
|
verbose_name = _('VsFTPd traffic')
|
||||||
|
@ -279,4 +307,3 @@ class VsFTPdTraffic(ServiceMonitor):
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
}
|
}
|
||||||
return replace(context, "'", '"')
|
return replace(context, "'", '"')
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@ from orchestra.contrib.resources import ServiceMonitor
|
||||||
|
|
||||||
|
|
||||||
class OpenVZTraffic(ServiceMonitor):
|
class OpenVZTraffic(ServiceMonitor):
|
||||||
|
"""
|
||||||
|
WARNING: Not fully implemeted
|
||||||
|
"""
|
||||||
model = 'vps.VPS'
|
model = 'vps.VPS'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,13 @@ from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .models import WebApp
|
from .models import WebApp
|
||||||
|
from .options import AppOption
|
||||||
from .serializers import WebAppSerializer
|
from .serializers import WebAppSerializer
|
||||||
|
from .types import AppType
|
||||||
|
|
||||||
|
|
||||||
class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = WebApp.objects.all()
|
queryset = WebApp.objects.prefetch_related('options').all()
|
||||||
serializer_class = WebAppSerializer
|
serializer_class = WebAppSerializer
|
||||||
filter_fields = ('name',)
|
filter_fields = ('name',)
|
||||||
|
|
||||||
|
@ -22,6 +24,31 @@ class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
metadata.data['settings'] = {
|
metadata.data['settings'] = {
|
||||||
name.lower(): getattr(settings, name, None) for name in names
|
name.lower(): getattr(settings, name, None) for name in names
|
||||||
}
|
}
|
||||||
|
# AppTypes
|
||||||
|
meta = self.metadata_class()
|
||||||
|
app_types = {}
|
||||||
|
for app_type in AppType.get_plugins():
|
||||||
|
if app_type.serializer:
|
||||||
|
data = meta.get_serializer_info(app_type.serializer())
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
options = []
|
||||||
|
for group, option in app_type.get_options():
|
||||||
|
options += [opt.name for opt in option]
|
||||||
|
app_types[app_type.get_name()] = {
|
||||||
|
'data': data,
|
||||||
|
'options': options,
|
||||||
|
}
|
||||||
|
metadata.data['actions']['types'] = app_types
|
||||||
|
# Options
|
||||||
|
options = {}
|
||||||
|
for option in AppOption.get_plugins():
|
||||||
|
options[option.get_name()] = {
|
||||||
|
'verbose_name': option.get_verbose_name(),
|
||||||
|
'help_text': option.help_text,
|
||||||
|
'group': option.group,
|
||||||
|
}
|
||||||
|
metadata.data['actions']['options'] = options
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,29 @@ from .. import settings
|
||||||
|
|
||||||
|
|
||||||
class PHPBackend(WebAppServiceMixin, ServiceController):
|
class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
|
"""
|
||||||
|
PHP support for apache-mod-fcgid and php-fpm.
|
||||||
|
It handles switching between these two PHP process management systemes.
|
||||||
|
WEBAPPS_MERGE_PHP_WEBAPPS = %s
|
||||||
|
WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = %s
|
||||||
|
WEBAPPS_PHP_CGI_BINARY_PATH = '%s'
|
||||||
|
WEBAPPS_PHP_CGI_RC_DIR = '%s'
|
||||||
|
WEBAPPS_PHP_CGI_INI_SCAN_DIR = '%s'
|
||||||
|
WEBAPPS_FCGID_CMD_OPTIONS_PATH = '%s'
|
||||||
|
WEBAPPS_PHPFPM_POOL_PATH = '%s'
|
||||||
|
WEBAPPS_PHP_MAX_REQUESTS = %s
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.WEBAPPS_MERGE_PHP_WEBAPPS,
|
||||||
|
settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN,
|
||||||
|
settings.WEBAPPS_PHP_CGI_BINARY_PATH,
|
||||||
|
settings.WEBAPPS_PHP_CGI_RC_DIR,
|
||||||
|
settings.WEBAPPS_PHP_CGI_INI_SCAN_DIR,
|
||||||
|
settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH,
|
||||||
|
settings.WEBAPPS_PHPFPM_POOL_PATH,
|
||||||
|
settings.WEBAPPS_PHP_MAX_REQUESTS,
|
||||||
|
)
|
||||||
|
|
||||||
verbose_name = _("PHP FPM/FCGID")
|
verbose_name = _("PHP FPM/FCGID")
|
||||||
default_route_match = "webapp.type.endswith('php')"
|
default_route_match = "webapp.type.endswith('php')"
|
||||||
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
|
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
|
||||||
|
|
|
@ -13,9 +13,19 @@ from .. import settings
|
||||||
class uWSGIPythonBackend(WebAppServiceMixin, ServiceController):
|
class uWSGIPythonBackend(WebAppServiceMixin, ServiceController):
|
||||||
"""
|
"""
|
||||||
Emperor mode
|
Emperor mode
|
||||||
|
|
||||||
http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
|
http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
|
||||||
|
WEBAPPS_UWSGI_BASE_DIR = '%s'
|
||||||
|
WEBAPPS_PYTHON_MAX_REQUESTS = %s
|
||||||
|
WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS = %s
|
||||||
|
WEBAPPS_PYTHON_DEFAULT_TIMEOUT = %s
|
||||||
"""
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||||
|
settings.WEBAPPS_PYTHON_MAX_REQUESTS,
|
||||||
|
settings.WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS,
|
||||||
|
settings.WEBAPPS_PYTHON_DEFAULT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
verbose_name = _("Python uWSGI")
|
verbose_name = _("Python uWSGI")
|
||||||
default_route_match = "webapp.type.endswith('python')"
|
default_route_match = "webapp.type.endswith('python')"
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,10 @@ from . import WebAppServiceMixin
|
||||||
|
|
||||||
|
|
||||||
class StaticBackend(WebAppServiceMixin, ServiceController):
|
class StaticBackend(WebAppServiceMixin, ServiceController):
|
||||||
|
"""
|
||||||
|
Static web pages.
|
||||||
|
Only creates the webapp dir and leaves the web server the decision to execute CGIs or not.
|
||||||
|
"""
|
||||||
verbose_name = _("Static")
|
verbose_name = _("Static")
|
||||||
default_route_match = "webapp.type == 'static'"
|
default_route_match = "webapp.type == 'static'"
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,10 @@ from .php import PHPBackend
|
||||||
|
|
||||||
|
|
||||||
class SymbolicLinkBackend(PHPBackend, ServiceController):
|
class SymbolicLinkBackend(PHPBackend, ServiceController):
|
||||||
|
"""
|
||||||
|
Same as PHPBackend but allows you to have the webapps on a directory diferent than the webapps dir.
|
||||||
|
"""
|
||||||
|
format_docstring = ()
|
||||||
verbose_name = _("Symbolic link webapp")
|
verbose_name = _("Symbolic link webapp")
|
||||||
model = 'webapps.WebApp'
|
model = 'webapps.WebApp'
|
||||||
default_route_match = "webapp.type == 'symbolic-link'"
|
default_route_match = "webapp.type == 'symbolic-link'"
|
||||||
|
|
|
@ -7,7 +7,9 @@ from . import WebAppServiceMixin
|
||||||
|
|
||||||
# TODO DEPRECATE
|
# TODO DEPRECATE
|
||||||
class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
|
class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
|
||||||
""" Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """
|
"""
|
||||||
|
Needed for cleaning up webalizer main folder when webapp deleteion withou related contents
|
||||||
|
"""
|
||||||
verbose_name = _("Webalizer App")
|
verbose_name = _("Webalizer App")
|
||||||
default_route_match = "webapp.type == 'webalizer'"
|
default_route_match = "webapp.type == 'webalizer'"
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,14 @@ from . import WebAppServiceMixin
|
||||||
|
|
||||||
# Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php
|
# Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php
|
||||||
class WordPressBackend(WebAppServiceMixin, ServiceController):
|
class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
|
"""
|
||||||
|
Installs the latest version of WordPress available on www.wordpress.org
|
||||||
|
It fully configures the wp-config.php (keys included) and sets up the database with initial admin password.
|
||||||
|
WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = '%s'
|
||||||
|
"""
|
||||||
|
format_docstring = (
|
||||||
|
settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST,
|
||||||
|
)
|
||||||
verbose_name = _("Wordpress")
|
verbose_name = _("Wordpress")
|
||||||
model = 'webapps.WebApp'
|
model = 'webapps.WebApp'
|
||||||
default_route_match = "webapp.type == 'wordpress-php'"
|
default_route_match = "webapp.type == 'wordpress-php'"
|
||||||
|
|
|
@ -35,7 +35,7 @@ class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
options_data = validated_data.pop('options')
|
options_data = validated_data.pop('options')
|
||||||
instance = super(WebAppSerializer, self).update(validated_data)
|
instance = super(WebAppSerializer, self).update(instance, validated_data)
|
||||||
existing = {}
|
existing = {}
|
||||||
for obj in instance.options.all():
|
for obj in instance.options.all():
|
||||||
existing[obj.name] = obj
|
existing[obj.name] = obj
|
||||||
|
|
|
@ -114,6 +114,11 @@ WEBAPPS_UWSGI_SOCKET = getattr(settings, 'WEBAPPS_UWSGI_SOCKET',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WEBAPPS_UWSGI_BASE_DIR = getattr(settings, 'WEBAPPS_UWSGI_BASE_DIR',
|
||||||
|
'/etc/uwsgi/'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
WEBAPPS_PYTHON_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PYTHON_MAX_REQUESTS',
|
WEBAPPS_PYTHON_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PYTHON_MAX_REQUESTS',
|
||||||
500
|
500
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from .serializers import WebsiteSerializer
|
||||||
|
|
||||||
|
|
||||||
class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Website.objects.all()
|
queryset = Website.objects.prefetch_related('domains', 'content_set__webapp', 'directives').all()
|
||||||
serializer_class = WebsiteSerializer
|
serializer_class = WebsiteSerializer
|
||||||
filter_fields = ('name',)
|
filter_fields = ('name',)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ from .. import settings
|
||||||
|
|
||||||
|
|
||||||
class WebalizerBackend(ServiceController):
|
class WebalizerBackend(ServiceController):
|
||||||
|
"""
|
||||||
|
Creates webalizer conf file for each time a webalizer webapp is mounted on a website.
|
||||||
|
"""
|
||||||
verbose_name = _("Webalizer Content")
|
verbose_name = _("Webalizer Content")
|
||||||
model = 'websites.Content'
|
model = 'websites.Content'
|
||||||
default_route_match = "content.webapp.type == 'webalizer'"
|
default_route_match = "content.webapp.type == 'webalizer'"
|
||||||
|
|
|
@ -82,7 +82,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
options_data = validated_data.pop('options')
|
options_data = validated_data.pop('options')
|
||||||
instance = super(WebsiteSerializer, self).update(validated_data)
|
instance = super(WebsiteSerializer, self).update(instance, validated_data)
|
||||||
existing = {}
|
existing = {}
|
||||||
for obj in instance.options.all():
|
for obj in instance.options.all():
|
||||||
existing[obj.name] = obj
|
existing[obj.name] = obj
|
||||||
|
|
Loading…
Reference in a new issue