PEP8 compliance
This commit is contained in:
parent
553be610cb
commit
7128db2640
|
@ -41,8 +41,8 @@ source env-django-orchestra/bin/activate
|
||||||
pip3 install django-orchestra==dev \
|
pip3 install django-orchestra==dev \
|
||||||
--allow-external django-orchestra \
|
--allow-external django-orchestra \
|
||||||
--allow-unverified django-orchestra
|
--allow-unverified django-orchestra
|
||||||
# The only non-pip required dependency for runing pip install is python3-dev
|
# The only non-pip required dependency for runing pip3 install is python3-dev
|
||||||
# sudo apt-get install python3.4-dev
|
sudo apt-get install python3-dev
|
||||||
pip3 install -r http://git.io/orchestra-requirements.txt
|
pip3 install -r http://git.io/orchestra-requirements.txt
|
||||||
|
|
||||||
# Create a new Orchestra site
|
# Create a new Orchestra site
|
||||||
|
|
|
@ -52,8 +52,6 @@ class SendEmail(object):
|
||||||
|
|
||||||
}
|
}
|
||||||
return self.confirm_email(request, **options)
|
return self.confirm_email(request, **options)
|
||||||
opts = self.modeladmin.model._meta
|
|
||||||
app_label = opts.app_label
|
|
||||||
self.context.update({
|
self.context.update({
|
||||||
'title': _("Send e-mail to %s") % self.opts.verbose_name_plural,
|
'title': _("Send e-mail to %s") % self.opts.verbose_name_plural,
|
||||||
'content_title': "",
|
'content_title': "",
|
||||||
|
|
|
@ -21,7 +21,7 @@ from ..utils.python import random_ascii, pairwise
|
||||||
|
|
||||||
from .forms import AdminPasswordChangeForm
|
from .forms import AdminPasswordChangeForm
|
||||||
#from django.contrib.auth.forms import AdminPasswordChangeForm
|
#from django.contrib.auth.forms import AdminPasswordChangeForm
|
||||||
from .utils import set_url_query, action_to_view
|
from .utils import action_to_view
|
||||||
|
|
||||||
|
|
||||||
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
|
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
|
||||||
|
|
|
@ -34,7 +34,7 @@ class LogApiMixin(object):
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
from django.contrib.admin.models import CHANGE
|
from django.contrib.admin.models import CHANGE
|
||||||
response = super(LogApiMixin, self).partial_update(request, *args, **kwargs)
|
response = super(LogApiMixin, self).partial_update(request, *args, **kwargs)
|
||||||
message = _('Changed %s') % str(response.data)
|
message = _('Changed %s') % response.data
|
||||||
self.log(request, message, CHANGE)
|
self.log(request, message, CHANGE)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
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 _
|
||||||
|
@ -69,7 +70,7 @@ class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||||
'url': "URL is required."
|
'url': "URL is required."
|
||||||
})
|
})
|
||||||
account = self.get_account()
|
account = self.get_account()
|
||||||
queryset = self.Meta.model.objects.filter(account=self.get_account())
|
queryset = self.Meta.model.objects.filter(account=account)
|
||||||
self.fields['url'].queryset = queryset
|
self.fields['url'].queryset = queryset
|
||||||
obj = self.fields['url'].to_internal_value(url)
|
obj = self.fields['url'].to_internal_value(url)
|
||||||
return obj
|
return obj
|
||||||
|
@ -108,26 +109,3 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
instance.set_password(password)
|
instance.set_password(password)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
#class MultiSelectField(serializers.ChoiceField):
|
|
||||||
# widget = widgets.CheckboxSelectMultiple
|
|
||||||
#
|
|
||||||
# def field_from_native(self, data, files, field_name, into):
|
|
||||||
# """ convert multiselect data into comma separated string """
|
|
||||||
# if field_name in data:
|
|
||||||
# data = data.copy()
|
|
||||||
# try:
|
|
||||||
# # data is a querydict when using forms
|
|
||||||
# data[field_name] = ','.join(data.getlist(field_name))
|
|
||||||
# except AttributeError:
|
|
||||||
# data[field_name] = ','.join(data[field_name])
|
|
||||||
# return super(MultiSelectField, self).field_from_native(data, files, field_name, into)
|
|
||||||
#
|
|
||||||
# def valid_value(self, value):
|
|
||||||
# """ checks for each item if is a valid value """
|
|
||||||
# for val in value.split(','):
|
|
||||||
# valid = super(MultiSelectField, self).valid_value(val)
|
|
||||||
# if not valid:
|
|
||||||
# return False
|
|
||||||
# return True
|
|
||||||
|
|
|
@ -4,4 +4,3 @@ from django.conf.urls import include, url
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'', include('orchestra.urls')),
|
url(r'', include('orchestra.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.contrib.admin.utils import NestedObjects, quote, model_ngettext
|
from django.contrib.admin.utils import NestedObjects, quote
|
||||||
from django.contrib.auth import get_permission_codename
|
from django.contrib.auth import get_permission_codename
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
from django.db import router
|
from django.db import router
|
||||||
|
@ -77,7 +77,6 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
related_services = []
|
related_services = []
|
||||||
to_delete = []
|
to_delete = []
|
||||||
|
|
||||||
user = request.user
|
|
||||||
admin_site = modeladmin.admin_site
|
admin_site = modeladmin.admin_site
|
||||||
|
|
||||||
def format(obj, account=False):
|
def format(obj, account=False):
|
||||||
|
@ -87,16 +86,14 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
|
|
||||||
if has_admin:
|
if has_admin:
|
||||||
try:
|
try:
|
||||||
admin_url = reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name),
|
admin_url = reverse(
|
||||||
|
'admin:%s_%s_change' % (opts.app_label, opts.model_name),
|
||||||
None, (quote(obj._get_pk_val()),)
|
None, (quote(obj._get_pk_val()),)
|
||||||
)
|
)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Change url doesn't exist -- don't display link to edit
|
# Change url doesn't exist -- don't display link to edit
|
||||||
return no_edit_link
|
return no_edit_link
|
||||||
|
|
||||||
p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts))
|
|
||||||
if not user.has_perm(p):
|
|
||||||
perms_needed.add(opts.verbose_name)
|
|
||||||
# Display a link to the admin page.
|
# Display a link to the admin page.
|
||||||
context = (capfirst(opts.verbose_name), admin_url, obj)
|
context = (capfirst(opts.verbose_name), admin_url, obj)
|
||||||
if account:
|
if account:
|
||||||
|
|
|
@ -91,8 +91,8 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdmin, self).change_view(request, object_id,
|
return super(AccountAdmin, self).change_view(
|
||||||
form_url=form_url, extra_context=context)
|
request, object_id, form_url, context)
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
||||||
|
@ -233,11 +233,14 @@ class AccountAdminMixin(object):
|
||||||
if self.account:
|
if self.account:
|
||||||
# Hack widget render in order to append ?account=id to the add url
|
# Hack widget render in order to append ?account=id to the add url
|
||||||
old_render = formfield.widget.render
|
old_render = formfield.widget.render
|
||||||
|
|
||||||
def render(*args, **kwargs):
|
def render(*args, **kwargs):
|
||||||
output = old_render(*args, **kwargs)
|
output = old_render(*args, **kwargs)
|
||||||
output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk)
|
output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk)
|
||||||
output = re.sub(r'/add/\?([^".]*)"', r'/add/?\1&account=%s"' % self.account.pk, output)
|
with_qargs = r'/add/?\1&account=%s"' % self.account.pk
|
||||||
|
output = re.sub(r'/add/\?([^".]*)"', with_qargs, output)
|
||||||
return mark_safe(output)
|
return mark_safe(output)
|
||||||
|
|
||||||
formfield.widget.render = render
|
formfield.widget.render = render
|
||||||
# Filter related object by account
|
# Filter related object by account
|
||||||
formfield.queryset = formfield.queryset.filter(account=self.account)
|
formfield.queryset = formfield.queryset.filter(account=self.account)
|
||||||
|
@ -282,8 +285,8 @@ class AccountAdminMixin(object):
|
||||||
'account_opts': Account._meta,
|
'account_opts': Account._meta,
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdminMixin, self).changeform_view(request,
|
return super(AccountAdminMixin, self).changeform_view(
|
||||||
object_id=object_id, form_url=form_url, extra_context=context)
|
request, object_id, form_url=form_url, extra_context=context)
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
account_id = request.GET.get('account')
|
account_id = request.GET.get('account')
|
||||||
|
@ -318,7 +321,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
account = self.account
|
account = self.account
|
||||||
else:
|
else:
|
||||||
account = Account.objects.get(pk=request.GET['account'])
|
account = Account.objects.get(pk=request.GET['account'])
|
||||||
[ setattr(inline, 'account', account) for inline in inlines ]
|
[setattr(inline, 'account', account) for inline in inlines]
|
||||||
return inlines
|
return inlines
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
|
@ -355,8 +358,8 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
'account_opts': Account._meta,
|
'account_opts': Account._meta,
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdminMixin, self).add_view(request,
|
return super(AccountAdminMixin, self).add_view(
|
||||||
form_url=form_url, extra_context=context)
|
request, form_url=form_url, extra_context=context)
|
||||||
return HttpResponseRedirect('./select-account/?%s' % request.META['QUERY_STRING'])
|
return HttpResponseRedirect('./select-account/?%s' % request.META['QUERY_STRING'])
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
|
|
@ -37,5 +37,3 @@ class IsActiveListFilter(SimpleListFilter):
|
||||||
elif self.value() == 'False':
|
elif self.value() == 'False':
|
||||||
return queryset.filter(Q(is_active=False) | Q(account__is_active=False))
|
return queryset.filter(Q(is_active=False) | Q(account__is_active=False))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import io
|
import io
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
@ -17,7 +15,6 @@ from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
from orchestra.admin.forms import adminmodelformset_factory
|
from orchestra.admin.forms import adminmodelformset_factory
|
||||||
from orchestra.admin.decorators import action_with_confirmation
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
from orchestra.admin.utils import get_object_from_url, change_url
|
from orchestra.admin.utils import get_object_from_url, change_url
|
||||||
from orchestra.utils.html import html_to_pdf
|
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .forms import SelectSourceForm
|
from .forms import SelectSourceForm
|
||||||
|
|
|
@ -52,7 +52,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
if sublines:
|
if sublines:
|
||||||
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
|
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
|
||||||
img = static('admin/img/icon_alert.gif')
|
img = static('admin/img/icon_alert.gif')
|
||||||
return '<span title="%s">%s <img src="%s"></img></span>' % (content, str(total), img)
|
return '<span title="%s">%s <img src="%s"></img></span>' % (content, total, img)
|
||||||
return total
|
return total
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
display_total.allow_tags = True
|
||||||
|
|
|
@ -25,4 +25,3 @@ def validate_contact(request, bill, error=True):
|
||||||
send(request, mark_safe(message))
|
send(request, mark_safe(message))
|
||||||
valid = False
|
valid = False
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
|
|
|
@ -13,4 +13,3 @@ class ContactViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
router.register(r'contacts', ContactViewSet)
|
router.register(r'contacts', ContactViewSet)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django import forms
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django import db
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
from orchestra.core import administration
|
from orchestra.core import administration
|
||||||
|
|
|
@ -54,4 +54,3 @@ class TicketStateListFilter(SimpleListFilter):
|
||||||
choices = iter(super(TicketStateListFilter, self).choices(cl))
|
choices = iter(super(TicketStateListFilter, self).choices(cl))
|
||||||
next(choices)
|
next(choices)
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
|
|
@ -45,4 +45,3 @@ class HasAddressListFilter(HasMailboxListFilter):
|
||||||
elif self.value() == 'False':
|
elif self.value() == 'False':
|
||||||
return queryset.filter(addresses__isnull=True)
|
return queryset.filter(addresses__isnull=True)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -158,7 +158,8 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
return list(scripts.items())
|
return list(scripts.items())
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self):
|
||||||
time = timezone.now().strftime("%h %d, %Y %I:%M:%S %Z")
|
now = timezone.localtime(timezone.now())
|
||||||
|
time = now.strftime("%h %d, %Y %I:%M:%S %Z")
|
||||||
return "Generated by Orchestra at %s" % time
|
return "Generated by Orchestra at %s" % time
|
||||||
|
|
||||||
def create_log(self, server, **kwargs):
|
def create_log(self, server, **kwargs):
|
||||||
|
|
|
@ -43,7 +43,7 @@ def get_backends_help_text(backends):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
help_settings.append("<tt>%s = '%s'</tt>" % (name, value))
|
help_settings.append("<tt>%s = '%s'</tt>" % (name, value))
|
||||||
else:
|
else:
|
||||||
help_settings.append("<tt>%s = %s</tt>" % (name, str(value)))
|
help_settings.append("<tt>%s = %s</tt>" % (name, value))
|
||||||
help_text += help_settings
|
help_text += help_settings
|
||||||
help_texts[backend.get_name()] = '<br>'.join(help_text)
|
help_texts[backend.get_name()] = '<br>'.join(help_text)
|
||||||
return help_texts
|
return help_texts
|
||||||
|
@ -154,4 +154,3 @@ def message_user(request, logs):
|
||||||
else:
|
else:
|
||||||
msg = async_msg.format(url=url, async_url=async_url, async=async)
|
msg = async_msg.format(url=url, async_url=async_url, async=async)
|
||||||
messages.success(request, mark_safe(msg + '.'))
|
messages.success(request, mark_safe(msg + '.'))
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ def keep_log(execute, log, operations):
|
||||||
log.state = log.EXCEPTION
|
log.state = log.EXCEPTION
|
||||||
log.stderr = trace
|
log.stderr = trace
|
||||||
log.save()
|
log.save()
|
||||||
subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs))
|
subject = 'EXCEPTION executing backend(s) %s %s' % (args, kwargs)
|
||||||
logger.error(subject)
|
logger.error(subject)
|
||||||
logger.error(trace)
|
logger.error(trace)
|
||||||
mail_admins(subject, trace)
|
mail_admins(subject, trace)
|
||||||
|
@ -40,7 +40,7 @@ def keep_log(execute, log, operations):
|
||||||
finally:
|
finally:
|
||||||
# Store and log the operation
|
# Store and log the operation
|
||||||
for operation in operations:
|
for operation in operations:
|
||||||
logger.info("Executed %s" % str(operation))
|
logger.info("Executed %s" % operation)
|
||||||
operation.store(log)
|
operation.store(log)
|
||||||
if not log.is_success:
|
if not log.is_success:
|
||||||
send_report(execute, args, log)
|
send_report(execute, args, log)
|
||||||
|
@ -57,7 +57,7 @@ def generate(operations):
|
||||||
serialize = False
|
serialize = False
|
||||||
# Generate scripts per route+backend
|
# Generate scripts per route+backend
|
||||||
for operation in operations:
|
for operation in operations:
|
||||||
logger.debug("Queued %s" % str(operation))
|
logger.debug("Queued %s" % operation)
|
||||||
if operation.routes is None:
|
if operation.routes is None:
|
||||||
operation.routes = router.objects.get_for_operation(operation, cache=cache)
|
operation.routes = router.objects.get_for_operation(operation, cache=cache)
|
||||||
for route in operation.routes:
|
for route in operation.routes:
|
||||||
|
|
|
@ -107,14 +107,11 @@ def OpenSSH(backend, log, server, cmds, async=False):
|
||||||
log.save(update_fields=('script', 'state', 'updated_at'))
|
log.save(update_fields=('script', 'state', 'updated_at'))
|
||||||
if not cmds:
|
if not cmds:
|
||||||
return
|
return
|
||||||
channel = None
|
|
||||||
ssh = None
|
|
||||||
try:
|
try:
|
||||||
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
|
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
|
||||||
persist=True, async=async, silent=True)
|
persist=True, async=async, silent=True)
|
||||||
logger.debug('%s running on %s' % (backend, server))
|
logger.debug('%s running on %s' % (backend, server))
|
||||||
if async:
|
if async:
|
||||||
second = False
|
|
||||||
for state in ssh:
|
for state in ssh:
|
||||||
log.stdout += state.stdout.decode('utf8')
|
log.stdout += state.stdout.decode('utf8')
|
||||||
log.stderr += state.stderr.decode('utf8')
|
log.stderr += state.stderr.decode('utf8')
|
||||||
|
@ -148,7 +145,7 @@ def SSH(*args, **kwargs):
|
||||||
def Python(backend, log, server, cmds, async=False):
|
def Python(backend, log, server, cmds, async=False):
|
||||||
script = ''
|
script = ''
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
script += '# %s\n' % (str(cmd.func.__name__) + str(cmd.args))
|
script += '# %s %s\n' % (cmd.func.__name__, cmd.args)
|
||||||
script += textwrap.dedent(''.join(inspect.getsourcelines(cmd.func)[0]))
|
script += textwrap.dedent(''.join(inspect.getsourcelines(cmd.func)[0]))
|
||||||
log.state = log.STARTED
|
log.state = log.STARTED
|
||||||
log.script = '\n'.join((log.script, script))
|
log.script = '\n'.join((log.script, script))
|
||||||
|
@ -160,6 +157,8 @@ def Python(backend, log, server, cmds, async=False):
|
||||||
result = cmd(server)
|
result = cmd(server)
|
||||||
for line in stdout:
|
for line in stdout:
|
||||||
log.stdout += line + '\n'
|
log.stdout += line + '\n'
|
||||||
|
if result:
|
||||||
|
log.stdout += '# Result: %s\n' % result
|
||||||
if async:
|
if async:
|
||||||
log.save(update_fields=('stdout', 'updated_at'))
|
log.save(update_fields=('stdout', 'updated_at'))
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -27,7 +27,7 @@ class PaymentMethod(plugins.Plugin):
|
||||||
try:
|
try:
|
||||||
plugins.append(import_class(cls))
|
plugins.append(import_class(cls))
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
logger.error('Error loading %s: %s' % (cls, str(exc)))
|
logger.error('Error loading %s: %s' % (cls, exc))
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def get_label(self):
|
def get_label(self):
|
||||||
|
|
|
@ -298,4 +298,3 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
pretty_print=True,
|
pretty_print=True,
|
||||||
xml_declaration=True,
|
xml_declaration=True,
|
||||||
encoding='UTF-8')
|
encoding='UTF-8')
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.utils.translation import ungettext, ugettext, ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.contrib.settings import Setting
|
from orchestra.contrib.settings import Setting
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
|
@ -13,7 +13,7 @@ from django.shortcuts import redirect
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import insertattr, get_modeladmin, admin_link, admin_date
|
from orchestra.admin.utils import insertattr, get_modeladmin, admin_link, admin_date
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Resource(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{}-{}".format(str(self.content_type), self.name)
|
return "%s-%s" % (self.content_type, self.name)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def aggregation_class(self):
|
def aggregation_class(self):
|
||||||
|
@ -122,7 +122,7 @@ class Resource(models.Model):
|
||||||
|
|
||||||
def sync_periodic_task(self, delete=False):
|
def sync_periodic_task(self, delete=False):
|
||||||
""" sync periodic task on save/delete resource operations """
|
""" sync periodic task on save/delete resource operations """
|
||||||
name = 'monitor.%s' % str(self)
|
name = 'monitor.%s' % self
|
||||||
if delete or not self.crontab or not self.is_active:
|
if delete or not self.crontab or not self.is_active:
|
||||||
PeriodicTask.objects.filter(name=name).delete()
|
PeriodicTask.objects.filter(name=name).delete()
|
||||||
elif self.pk:
|
elif self.pk:
|
||||||
|
@ -196,7 +196,7 @@ class ResourceData(models.Model):
|
||||||
verbose_name_plural = _("resource data")
|
verbose_name_plural = _("resource data")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s: %s" % (str(self.resource), str(self.content_object))
|
return "%s: %s" % (self.resource, self.content_object)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit(self):
|
def unit(self):
|
||||||
|
|
|
@ -7,5 +7,5 @@ def validate_scale(value):
|
||||||
int(eval(value))
|
int(eval(value))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("'%s' is not a valid scale expression. (%s)") % (value, str(e))
|
_("'%s' is not a valid scale expression. (%s)") % (value, e)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import os
|
|
||||||
import textwrap
|
import textwrap
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.contrib.orchestration import ServiceController, replace
|
from orchestra.contrib.orchestration import ServiceController
|
||||||
|
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,8 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
'adminemail': saas.account.username,
|
'adminemail': saas.account.username,
|
||||||
'adminpassword': saas.password,
|
'adminpassword': saas.password,
|
||||||
}
|
}
|
||||||
response = requests.post(install_link, data=post, verify=settings.SAAS_PHPLIST_VERIFY_SSL)
|
response = requests.post(
|
||||||
|
install_link, data=post, verify=settings.SAAS_PHPLIST_VERIFY_SSL)
|
||||||
sys.stdout.write(response.content.decode('utf8')+'\n')
|
sys.stdout.write(response.content.decode('utf8')+'\n')
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
self.error("Bad status code %i." % response.status_code)
|
self.error("Bad status code %i." % response.status_code)
|
||||||
|
|
|
@ -18,4 +18,3 @@ class CustomURLListFilter(SimpleListFilter):
|
||||||
elif self.value() == 'False':
|
elif self.value() == 'False':
|
||||||
return queryset.filter(custom_url='')
|
return queryset.filter(custom_url='')
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from .options import SoftwareService
|
||||||
|
|
||||||
|
|
||||||
class BSCWForm(SaaSPasswordForm):
|
class BSCWForm(SaaSPasswordForm):
|
||||||
email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'}))
|
email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size': '40'}))
|
||||||
|
|
||||||
|
|
||||||
class BSCWDataSerializer(serializers.Serializer):
|
class BSCWDataSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -33,4 +33,3 @@ class GitLabService(SoftwareService):
|
||||||
change_readonly_fileds = ('email', 'user_id',)
|
change_readonly_fileds = ('email', 'user_id',)
|
||||||
verbose_name = "GitLab"
|
verbose_name = "GitLab"
|
||||||
icon = 'orchestra/icons/apps/gitlab.png'
|
icon = 'orchestra/icons/apps/gitlab.png'
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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.utils import normurlpath
|
|
||||||
from orchestra.contrib.websites.validators import validate_domain_protocol
|
from orchestra.contrib.websites.validators import validate_domain_protocol
|
||||||
from orchestra.utils.python import AttrDict
|
from orchestra.utils.python import AttrDict
|
||||||
|
|
||||||
|
@ -55,7 +54,7 @@ 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)
|
website = Website(name=url.netloc, protocol=protocol, account=account)
|
||||||
full_clean(website)
|
full_clean(website)
|
||||||
try:
|
try:
|
||||||
validate_domain_protocol(website, domain, protocol)
|
validate_domain_protocol(website, domain, protocol)
|
||||||
|
@ -78,7 +77,8 @@ def clean_custom_url(saas):
|
||||||
Content.objects.filter(website=website).values_list('path', flat=True)
|
Content.objects.filter(website=website).values_list('path', flat=True)
|
||||||
)
|
)
|
||||||
values = defaultdict(list)
|
values = defaultdict(list)
|
||||||
for wdirective in WebsiteDirective.objects.filter(website=website).exclude(pk=directive.pk):
|
directives = WebsiteDirective.objects.filter(website=website)
|
||||||
|
for wdirective in directives.exclude(pk=directive.pk):
|
||||||
fdirective = AttrDict({
|
fdirective = AttrDict({
|
||||||
'name': wdirective.name,
|
'name': wdirective.name,
|
||||||
'value': wdirective.value
|
'value': wdirective.value
|
||||||
|
@ -110,7 +110,7 @@ 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)
|
website = Website(name=url.netloc, protocol=protocol, account=account)
|
||||||
website.save()
|
website.save()
|
||||||
website.domains.add(domain)
|
website.domains.add(domain)
|
||||||
# get or create directive
|
# get or create directive
|
||||||
|
|
|
@ -261,7 +261,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
elif len(dates) == 1:
|
elif len(dates) == 1:
|
||||||
ini, end = dates[0], dates[0]
|
ini, end = dates[0], dates[0]
|
||||||
else:
|
else:
|
||||||
raise AttributeError("WTF is '%s'?" % str(dates))
|
raise AttributeError("WTF is '%s'?" % dates)
|
||||||
discounts = discounts or ()
|
discounts = discounts or ()
|
||||||
|
|
||||||
size = self.get_price_size(ini, end)
|
size = self.get_price_size(ini, end)
|
||||||
|
|
|
@ -45,10 +45,10 @@ class Setting(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_choices(cls, value):
|
def validate_choices(cls, value):
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
raise ValidationError("%s is not a valid choices." % str(value))
|
raise ValidationError("%s is not a valid choices." % value)
|
||||||
for choice in value:
|
for choice in value:
|
||||||
if not isinstance(choice, (list, tuple)) or len(choice) != 2:
|
if not isinstance(choice, (list, tuple)) or len(choice) != 2:
|
||||||
raise ValidationError("%s is not a valid choice." % str(choice))
|
raise ValidationError("%s is not a valid choice." % choice)
|
||||||
value, verbose = choice
|
value, verbose = choice
|
||||||
if not isinstance(verbose, (str, Promise)):
|
if not isinstance(verbose, (str, Promise)):
|
||||||
raise ValidationError("%s is not a valid verbose name." % value)
|
raise ValidationError("%s is not a valid verbose name." % value)
|
||||||
|
|
|
@ -2,7 +2,6 @@ from django.contrib import admin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
from orchestra.admin.actions import disable
|
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
|
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
|
||||||
from orchestra.contrib.accounts.filters import IsActiveListFilter
|
from orchestra.contrib.accounts.filters import IsActiveListFilter
|
||||||
|
|
|
@ -49,7 +49,7 @@ class SystemUserFormMixin(object):
|
||||||
} else {
|
} else {
|
||||||
field.removeClass("hidden");
|
field.removeClass("hidden");
|
||||||
input.removeAttr("type");
|
input.removeAttr("type");
|
||||||
};""" % str(list(settings.SYSTEMUSERS_DISABLED_SHELLS))
|
};""" % list(settings.SYSTEMUSERS_DISABLED_SHELLS)
|
||||||
)
|
)
|
||||||
self.fields['home'].widget.attrs['onChange'] = textwrap.dedent("""\
|
self.fields['home'].widget.attrs['onChange'] = textwrap.dedent("""\
|
||||||
field = $(".field-box.field-directory");
|
field = $(".field-box.field-directory");
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .decorators import task, periodic_task, keep_state, apply_async
|
from .decorators import task, periodic_task, keep_state, apply_async
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,14 @@ def keep_state(fn):
|
||||||
_task_id = get_id()
|
_task_id = get_id()
|
||||||
if _name is None:
|
if _name is None:
|
||||||
_name = get_name(fn)
|
_name = get_name(fn)
|
||||||
state = TaskState.objects.create(state=states.STARTED, task_id=_task_id, name=_name, args=str(args),
|
state = TaskState.objects.create(
|
||||||
kwargs=str(kwargs), tstamp=now)
|
state=states.STARTED, task_id=_task_id, name=_name,
|
||||||
|
args=str(args), kwargs=str(kwargs), tstamp=now)
|
||||||
try:
|
try:
|
||||||
result = fn(*args, **kwargs)
|
result = fn(*args, **kwargs)
|
||||||
except:
|
except:
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (_name, str(args), str(kwargs))
|
subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (_name, args, kwargs)
|
||||||
logger.error(subject)
|
logger.error(subject)
|
||||||
logger.error(trace)
|
logger.error(trace)
|
||||||
state.state = states.FAILURE
|
state.state = states.FAILURE
|
||||||
|
|
|
@ -116,4 +116,3 @@
|
||||||
# 'Invalid beginning range: {0} < {1}.'.format(i, self.min_))
|
# 'Invalid beginning range: {0} < {1}.'.format(i, self.min_))
|
||||||
|
|
||||||
# return i
|
# return i
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from django.conf.urls import url
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
|
|
|
@ -8,6 +8,6 @@ class WebAppsConfig(AppConfig):
|
||||||
verbose_name = 'Webapps'
|
verbose_name = 'Webapps'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals
|
|
||||||
from .models import WebApp
|
from .models import WebApp
|
||||||
services.register(WebApp, icon='Applications-other.png')
|
services.register(WebApp, icon='Applications-other.png')
|
||||||
|
from . import signals
|
||||||
|
|
|
@ -59,7 +59,7 @@ class PHPAppOption(AppOption):
|
||||||
php_version = self.instance.webapp.type_instance.get_php_version_number()
|
php_version = self.instance.webapp.type_instance.get_php_version_number()
|
||||||
if php_version and self.deprecated and float(php_version) > self.deprecated:
|
if php_version and self.deprecated and float(php_version) > self.deprecated:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("This option is deprecated since PHP version %s.") % str(self.deprecated)
|
_("This option is deprecated since PHP version %s.") % self.deprecated
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -80,4 +80,3 @@ class AppType(plugins.Plugin):
|
||||||
'user': self.instance.account.username,
|
'user': self.instance.account.username,
|
||||||
'home': self.instance.account.main_systemuser.get_home(),
|
'home': self.instance.account.main_systemuser.get_home(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,3 @@ def site(request):
|
||||||
'ORCHESTRA_SITE_NAME': settings.ORCHESTRA_SITE_NAME,
|
'ORCHESTRA_SITE_NAME': settings.ORCHESTRA_SITE_NAME,
|
||||||
'ORCHESTRA_SITE_VERBOSE_NAME': settings.ORCHESTRA_SITE_VERBOSE_NAME
|
'ORCHESTRA_SITE_VERBOSE_NAME': settings.ORCHESTRA_SITE_VERBOSE_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class SpanWidget(forms.Widget):
|
||||||
# Display icon
|
# Display icon
|
||||||
if isinstance(original, bool):
|
if isinstance(original, bool):
|
||||||
icon = static('admin/img/icon-%s.gif' % ('yes' if original else 'no',))
|
icon = static('admin/img/icon-%s.gif' % ('yes' if original else 'no',))
|
||||||
return mark_safe('<img src="%s" alt="%s">' % (icon, str(display)))
|
return mark_safe('<img src="%s" alt="%s">' % (icon, display))
|
||||||
tag = self.tag[:-1]
|
tag = self.tag[:-1]
|
||||||
endtag = '/'.join((self.tag[0], self.tag[1:]))
|
endtag = '/'.join((self.tag[0], self.tag[1:]))
|
||||||
return mark_safe('%s%s >%s%s' % (tag, forms.utils.flatatt(final_attrs), display, endtag))
|
return mark_safe('%s%s >%s%s' % (tag, forms.utils.flatatt(final_attrs), display, endtag))
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.db.models.fields.files import FileField, FieldFile
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
|
||||||
from ..forms.fields import MultiSelectFormField
|
from ..forms.fields import MultiSelectFormField
|
||||||
from ..utils.apps import isinstalled
|
|
||||||
|
|
||||||
|
|
||||||
class MultiSelectField(models.CharField, metaclass=models.SubfieldBase):
|
class MultiSelectField(models.CharField, metaclass=models.SubfieldBase):
|
||||||
|
|
|
@ -104,4 +104,3 @@ class RelatedPermission(Permission):
|
||||||
setattr(call, name, func)
|
setattr(call, name, func)
|
||||||
|
|
||||||
return call
|
return call
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@ class SelectPluginAdminMixin(object):
|
||||||
def display_plugin_field(field_name):
|
def display_plugin_field(field_name):
|
||||||
def inner(modeladmin, obj, field_name=field_name):
|
def inner(modeladmin, obj, field_name=field_name):
|
||||||
try:
|
try:
|
||||||
plugin_class = getattr(obj, '%s_class' % field_name)
|
getattr(obj, '%s_class' % field_name)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
value = getattr(obj, field_name)
|
value = getattr(obj, field_name)
|
||||||
return "<span style='color:red;' title='Not available'>%s</span>" % value
|
return "<span style='color:red;' title='Not available'>%s</span>" % value
|
||||||
|
|
|
@ -8,4 +8,3 @@ register = template.Library()
|
||||||
@register.filter(name='markdown')
|
@register.filter(name='markdown')
|
||||||
def do_markdown(text):
|
def do_markdown(text):
|
||||||
return markdown(text)
|
return markdown(text)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
|
|
|
@ -21,4 +21,3 @@ def remove_app(INSTALLED_APPS, app):
|
||||||
apps.remove(app)
|
apps.remove(app)
|
||||||
return tuple(apps)
|
return tuple(apps)
|
||||||
return INSTALLED_APPS
|
return INSTALLED_APPS
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ def close_connection(execute):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
log = execute(*args, **kwargs)
|
log = execute(*args, **kwargs)
|
||||||
except Exception as e:
|
except:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
wrapper.log = log
|
wrapper.log = log
|
||||||
|
|
|
@ -38,4 +38,3 @@ def send_email_template(template, context, to, email_from=None, html=None, attac
|
||||||
subject, html_message = render_email_template(html, context)
|
subject, html_message = render_email_template(html, context)
|
||||||
msg.attach_alternative(html_message, "text/html")
|
msg.attach_alternative(html_message, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
|
|
9
setup.py
9
setup.py
|
@ -1,4 +1,5 @@
|
||||||
import os, sys
|
import os
|
||||||
|
import sys
|
||||||
from distutils.sysconfig import get_python_lib
|
from distutils.sysconfig import get_python_lib
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ setup(
|
||||||
version = version,
|
version = version,
|
||||||
author = "Marc Aymerich",
|
author = "Marc Aymerich",
|
||||||
author_email = "marcay@pangea.org",
|
author_email = "marcay@pangea.org",
|
||||||
url = "http://orchestra.pangea.org",
|
url = "https://github.com/glic3rinu/django-orchestra",
|
||||||
license = "GPLv3",
|
license = "GPLv3",
|
||||||
description = "A framework for building web hosting control panels",
|
description = "A framework for building web hosting control panels",
|
||||||
long_description = (
|
long_description = (
|
||||||
|
@ -40,8 +41,8 @@ setup(
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: POSIX :: Linux',
|
'Operating System :: POSIX :: Linux',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2.6',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Topic :: Internet :: WWW/HTTP',
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||||
|
|
Loading…
Reference in New Issue