Random fixes
This commit is contained in:
parent
882c03a416
commit
124124da6c
49
TODO.md
49
TODO.md
|
@ -203,39 +203,32 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
|
||||||
|
|
||||||
* normurlpath '' return '/'
|
* normurlpath '' return '/'
|
||||||
|
|
||||||
* initial configuration of multisite sas apps with password stored in DATA ?? Dsign decission: initial pwds vs eventual consistency vs externa service vs backend raise exception?
|
* more robust backend error handling, continue executing but exit code > 0 if failure: failing_cmd || exit_code=1 and don't forget to call super.commit()!!
|
||||||
|
|
||||||
* webapps installation complete, passowrd protected
|
|
||||||
* saas.initial_password autogenerated (ok because its random and not user provided) vs saas.password /change_Form provided + send email with initial_password
|
|
||||||
|
|
||||||
* more robust backend error handling, continue executing but exit code > 0 if failure, replace exit_code=0; do_sometging || exit_code=1
|
|
||||||
|
|
||||||
* automaitcally set passwords and email users?
|
|
||||||
|
|
||||||
* website directives uniquenes validation on serializers
|
* website directives uniquenes validation on serializers
|
||||||
|
|
||||||
+ is_Active custom filter with support for instance.account.is_Active
|
+ is_Active custom filter with support for instance.account.is_Active annotate with F() needed (django 1.8)
|
||||||
|
|
||||||
* django virtual field for saas and webapps related objects (db) to show on delete confirmation
|
* delete apache logs and php logs
|
||||||
if only extra related objects are databases and user databases why not make them first class relations?????
|
|
||||||
* >>> Account._meta.virtual_fields[0].bulk_related_objects([Account.objects.all()[0]])
|
|
||||||
[<ResourceData: account-disk: entrep>, <ResourceData: account-traffic: entrep>]
|
|
||||||
https://github.com/django/django/blob/master/django/db/models/deletion.py#L232
|
|
||||||
https://github.com/django/django/blob/master/django/contrib/contenttypes/fields.py#L282
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
* document service help things: discount/refound/compensation effect and metric table
|
||||||
from django.db import DEFAULT_DB_ALIAS
|
* Document metric interpretation help_text
|
||||||
from orchestra.apps.databases.models import Database
|
* document plugin serialization, data_serializer?
|
||||||
class VirtualRelation(GenericRelation):
|
|
||||||
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
|
|
||||||
return []
|
|
||||||
# return Database.objects.filter(name__in=
|
|
||||||
# obj.service_instance.get_related() for obj in objs
|
|
||||||
## return self.remote_field.model._base_manager.db_manager(using).all()
|
|
||||||
relation = VirtualRelation('databases.Database')
|
|
||||||
SaaS.add_to_class('databases', relation)
|
|
||||||
|
|
||||||
* one to one relation deleteion on both sides??
|
* bill line managemente, remove, undo (only when possible), move, copy, paste
|
||||||
|
* budgets: no undo feature
|
||||||
|
|
||||||
|
* Autocomplete admin fields like <site_name>.phplist... with js
|
||||||
|
* autoexpand mailbox.filter according to filtering options
|
||||||
|
|
||||||
* webapps/saas delete related db by id not name !! type!=Mysql
|
* allow empty metric pack for default rates? changes on rating algo
|
||||||
|
* rates plan verbose name!"!
|
||||||
|
* IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0
|
||||||
|
* IMPORTANT maildis updae and metric storage ?? threshold ? or what?
|
||||||
|
|
||||||
|
* Improve performance of admin change lists with debug toolbar and prefech_related
|
||||||
|
* and miscellaneous.service.name == 'domini-registre'
|
||||||
|
* DOMINI REGISTRE MIGRATION SCRIPTS
|
||||||
|
|
||||||
|
* detect subdomains accounts correctly with subdomains: i.e. www.marcay.pangea.org
|
||||||
|
* lines too long on invoice, double lines or cut
|
||||||
|
|
|
@ -111,7 +111,7 @@ class ChangeAddFieldsMixin(object):
|
||||||
add_form = None
|
add_form = None
|
||||||
add_prepopulated_fields = {}
|
add_prepopulated_fields = {}
|
||||||
change_readonly_fields = ()
|
change_readonly_fields = ()
|
||||||
add_inlines = ()
|
add_inlines = None
|
||||||
|
|
||||||
def get_prepopulated_fields(self, request, obj=None):
|
def get_prepopulated_fields(self, request, obj=None):
|
||||||
if not obj:
|
if not obj:
|
||||||
|
@ -140,7 +140,7 @@ class ChangeAddFieldsMixin(object):
|
||||||
if obj:
|
if obj:
|
||||||
self.inlines = type(self).inlines
|
self.inlines = type(self).inlines
|
||||||
else:
|
else:
|
||||||
self.inlines = self.add_inlines or self.inlines
|
self.inlines = self.inlines if self.add_inlines is None else self.add_inlines
|
||||||
inlines = super(ChangeAddFieldsMixin, self).get_inline_instances(request, obj)
|
inlines = super(ChangeAddFieldsMixin, self).get_inline_instances(request, obj)
|
||||||
for inline in inlines:
|
for inline in inlines:
|
||||||
inline.parent_object = obj
|
inline.parent_object = obj
|
||||||
|
|
|
@ -172,10 +172,11 @@ class AccountAdminMixin(object):
|
||||||
account_link.allow_tags = True
|
account_link.allow_tags = True
|
||||||
account_link.admin_order_field = 'account__username'
|
account_link.admin_order_field = 'account__username'
|
||||||
|
|
||||||
def render_change_form(self, request, context, *args, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
""" Warns user when object's account is disabled """
|
""" Warns user when object's account is disabled """
|
||||||
|
form = super(AccountAdminMixin, self).get_form(request, obj, **kwargs)
|
||||||
try:
|
try:
|
||||||
field = context['adminform'].form.fields['is_active']
|
field = form.base_fields['is_active']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -183,11 +184,10 @@ class AccountAdminMixin(object):
|
||||||
"Designates whether this account should be treated as active. "
|
"Designates whether this account should be treated as active. "
|
||||||
"Unselect this instead of deleting accounts."
|
"Unselect this instead of deleting accounts."
|
||||||
)
|
)
|
||||||
obj = kwargs.get('obj')
|
|
||||||
if obj and not obj.account.is_active:
|
if obj and not obj.account.is_active:
|
||||||
help_text += "<br><b style='color:red;'>This user's account is dissabled</b>"
|
help_text += "<br><b style='color:red;'>This user's account is dissabled</b>"
|
||||||
field.help_text = _(help_text)
|
field.help_text = _(help_text)
|
||||||
return super(AccountAdminMixin, self).render_change_form(request, context, *args, **kwargs)
|
return form
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
""" remove account or account_link depending on the case """
|
""" remove account or account_link depending on the case """
|
||||||
|
|
|
@ -42,8 +42,8 @@ class MySQLBackend(ServiceController):
|
||||||
self.append("mysql mysql -e 'DELETE FROM db WHERE db = \"%(database)s\";'" % context)
|
self.append("mysql mysql -e 'DELETE FROM db WHERE db = \"%(database)s\";'" % context)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
super(MySQLBackend, self).commit()
|
|
||||||
self.append("mysql -e 'FLUSH PRIVILEGES;'")
|
self.append("mysql -e 'FLUSH PRIVILEGES;'")
|
||||||
|
super(MySQLBackend, self).commit()
|
||||||
|
|
||||||
def get_context(self, database):
|
def get_context(self, database):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -37,6 +37,9 @@ DOMAINS_CHECKZONE_BIN_PATH = getattr(settings, 'DOMAINS_CHECKZONE_BIN_PATH',
|
||||||
'/usr/sbin/named-checkzone -i local -k fail -n fail')
|
'/usr/sbin/named-checkzone -i local -k fail -n fail')
|
||||||
|
|
||||||
|
|
||||||
|
DOMAINS_CHECKZONE_PATH = getattr(settings, 'DOMAINS_CHECKZONE_PATH', '/dev/shm')
|
||||||
|
|
||||||
|
|
||||||
DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A', '10.0.3.13')
|
DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A', '10.0.3.13')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -108,9 +108,12 @@ def validate_soa_record(value):
|
||||||
def validate_zone(zone):
|
def validate_zone(zone):
|
||||||
""" Ultimate zone file validation using named-checkzone """
|
""" Ultimate zone file validation using named-checkzone """
|
||||||
zone_name = zone.split()[0][:-1]
|
zone_name = zone.split()[0][:-1]
|
||||||
|
path = os.path.join(settings.DOMAINS_CHECKZONE_PATH, zone_name)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(zone)
|
||||||
|
# Don't use /dev/stdin becuase the 'argument list is too long' error
|
||||||
checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH
|
checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH
|
||||||
cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin'])
|
check = run(' '.join([checkzone, zone_name, path]), error_codes=[0,1], display=False)
|
||||||
check = run(cmd, error_codes=[0, 1], display=False)
|
|
||||||
if check.return_code == 1:
|
if check.return_code == 1:
|
||||||
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1]
|
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1]
|
||||||
raise ValidationError(', '.join(errors))
|
raise ValidationError(', '.join(errors))
|
||||||
|
|
|
@ -72,7 +72,7 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA
|
||||||
|
|
||||||
def get_service(self, obj):
|
def get_service(self, obj):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self.plugin.get_plugin(self.plugin_value)().instance
|
return self.plugin.get_plugin(self.plugin_value).related_instance
|
||||||
else:
|
else:
|
||||||
return obj.service
|
return obj.service
|
||||||
|
|
||||||
|
@ -106,6 +106,14 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
||||||
return super(MiscellaneousAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(MiscellaneousAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change:
|
||||||
|
plugin = self.plugin
|
||||||
|
kwargs = {
|
||||||
|
plugin.name_field: self.plugin_value
|
||||||
|
}
|
||||||
|
setattr(obj, self.plugin_field, plugin.model.objects.get(**kwargs))
|
||||||
|
obj.save()
|
||||||
|
|
||||||
admin.site.register(MiscService, MiscServiceAdmin)
|
admin.site.register(MiscService, MiscServiceAdmin)
|
||||||
admin.site.register(Miscellaneous, MiscellaneousAdmin)
|
admin.site.register(Miscellaneous, MiscellaneousAdmin)
|
||||||
|
|
|
@ -63,6 +63,10 @@ class Miscellaneous(models.Model):
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
|
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def service_class(self):
|
||||||
|
return self.service
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.identifier:
|
if self.identifier:
|
||||||
self.identifier = self.identifier.strip()
|
self.identifier = self.identifier.strip()
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ChangeListDefaultFilter
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_link, admin_date
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils.humanize import naturaldate
|
from orchestra.utils.humanize import naturaldate
|
||||||
|
@ -13,17 +15,50 @@ from .filters import IgnoreOrderListFilter, ActiveOrderListFilter, BilledOrderLi
|
||||||
from .models import Order, MetricStorage
|
from .models import Order, MetricStorage
|
||||||
|
|
||||||
|
|
||||||
class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
|
class MetricStorageInline(admin.TabularInline):
|
||||||
|
model = MetricStorage
|
||||||
|
readonly_fields = ('value', 'updated_on')
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
def has_add_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_fieldsets(self, request, obj=None):
|
||||||
|
if obj:
|
||||||
|
url = reverse('admin:orders_metricstorage_changelist')
|
||||||
|
url += '?order=%i' % obj.pk
|
||||||
|
title = _('Metric storage, last 10 entries, <a href="%s">(See all)</a>')
|
||||||
|
self.verbose_name_plural = mark_safe(title % url)
|
||||||
|
return super(MetricStorageInline, self).get_fieldsets(request, obj)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(MetricStorageInline, self).get_queryset(request)
|
||||||
|
if self.parent_object and self.parent_object.pk:
|
||||||
|
qs = qs.filter(order=self.parent_object.pk).order_by('-id')
|
||||||
|
try:
|
||||||
|
tenth_id = qs.values_list('id', flat=True)[10]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return qs.filter(pk__lte=tenth_id)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'service_link', 'account_link', 'content_object_link',
|
'id', 'service_link', 'account_link', 'content_object_link',
|
||||||
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
'display_registered_on', 'display_billed_until', 'display_cancelled_on', 'display_metric'
|
||||||
)
|
)
|
||||||
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',)
|
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, IgnoreOrderListFilter, 'service',)
|
||||||
default_changelist_filters = (
|
default_changelist_filters = (
|
||||||
('ignore', '0'),
|
('ignore', '0'),
|
||||||
)
|
)
|
||||||
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
|
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
|
||||||
|
change_view_actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
|
||||||
date_hierarchy = 'registered_on'
|
date_hierarchy = 'registered_on'
|
||||||
|
inlines = (MetricStorageInline,)
|
||||||
|
add_inlines = ()
|
||||||
|
search_fields = ('account__username', 'description')
|
||||||
|
|
||||||
service_link = admin_link('service')
|
service_link = admin_link('service')
|
||||||
content_object_link = admin_link('content_object', order=False)
|
content_object_link = admin_link('content_object', order=False)
|
||||||
|
@ -42,6 +77,11 @@ class OrderAdmin(ChangeListDefaultFilter, AccountAdminMixin, admin.ModelAdmin):
|
||||||
display_billed_until.allow_tags = True
|
display_billed_until.allow_tags = True
|
||||||
display_billed_until.admin_order_field = 'billed_until'
|
display_billed_until.admin_order_field = 'billed_until'
|
||||||
|
|
||||||
|
def display_metric(self, order):
|
||||||
|
metric = order.metrics.latest()
|
||||||
|
return metric.value if metric else ''
|
||||||
|
display_metric.short_description = _("Metric")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(OrderAdmin, self).get_queryset(request)
|
qs = super(OrderAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('service').prefetch_related('content_object')
|
return qs.select_related('service').prefetch_related('content_object')
|
||||||
|
|
|
@ -29,8 +29,8 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
|
||||||
|
|
||||||
def selected_related_choices(queryset):
|
def selected_related_choices(queryset):
|
||||||
for order in queryset:
|
for order in queryset:
|
||||||
verbose = '<a href="{order_url}">{description}</a> '
|
verbose = u'<a href="{order_url}">{description}</a> '
|
||||||
verbose += '<a class="account" href="{account_url}">{account}</a>'
|
verbose += u'<a class="account" href="{account_url}">{account}</a>'
|
||||||
verbose = verbose.format(
|
verbose = verbose.format(
|
||||||
order_url=change_url(order), description=order.description,
|
order_url=change_url(order), description=order.description,
|
||||||
account_url=change_url(order.account), account=str(order.account)
|
account_url=change_url(order.account), account=str(order.account)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import decimal
|
||||||
|
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services, accounts
|
from orchestra.core import services, accounts
|
||||||
|
@ -22,7 +23,7 @@ class Plan(models.Model):
|
||||||
help_text=_("Designates whether this plan allow for multiple contractions."))
|
help_text=_("Designates whether this plan allow for multiple contractions."))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.get_verbose_name()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.verbose_name = self.verbose_name.strip()
|
self.verbose_name = self.verbose_name.strip()
|
||||||
|
@ -43,7 +44,7 @@ class ContractedPlan(models.Model):
|
||||||
return str(self.plan)
|
return str(self.plan)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.pk and not self.plan.allow_multiples:
|
if not self.pk and not self.plan.allow_multiple:
|
||||||
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
|
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
|
||||||
raise ValidationError("A contracted plan for this account already exists.")
|
raise ValidationError("A contracted plan for this account already exists.")
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
raise RuntimeError("Database is not yet configured")
|
raise RuntimeError("Database is not yet configured")
|
||||||
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
|
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
|
||||||
if install:
|
if install:
|
||||||
if not saas.password:
|
if not hasattr(saas, 'password'):
|
||||||
raise RuntimeError("Password is missing")
|
raise RuntimeError("Password is missing")
|
||||||
install = install.groups()[0]
|
install = install.groups()[0]
|
||||||
install_link = admin_link + install[1:]
|
install_link = admin_link + install[1:]
|
||||||
|
@ -38,7 +38,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
print response.content
|
print response.content
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise RuntimeError("Bad status code %i" % response.status_code)
|
raise RuntimeError("Bad status code %i" % response.status_code)
|
||||||
elif saas.password:
|
elif hasattr(saas, 'password'):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def save(self, saas):
|
def save(self, saas):
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
|
|
||||||
|
from orchestra.apps.databases.models import Database
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualDatabaseRelation(GenericRelation):
|
||||||
|
""" Delete related databases if any """
|
||||||
|
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
|
||||||
|
pks = []
|
||||||
|
for obj in objs:
|
||||||
|
if obj.database_id:
|
||||||
|
pks.append(obj.database_id)
|
||||||
|
if not pks:
|
||||||
|
return []
|
||||||
|
# TODO renamed to self.remote_field in django 1.8
|
||||||
|
return self.rel.to._base_manager.db_manager(using).filter(pk__in=pks)
|
|
@ -8,6 +8,7 @@ from jsonfield import JSONField
|
||||||
from orchestra.core import services, validators
|
from orchestra.core import services, validators
|
||||||
from orchestra.models.fields import NullableCharField
|
from orchestra.models.fields import NullableCharField
|
||||||
|
|
||||||
|
from .fields import VirtualDatabaseRelation
|
||||||
from .services import SoftwareService
|
from .services import SoftwareService
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +24,10 @@ class SaaS(models.Model):
|
||||||
help_text=_("Designates whether this service should be treated as active. "))
|
help_text=_("Designates whether this service should be treated as active. "))
|
||||||
data = JSONField(_("data"), default={},
|
data = JSONField(_("data"), default={},
|
||||||
help_text=_("Extra information dependent of each service."))
|
help_text=_("Extra information dependent of each service."))
|
||||||
|
database = models.ForeignKey('databases.Database', null=True, blank=True)
|
||||||
|
|
||||||
|
# Some SaaS sites may need a database, with this virtual field we tell the ORM to delete them
|
||||||
|
databases = VirtualDatabaseRelation('databases.Database')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "SaaS"
|
verbose_name = "SaaS"
|
||||||
|
|
|
@ -3,13 +3,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.plugins.forms import PluginDataForm
|
from orchestra.plugins.forms import PluginDataForm
|
||||||
|
|
||||||
from .options import SoftwareService
|
from .options import SoftwareService, SoftwareServiceForm
|
||||||
|
|
||||||
|
|
||||||
class MoodleForm(PluginDataForm):
|
class MoodleForm(SoftwareServiceForm):
|
||||||
username = forms.CharField(label=_("Username"), max_length=64)
|
|
||||||
password = forms.CharField(label=_("Password"), max_length=64)
|
|
||||||
site_name = forms.CharField(label=_("Site name"), max_length=64)
|
|
||||||
email = forms.EmailField(label=_("Email"))
|
email = forms.EmailField(label=_("Email"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,9 @@ class SoftwareServiceForm(PluginDataForm):
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
help_text=_("Enter the same password as above, for verification."))
|
help_text=_("Enter the same password as above, for verification."))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
exclude = ('database',)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SoftwareServiceForm, self).__init__(*args, **kwargs)
|
super(SoftwareServiceForm, self).__init__(*args, **kwargs)
|
||||||
self.is_change = bool(self.instance and self.instance.pk)
|
self.is_change = bool(self.instance and self.instance.pk)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
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
|
||||||
|
@ -24,7 +25,7 @@ class PHPListForm(SoftwareServiceForm):
|
||||||
|
|
||||||
|
|
||||||
class PHPListChangeForm(PHPListForm):
|
class PHPListChangeForm(PHPListForm):
|
||||||
db_name = forms.CharField(label=_("Database name"),
|
database = forms.CharField(label=_("Database"), required=False,
|
||||||
help_text=_("Database used for this webapp."))
|
help_text=_("Database used for this webapp."))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -33,10 +34,11 @@ class PHPListChangeForm(PHPListForm):
|
||||||
admin_url = "http://%s/admin/" % site_domain
|
admin_url = "http://%s/admin/" % site_domain
|
||||||
help_text = _("Admin URL <a href={0}>{0}</a>").format(admin_url)
|
help_text = _("Admin URL <a href={0}>{0}</a>").format(admin_url)
|
||||||
self.fields['site_url'].help_text = help_text
|
self.fields['site_url'].help_text = help_text
|
||||||
|
# DB link
|
||||||
|
db = self.instance.database
|
||||||
class PHPListSerializer(serializers.Serializer):
|
db_url = reverse('admin:databases_database_change', args=(db.pk,))
|
||||||
db_name = serializers.CharField(label=_("Database name"), required=False)
|
db_link = mark_safe('<a href="%s">%s</a>' % (db_url, db.name))
|
||||||
|
self.fields['database'].widget = widgets.ReadOnlyWidget(db.name, db_link)
|
||||||
|
|
||||||
|
|
||||||
class PHPListService(SoftwareService):
|
class PHPListService(SoftwareService):
|
||||||
|
@ -44,8 +46,6 @@ class PHPListService(SoftwareService):
|
||||||
verbose_name = "phpList"
|
verbose_name = "phpList"
|
||||||
form = PHPListForm
|
form = PHPListForm
|
||||||
change_form = PHPListChangeForm
|
change_form = PHPListChangeForm
|
||||||
change_readonly_fileds = ('db_name',)
|
|
||||||
serializer = PHPListSerializer
|
|
||||||
icon = 'orchestra/icons/apps/Phplist.png'
|
icon = 'orchestra/icons/apps/Phplist.png'
|
||||||
site_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
|
site_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
|
||||||
|
|
||||||
|
@ -77,29 +77,7 @@ class PHPListService(SoftwareService):
|
||||||
db_name = self.get_db_name()
|
db_name = self.get_db_name()
|
||||||
db_user = self.get_db_user()
|
db_user = self.get_db_user()
|
||||||
account = self.get_account()
|
account = self.get_account()
|
||||||
db, db_created = account.databases.get_or_create(name=db_name)
|
db, db_created = account.databases.get_or_create(name=db_name, type=Database.MYSQL)
|
||||||
user = DatabaseUser.objects.get(username=db_user)
|
user = DatabaseUser.objects.get(username=db_user)
|
||||||
db.users.add(user)
|
db.users.add(user)
|
||||||
self.instance.data = {
|
self.instance.database_id = db.pk
|
||||||
'db_name': db_name,
|
|
||||||
}
|
|
||||||
if not db_created:
|
|
||||||
# Trigger related backends
|
|
||||||
for related in self.get_related():
|
|
||||||
related.save(update_fields=[])
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
for related in self.get_related():
|
|
||||||
related.delete()
|
|
||||||
|
|
||||||
def get_related(self):
|
|
||||||
related = []
|
|
||||||
account = self.get_account()
|
|
||||||
db_name = self.instance.data.get('db_name')
|
|
||||||
try:
|
|
||||||
db = account.databases.get(name=db_name)
|
|
||||||
except Database.DoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
related.append(db)
|
|
||||||
return related
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ class Service(models.Model):
|
||||||
"Related instance can be instantiated with <tt>instance</tt> keyword or "
|
"Related instance can be instantiated with <tt>instance</tt> keyword or "
|
||||||
"<tt>content_type.model_name</tt>.</br>"
|
"<tt>content_type.model_name</tt>.</br>"
|
||||||
"<tt> databaseuser.type == 'MYSQL'</tt><br>"
|
"<tt> databaseuser.type == 'MYSQL'</tt><br>"
|
||||||
"<tt> miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))</tt><br>"
|
"<tt> miscellaneous.active and str(miscellaneous.identifier).endswith(('.org', '.net', '.com'))</tt><br>"
|
||||||
"<tt> contractedplan.plan.name == 'association_fee''</tt><br>"
|
"<tt> contractedplan.plan.name == 'association_fee''</tt><br>"
|
||||||
"<tt> instance.active</tt>"))
|
"<tt> instance.active</tt>"))
|
||||||
handler_type = models.CharField(_("handler"), max_length=256, blank=True,
|
handler_type = models.CharField(_("handler"), max_length=256, blank=True,
|
||||||
|
@ -117,9 +117,10 @@ class Service(models.Model):
|
||||||
decimal_places=2)
|
decimal_places=2)
|
||||||
tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES,
|
tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES,
|
||||||
default=settings.SERVICES_SERVICE_DEFAULT_TAX)
|
default=settings.SERVICES_SERVICE_DEFAULT_TAX)
|
||||||
pricing_period = models.CharField(_("pricing period"), max_length=16,
|
pricing_period = models.CharField(_("pricing period"), max_length=16, blank=True,
|
||||||
help_text=_("Time period that is used for computing the rate metric."),
|
help_text=_("Time period that is used for computing the rate metric."),
|
||||||
choices=(
|
choices=(
|
||||||
|
(NEVER, _("Current value")),
|
||||||
(BILLING_PERIOD, _("Same as billing period")),
|
(BILLING_PERIOD, _("Same as billing period")),
|
||||||
(MONTHLY, _("Monthly data")),
|
(MONTHLY, _("Monthly data")),
|
||||||
(ANUAL, _("Anual data")),
|
(ANUAL, _("Anual data")),
|
||||||
|
|
|
@ -208,7 +208,10 @@ class Exim4Traffic(ServiceMonitor):
|
||||||
with open(mainlog, 'r') as mainlog:
|
with open(mainlog, 'r') as mainlog:
|
||||||
for line in mainlog.readlines():
|
for line in mainlog.readlines():
|
||||||
if ' <= ' in line and 'P=local' in line:
|
if ' <= ' in line and 'P=local' in line:
|
||||||
username = user_regex.search(line).groups()[0]
|
username = user_regex.search(line)
|
||||||
|
if not username:
|
||||||
|
continue
|
||||||
|
username = username.groups()[0]
|
||||||
try:
|
try:
|
||||||
sender = users[username]
|
sender = users[username]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -39,9 +39,8 @@ class WebAppOptionInline(admin.TabularInline):
|
||||||
plugin = AppType.get_plugin(request.GET['type'])
|
plugin = AppType.get_plugin(request.GET['type'])
|
||||||
kwargs['choices'] = plugin.get_options_choices()
|
kwargs['choices'] = plugin.get_options_choices()
|
||||||
# Help text based on select widget
|
# Help text based on select widget
|
||||||
kwargs['widget'] = DynamicHelpTextSelect(
|
target = 'this.id.replace("name", "value")'
|
||||||
'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT
|
kwargs['widget'] = DynamicHelpTextSelect(target, self.OPTIONS_HELP_TEXT)
|
||||||
)
|
|
||||||
return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +65,6 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
|
||||||
websites.append('<a href="%s">%s</a>' % (url, name))
|
websites.append('<a href="%s">%s</a>' % (url, name))
|
||||||
if not websites:
|
if not websites:
|
||||||
add_url = reverse('admin:websites_website_add')
|
add_url = reverse('admin:websites_website_add')
|
||||||
# TODO support for preselecting related web app on website
|
|
||||||
add_url += '?account=%s' % webapp.account_id
|
add_url += '?account=%s' % webapp.account_id
|
||||||
plus = '<strong style="color:green; font-size:12px">+</strong>'
|
plus = '<strong style="color:green; font-size:12px">+</strong>'
|
||||||
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
|
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
|
||||||
|
|
|
@ -37,7 +37,8 @@ class WebAppServiceMixin(object):
|
||||||
'type': webapp.type,
|
'type': webapp.type,
|
||||||
'app_path': webapp.get_path().rstrip('/'),
|
'app_path': webapp.get_path().rstrip('/'),
|
||||||
'banner': self.get_banner(),
|
'banner': self.get_banner(),
|
||||||
'under_construction_path': settings.settings.WEBAPPS_UNDER_CONSTRUCTION_PATH
|
'under_construction_path': settings.settings.WEBAPPS_UNDER_CONSTRUCTION_PATH,
|
||||||
|
'is_mounted': webapp.content_set.exists(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from .. import settings
|
||||||
|
|
||||||
class PHPBackend(WebAppServiceMixin, ServiceController):
|
class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
verbose_name = _("PHP FPM/FCGID")
|
verbose_name = _("PHP FPM/FCGID")
|
||||||
default_route_match = "webapp.type == 'php'"
|
default_route_match = "webapp.type.endswith('php')"
|
||||||
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
|
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
|
||||||
|
|
||||||
def save(self, webapp):
|
def save(self, webapp):
|
||||||
|
@ -34,7 +34,8 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
} || {
|
} || {
|
||||||
echo -e "${fpm_config}" > %(fpm_path)s
|
echo -e "${fpm_config}" > %(fpm_path)s
|
||||||
UPDATEDFPM=1
|
UPDATEDFPM=1
|
||||||
}""") % context
|
}
|
||||||
|
""") % context
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_fcgid(self, webapp, context):
|
def save_fcgid(self, webapp, context):
|
||||||
|
@ -46,8 +47,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
{
|
{
|
||||||
echo -e "${wrapper}" | diff -N -I'^\s*#' %(wrapper_path)s -
|
echo -e "${wrapper}" | diff -N -I'^\s*#' %(wrapper_path)s -
|
||||||
} || {
|
} || {
|
||||||
echo -e "${wrapper}" > %(wrapper_path)s; UPDATED_APACHE=1
|
echo -e "${wrapper}" > %(wrapper_path)s
|
||||||
}""") % context
|
[[ ${UPDATED_APACHE} -eq 0 ]] && UPDATED_APACHE=%(is_mounted)i
|
||||||
|
}
|
||||||
|
""") % context
|
||||||
)
|
)
|
||||||
self.append("chmod 550 %(wrapper_dir)s" % context)
|
self.append("chmod 550 %(wrapper_dir)s" % context)
|
||||||
self.append("chmod 550 %(wrapper_path)s" % context)
|
self.append("chmod 550 %(wrapper_path)s" % context)
|
||||||
|
@ -58,8 +61,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
{
|
{
|
||||||
echo -e "${cmd_options}" | diff -N -I'^\s*#' %(cmd_options_path)s -
|
echo -e "${cmd_options}" | diff -N -I'^\s*#' %(cmd_options_path)s -
|
||||||
} || {
|
} || {
|
||||||
echo -e "${cmd_options}" > %(cmd_options_path)s; UPDATED_APACHE=1
|
echo -e "${cmd_options}" > %(cmd_options_path)s
|
||||||
}""" ) % context
|
[[ ${UPDATED_APACHE} -eq 0 ]] && UPDATED_APACHE=%(is_mounted)i
|
||||||
|
}
|
||||||
|
""" ) % context
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.append("rm -f %(cmd_options_path)s" % context)
|
self.append("rm -f %(cmd_options_path)s" % context)
|
||||||
|
@ -85,12 +90,14 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
if [[ $UPDATEDFPM == 1 ]]; then
|
if [[ $UPDATEDFPM == 1 ]]; then
|
||||||
service php5-fpm reload
|
service php5-fpm reload
|
||||||
service php5-fpm start
|
service php5-fpm start
|
||||||
fi""")
|
fi
|
||||||
|
""")
|
||||||
)
|
)
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
if [[ $UPDATED_APACHE == 1 ]]; then
|
if [[ $UPDATED_APACHE == 1 ]]; then
|
||||||
service apache2 reload
|
service apache2 reload
|
||||||
fi""")
|
fi
|
||||||
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_fpm_config(self, webapp, context):
|
def get_fpm_config(self, webapp, context):
|
||||||
|
@ -149,7 +156,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
if value:
|
if value:
|
||||||
cmd_options.append("%s %s" % (directive, value))
|
cmd_options.append("%s %s" % (directive, value))
|
||||||
if cmd_options:
|
if cmd_options:
|
||||||
head = '# %(banner)s\nFcgidCmdOptions %(wrapper_path)s' % context
|
head = (
|
||||||
|
'# %(banner)s\n'
|
||||||
|
'FcgidCmdOptions %(wrapper_path)s'
|
||||||
|
) % context
|
||||||
cmd_options.insert(0, head)
|
cmd_options.insert(0, head)
|
||||||
return ' \\\n '.join(cmd_options)
|
return ' \\\n '.join(cmd_options)
|
||||||
|
|
||||||
|
|
|
@ -9,40 +9,112 @@ from .. import settings
|
||||||
from . import WebAppServiceMixin
|
from . import WebAppServiceMixin
|
||||||
|
|
||||||
|
|
||||||
|
# Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php
|
||||||
class WordPressBackend(WebAppServiceMixin, ServiceController):
|
class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
verbose_name = _("Wordpress")
|
verbose_name = _("Wordpress")
|
||||||
model = 'webapps.WebApp'
|
model = 'webapps.WebApp'
|
||||||
default_route_match = "webapp.type == 'wordpress'"
|
default_route_match = "webapp.type == 'wordpress-php'"
|
||||||
|
script_executable = '/usr/bin/php'
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
<?php
|
||||||
|
function exc($cmd) {
|
||||||
|
passthru($cmd, $exit_code);
|
||||||
|
if ($exit_code != 0) {
|
||||||
|
echo "ERROR: execution returned non-zero code: $exit_code. cmd was:\\n$cmd\\n";
|
||||||
|
exit($exit_code);
|
||||||
|
}
|
||||||
|
}""")
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, webapp):
|
def save(self, webapp):
|
||||||
context = self.get_context(webapp)
|
context = self.get_context(webapp)
|
||||||
self.create_webapp_dir(context)
|
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
# Check if directory is empty befor doing anything
|
if (count(glob("%(app_path)s/*")) > 1) {
|
||||||
if [[ ! $(ls -A %(app_path)s) ]]; then
|
die("App directory not empty.");
|
||||||
wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate \\
|
}
|
||||||
| tar -xzvf - -C %(app_path)s --strip-components=1
|
exc('mkdir -p %(app_path)s');
|
||||||
cp %(app_path)s/wp-config-sample.php %(app_path)s/wp-config.php
|
exc('rm -f %(app_path)s/index.html');
|
||||||
sed -i "s/database_name_here/%(db_name)s/" %(app_path)s/wp-config.php
|
exc('wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate | tar -xzvf - -C %(app_path)s --strip-components=1');
|
||||||
sed -i "s/username_here/%(db_user)s/" %(app_path)s/wp-config.php
|
exc('mkdir %(app_path)s/wp-content/uploads');
|
||||||
sed -i "s/password_here/%(db_pass)s/" %(app_path)s/wp-config.php
|
exc('chmod 750 %(app_path)s/wp-content/uploads');
|
||||||
sed -i "s/localhost/%(db_host)s/" %(app_path)s/wp-config.php
|
exc('chown -R %(user)s:%(group)s %(app_path)s');
|
||||||
mkdir %(app_path)s/wp-content/uploads
|
|
||||||
chmod 750 %(app_path)s/wp-content/uploads
|
$config_file = file('%(app_path)s/' . 'wp-config-sample.php');
|
||||||
chown -R %(user)s:%(group)s %(app_path)s
|
$secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/');
|
||||||
fi""") % context
|
$secret_keys = explode( "\\n", $secret_keys );
|
||||||
|
foreach ( $secret_keys as $k => $v ) {
|
||||||
|
$secret_keys[$k] = substr( $v, 28, 64 );
|
||||||
|
}
|
||||||
|
array_pop($secret_keys);
|
||||||
|
|
||||||
|
$config_file = str_replace('database_name_here', '%(db_name)s', $config_file);
|
||||||
|
$config_file = str_replace('username_here', '%(db_user)s', $config_file);
|
||||||
|
$config_file = str_replace('password_here', '%(password)s', $config_file);
|
||||||
|
$config_file = str_replace('localhost', '%(db_host)s', $config_file);
|
||||||
|
$config_file = str_replace("'AUTH_KEY', 'put your unique phrase here'", "'AUTH_KEY', '{$secret_keys[0]}'", $config_file);
|
||||||
|
$config_file = str_replace("'SECURE_AUTH_KEY', 'put your unique phrase here'", "'SECURE_AUTH_KEY', '{$secret_keys[1]}'", $config_file);
|
||||||
|
$config_file = str_replace("'LOGGED_IN_KEY', 'put your unique phrase here'", "'LOGGED_IN_KEY', '{$secret_keys[2]}'", $config_file);
|
||||||
|
$config_file = str_replace("'NONCE_KEY', 'put your unique phrase here'", "'NONCE_KEY', '{$secret_keys[3]}'", $config_file);
|
||||||
|
$config_file = str_replace("'AUTH_SALT', 'put your unique phrase here'", "'AUTH_SALT', '{$secret_keys[4]}'", $config_file);
|
||||||
|
$config_file = str_replace("'SECURE_AUTH_SALT', 'put your unique phrase here'", "'SECURE_AUTH_SALT', '{$secret_keys[5]}'", $config_file);
|
||||||
|
$config_file = str_replace("'LOGGED_IN_SALT', 'put your unique phrase here'", "'LOGGED_IN_SALT', '{$secret_keys[6]}'", $config_file);
|
||||||
|
$config_file = str_replace("'NONCE_SALT', 'put your unique phrase here'", "'NONCE_SALT', '{$secret_keys[7]}'", $config_file);
|
||||||
|
|
||||||
|
if(file_exists('%(app_path)s/' .'wp-config.php')) {
|
||||||
|
unlink('%(app_path)s/' .'wp-config.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fw = fopen('%(app_path)s/' . 'wp-config.php', 'a');
|
||||||
|
foreach ( $config_file as $line_num => $line ) {
|
||||||
|
fwrite($fw, $line);
|
||||||
|
}
|
||||||
|
define('WP_CONTENT_DIR', 'wp-content/');
|
||||||
|
define('WP_LANG_DIR', WP_CONTENT_DIR . '/languages' );
|
||||||
|
define('WP_USE_THEMES', true);
|
||||||
|
define('DB_NAME', '%(db_name)s');
|
||||||
|
define('DB_USER', '%(db_user)s');
|
||||||
|
define('DB_PASSWORD', '%(password)s');
|
||||||
|
define('DB_HOST', '%(db_host)s');
|
||||||
|
|
||||||
|
$_GET['step'] = 2;
|
||||||
|
$_POST['weblog_title'] = "%(title)s";
|
||||||
|
$_POST['user_name'] = "admin";
|
||||||
|
$_POST['admin_email'] = "%(email)s";
|
||||||
|
$_POST['blog_public'] = true;
|
||||||
|
$_POST['admin_password'] = "%(password)s";
|
||||||
|
$_POST['admin_password2'] = "%(password)s";
|
||||||
|
|
||||||
|
function wp_new_blog_notification($blog_title, $blog_url, $user_id, $password){
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
ob_start();
|
||||||
|
require_once('%(app_path)s/wp-admin/install.php');
|
||||||
|
$response = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
if (strpos($response, '<h1>Success!</h1>') === false) {
|
||||||
|
echo "Error has occured during installation\\n";
|
||||||
|
echo $msg;
|
||||||
|
exit(1);
|
||||||
|
}""") % context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
self.append('?>')
|
||||||
|
|
||||||
def delete(self, webapp):
|
def delete(self, webapp):
|
||||||
context = self.get_context(webapp)
|
context = self.get_context(webapp)
|
||||||
self.delete_webapp_dir(context)
|
self.append("exc('rm -rf %(app_path)s');" % context)
|
||||||
|
|
||||||
def get_context(self, webapp):
|
def get_context(self, webapp):
|
||||||
context = super(WordPressBackend, self).get_context(webapp)
|
context = super(WordPressBackend, self).get_context(webapp)
|
||||||
context.update({
|
context.update({
|
||||||
'db_name': webapp.data['db_name'],
|
'db_name': webapp.data['db_name'],
|
||||||
'db_user': webapp.data['db_user'],
|
'db_user': webapp.data['db_user'],
|
||||||
'db_pass': webapp.data['db_pass'],
|
'password': webapp.data['password'],
|
||||||
'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST,
|
'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST,
|
||||||
|
'title': "%s blog's" % webapp.account.get_full_name(),
|
||||||
|
'email': webapp.account.email,
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
|
|
||||||
|
from orchestra.apps.databases.models import Database, DatabaseUser
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualDatabaseRelation(GenericRelation):
|
||||||
|
""" Delete related databases if any """
|
||||||
|
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
|
||||||
|
pks = []
|
||||||
|
for obj in objs:
|
||||||
|
db_id = obj.data.get('db_id')
|
||||||
|
if db_id:
|
||||||
|
pks.append(db_id)
|
||||||
|
if not pks:
|
||||||
|
return []
|
||||||
|
# TODO renamed to self.remote_field in django 1.8
|
||||||
|
return self.rel.to._base_manager.db_manager(using).filter(pk__in=pks)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualDatabaseUserRelation(GenericRelation):
|
||||||
|
""" Delete related databases if any """
|
||||||
|
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
|
||||||
|
pks = []
|
||||||
|
for obj in objs:
|
||||||
|
db_id = obj.data.get('db_user_id')
|
||||||
|
if db_id:
|
||||||
|
pks.append(db_id)
|
||||||
|
if not pks:
|
||||||
|
return []
|
||||||
|
# TODO renamed to self.remote_field in django 1.8
|
||||||
|
return self.rel.to._base_manager.db_manager(using).filter(pk__in=pks)
|
|
@ -13,6 +13,7 @@ from orchestra.core import validators, services
|
||||||
from orchestra.utils.functional import cached
|
from orchestra.utils.functional import cached
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from .fields import VirtualDatabaseRelation, VirtualDatabaseUserRelation
|
||||||
from .options import AppOption
|
from .options import AppOption
|
||||||
from .types import AppType
|
from .types import AppType
|
||||||
|
|
||||||
|
@ -27,6 +28,10 @@ class WebApp(models.Model):
|
||||||
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."))
|
||||||
|
|
||||||
|
# CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them
|
||||||
|
databases = VirtualDatabaseRelation('databases.Database')
|
||||||
|
databaseusers = VirtualDatabaseUserRelation('databases.DatabaseUser')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('name', 'account')
|
unique_together = ('name', 'account')
|
||||||
verbose_name = _("Web App")
|
verbose_name = _("Web App")
|
||||||
|
|
|
@ -71,9 +71,6 @@ class AppType(plugins.Plugin):
|
||||||
def delete(self):
|
def delete(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_related_objects(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_directive_context(self):
|
def get_directive_context(self):
|
||||||
return {
|
return {
|
||||||
'app_id': self.instance.id,
|
'app_id': self.instance.id,
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from orchestra.apps.databases.models import Database, DatabaseUser
|
||||||
|
from orchestra.forms import widgets
|
||||||
|
from orchestra.plugins.forms import PluginDataForm
|
||||||
|
from orchestra.utils.python import random_ascii
|
||||||
|
|
||||||
|
from .. import settings
|
||||||
|
|
||||||
|
from .php import PHPApp, PHPAppForm, PHPAppSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CMSAppForm(PHPAppForm):
|
||||||
|
db_name = forms.CharField(label=_("Database name"),
|
||||||
|
help_text=_("Database exclusively used for this webapp."))
|
||||||
|
db_user = forms.CharField(label=_("Database user"),
|
||||||
|
help_text=_("Database user exclusively used for this webapp."))
|
||||||
|
password = forms.CharField(label=_("Password"),
|
||||||
|
help_text=_("Initial database and WordPress admin password.<br>"
|
||||||
|
"Subsequent changes to the admin password will not be reflected."))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CMSAppForm, self).__init__(*args, **kwargs)
|
||||||
|
if self.instance:
|
||||||
|
data = self.instance.data
|
||||||
|
# DB link
|
||||||
|
db_name = data.get('db_name')
|
||||||
|
db_id = data.get('db_id')
|
||||||
|
db_url = reverse('admin:databases_database_change', args=(db_id,))
|
||||||
|
db_link = mark_safe('<a href="%s">%s</a>' % (db_url, db_name))
|
||||||
|
self.fields['db_name'].widget = widgets.ReadOnlyWidget(db_name, db_link)
|
||||||
|
# DB user link
|
||||||
|
db_user = data.get('db_user')
|
||||||
|
db_user_id = data.get('db_user_id')
|
||||||
|
db_user_url = reverse('admin:databases_databaseuser_change', args=(db_user_id,))
|
||||||
|
db_user_link = mark_safe('<a href="%s">%s</a>' % (db_user_url, db_user))
|
||||||
|
self.fields['db_user'].widget = widgets.ReadOnlyWidget(db_user, db_user_link)
|
||||||
|
|
||||||
|
|
||||||
|
class CMSAppSerializer(PHPAppSerializer):
|
||||||
|
db_name = serializers.CharField(label=_("Database name"), required=False)
|
||||||
|
db_user = serializers.CharField(label=_("Database user"), required=False)
|
||||||
|
password = serializers.CharField(label=_("Password"), required=False)
|
||||||
|
db_id = serializers.IntegerField(label=_("Database ID"), required=False)
|
||||||
|
db_user_id = serializers.IntegerField(label=_("Database user ID"), required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CMSApp(PHPApp):
|
||||||
|
""" Abstract AppType with common CMS functionality """
|
||||||
|
serializer = CMSAppSerializer
|
||||||
|
change_form = CMSAppForm
|
||||||
|
change_readonly_fileds = ('db_name', 'db_user', 'password',)
|
||||||
|
db_type = Database.MYSQL
|
||||||
|
|
||||||
|
def get_db_name(self):
|
||||||
|
db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)
|
||||||
|
# Limit for mysql database names
|
||||||
|
return db_name[:65]
|
||||||
|
|
||||||
|
def get_db_user(self):
|
||||||
|
db_name = self.get_db_name()
|
||||||
|
# Limit for mysql user names
|
||||||
|
return db_name[:16]
|
||||||
|
|
||||||
|
def get_password(self):
|
||||||
|
return random_ascii(10)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
super(CMSApp, self).validate()
|
||||||
|
create = not self.instance.pk
|
||||||
|
if create:
|
||||||
|
db = Database(name=self.get_db_name(), account=self.instance.account, type=self.db_type)
|
||||||
|
user = DatabaseUser(username=self.get_db_user(), password=self.get_password(),
|
||||||
|
account=self.instance.account, type=self.db_type)
|
||||||
|
for obj in (db, user):
|
||||||
|
try:
|
||||||
|
obj.full_clean()
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'name': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
db_name = self.get_db_name()
|
||||||
|
db_user = self.get_db_user()
|
||||||
|
password = self.get_password()
|
||||||
|
db, db_created = self.instance.account.databases.get_or_create(name=db_name, type=self.db_type)
|
||||||
|
if db_created:
|
||||||
|
user = DatabaseUser(username=db_user, account=self.instance.account, type=self.db_type)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
db.users.add(user)
|
||||||
|
self.instance.data = {
|
||||||
|
'db_name': db_name,
|
||||||
|
'db_user': db_user,
|
||||||
|
'password': password,
|
||||||
|
'db_id': db.id,
|
||||||
|
'db_user_id': user.id,
|
||||||
|
}
|
|
@ -1,105 +1,13 @@
|
||||||
from django import forms
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from orchestra.apps.databases.models import Database, DatabaseUser
|
from .cms import CMSApp
|
||||||
from orchestra.plugins.forms import PluginDataForm
|
|
||||||
from orchestra.utils.python import random_ascii
|
|
||||||
|
|
||||||
from .. import settings
|
|
||||||
|
|
||||||
from .php import PHPApp, PHPAppForm, PHPAppSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class WordPressAppForm(PHPAppForm):
|
class WordPressApp(CMSApp):
|
||||||
db_name = forms.CharField(label=_("Database name"),
|
name = 'wordpress-php'
|
||||||
help_text=_("Database used for this webapp."))
|
|
||||||
db_user = forms.CharField(label=_("Database user"),)
|
|
||||||
db_pass = forms.CharField(label=_("Database user password"),
|
|
||||||
help_text=_("Initial database password."))
|
|
||||||
|
|
||||||
|
|
||||||
class WordPressAppSerializer(PHPAppSerializer):
|
|
||||||
db_name = serializers.CharField(label=_("Database name"), required=False)
|
|
||||||
db_user = serializers.CharField(label=_("Database user"), required=False)
|
|
||||||
db_pass = serializers.CharField(label=_("Database user password"), required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class WordPressApp(PHPApp):
|
|
||||||
name = 'wordpress'
|
|
||||||
verbose_name = "WordPress"
|
verbose_name = "WordPress"
|
||||||
serializer = WordPressAppSerializer
|
help_text = _(
|
||||||
change_form = WordPressAppForm
|
"Visit http://<domain.lan>/wp-admin/install.php to finish the installation.<br>"
|
||||||
change_readonly_fileds = ('db_name', 'db_user', 'db_pass',)
|
"A database and database user will automatically be created for this webapp."
|
||||||
help_text = _("Visit http://<domain.lan>/wp-admin/install.php to finish the installation.")
|
)
|
||||||
icon = 'orchestra/icons/apps/WordPress.png'
|
icon = 'orchestra/icons/apps/WordPress.png'
|
||||||
|
|
||||||
def get_db_name(self):
|
|
||||||
db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)
|
|
||||||
# Limit for mysql database names
|
|
||||||
return db_name[:65]
|
|
||||||
|
|
||||||
def get_db_user(self):
|
|
||||||
db_name = self.get_db_name()
|
|
||||||
# Limit for mysql user names
|
|
||||||
return db_name[:16]
|
|
||||||
|
|
||||||
def get_db_pass(self):
|
|
||||||
return random_ascii(10)
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
super(WordPressApp, self).validate()
|
|
||||||
create = not self.instance.pk
|
|
||||||
if create:
|
|
||||||
db = Database(name=self.get_db_name(), account=self.instance.account)
|
|
||||||
user = DatabaseUser(username=self.get_db_user(), password=self.get_db_pass(),
|
|
||||||
account=self.instance.account)
|
|
||||||
for obj in (db, user):
|
|
||||||
try:
|
|
||||||
obj.full_clean()
|
|
||||||
except ValidationError as e:
|
|
||||||
raise ValidationError({
|
|
||||||
'name': e.messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
db_name = self.get_db_name()
|
|
||||||
db_user = self.get_db_user()
|
|
||||||
db_pass = self.get_db_pass()
|
|
||||||
db, db_created = Database.objects.get_or_create(name=db_name, account=self.instance.account)
|
|
||||||
if db_created:
|
|
||||||
user = DatabaseUser(username=db_user, account=self.instance.account)
|
|
||||||
user.set_password(db_pass)
|
|
||||||
user.save()
|
|
||||||
db.users.add(user)
|
|
||||||
self.instance.data = {
|
|
||||||
'db_name': db_name,
|
|
||||||
'db_user': db_user,
|
|
||||||
'db_pass': db_pass,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# Trigger related backends
|
|
||||||
for related in self.get_related():
|
|
||||||
related.save(update_fields=[])
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
for related in self.get_related():
|
|
||||||
related.delete()
|
|
||||||
|
|
||||||
def get_related(self):
|
|
||||||
related = []
|
|
||||||
account = self.instance.account
|
|
||||||
try:
|
|
||||||
db_user = account.databaseusers.get(username=self.instance.data.get('db_user'))
|
|
||||||
except DatabaseUser.DoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
related.append(db_user)
|
|
||||||
try:
|
|
||||||
db = account.databases.get(name=self.instance.data.get('db_name'))
|
|
||||||
except Database.DoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
related.append(db)
|
|
||||||
return related
|
|
||||||
|
|
|
@ -30,9 +30,8 @@ class WebsiteDirectiveInline(admin.TabularInline):
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
|
||||||
if db_field.name == 'name':
|
if db_field.name == 'name':
|
||||||
# Help text based on select widget
|
# Help text based on select widget
|
||||||
kwargs['widget'] = DynamicHelpTextSelect(
|
target = 'this.id.replace("name", "value")'
|
||||||
'this.id.replace("name", "value")', self.DIRECTIVES_HELP_TEXT
|
kwargs['widget'] = DynamicHelpTextSelect(target, self.DIRECTIVES_HELP_TEXT)
|
||||||
)
|
|
||||||
return super(WebsiteDirectiveInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(WebsiteDirectiveInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
for content in website.content_set.all():
|
for content in website.content_set.all():
|
||||||
webapp = content.webapp
|
webapp = content.webapp
|
||||||
url = change_url(webapp)
|
url = change_url(webapp)
|
||||||
name = "%s on %s" % (webapp.get_type_display(), content.path)
|
name = "%s on %s" % (webapp.get_type_display(), content.path or '/')
|
||||||
webapps.append('<a href="%s">%s</a>' % (url, name))
|
webapps.append('<a href="%s">%s</a>' % (url, name))
|
||||||
return '<br>'.join(webapps)
|
return '<br>'.join(webapps)
|
||||||
display_webapps.allow_tags = True
|
display_webapps.allow_tags = True
|
||||||
|
|
|
@ -32,6 +32,9 @@ class Apache2Backend(ServiceController):
|
||||||
extra_conf += self.get_redirects(directives)
|
extra_conf += self.get_redirects(directives)
|
||||||
extra_conf += self.get_proxies(directives)
|
extra_conf += self.get_proxies(directives)
|
||||||
extra_conf += self.get_saas(directives)
|
extra_conf += self.get_saas(directives)
|
||||||
|
settings_context = site.get_settings_context()
|
||||||
|
for location, directive in settings.WEBSITES_VHOST_EXTRA_DIRECTIVES:
|
||||||
|
extra_conf.append((location, directive % settings_context))
|
||||||
# Order extra conf directives based on directives (longer first)
|
# Order extra conf directives based on directives (longer first)
|
||||||
extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True)
|
extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True)
|
||||||
context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf])
|
context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf])
|
||||||
|
|
|
@ -93,12 +93,12 @@ class Website(models.Model):
|
||||||
def get_www_access_log_path(self):
|
def get_www_access_log_path(self):
|
||||||
context = self.get_settings_context()
|
context = self.get_settings_context()
|
||||||
path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % context
|
path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % context
|
||||||
return os.path.normpath(path.replace('//', '/'))
|
return os.path.normpath(path)
|
||||||
|
|
||||||
def get_www_error_log_path(self):
|
def get_www_error_log_path(self):
|
||||||
context = self.get_settings_context()
|
context = self.get_settings_context()
|
||||||
path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context
|
path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context
|
||||||
return os.path.normpath(path.replace('//', '/'))
|
return os.path.normpath(path)
|
||||||
|
|
||||||
|
|
||||||
class WebsiteDirective(models.Model):
|
class WebsiteDirective(models.Model):
|
||||||
|
|
|
@ -94,3 +94,8 @@ WEBSITES_DEFAULT_SSL_KEY = getattr(settings, 'WEBSITES_DEFAULT_SSL_KEY',
|
||||||
WEBSITES_DEFAULT_SSL_CA = getattr(settings, 'WEBSITES_DEFAULT_SSL_CA',
|
WEBSITES_DEFAULT_SSL_CA = getattr(settings, 'WEBSITES_DEFAULT_SSL_CA',
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
WEBSITES_VHOST_EXTRA_DIRECTIVES = getattr(settings, 'WEBSITES_VHOST_EXTRA_DIRECTIVES', (
|
||||||
|
# (<location>, <directive>),
|
||||||
|
# ('/cgi-bin/', 'ScriptAlias /cgi-bin/ %(home)s/cgi-bin/'),
|
||||||
|
))
|
||||||
|
|
|
@ -13,11 +13,10 @@ class PluginDataForm(forms.ModelForm):
|
||||||
display = '%s <a href=".">change</a>' % unicode(self.plugin.verbose_name)
|
display = '%s <a href=".">change</a>' % unicode(self.plugin.verbose_name)
|
||||||
self.fields[self.plugin_field].widget = ReadOnlyWidget(value, display)
|
self.fields[self.plugin_field].widget = ReadOnlyWidget(value, display)
|
||||||
self.fields[self.plugin_field].help_text = getattr(self.plugin, 'help_text', '')
|
self.fields[self.plugin_field].help_text = getattr(self.plugin, 'help_text', '')
|
||||||
instance = kwargs.get('instance')
|
if self.instance:
|
||||||
if instance:
|
|
||||||
for field in self.declared_fields:
|
for field in self.declared_fields:
|
||||||
initial = self.fields[field].initial
|
initial = self.fields[field].initial
|
||||||
self.fields[field].initial = instance.data.get(field, initial)
|
self.fields[field].initial = self.instance.data.get(field, initial)
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
for field in self.plugin.get_change_readonly_fileds():
|
for field in self.plugin.get_change_readonly_fileds():
|
||||||
value = getattr(self.instance, field, None) or self.instance.data[field]
|
value = getattr(self.instance, field, None) or self.instance.data[field]
|
||||||
|
@ -27,14 +26,12 @@ class PluginDataForm(forms.ModelForm):
|
||||||
display = foo_display()
|
display = foo_display()
|
||||||
self.fields[field].required = False
|
self.fields[field].required = False
|
||||||
self.fields[field].widget = ReadOnlyWidget(value, display)
|
self.fields[field].widget = ReadOnlyWidget(value, display)
|
||||||
# self.fields[field].help_text = None
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# TODO clean all filed within data???
|
|
||||||
data = {}
|
data = {}
|
||||||
for field in self.declared_fields:
|
for field, value in self.instance.data.iteritems():
|
||||||
try:
|
try:
|
||||||
data[field] = self.cleaned_data[field]
|
data[field] = self.cleaned_data[field]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
data[field] = self.data[field]
|
data[field] = value
|
||||||
self.cleaned_data['data'] = data
|
self.cleaned_data['data'] = data
|
||||||
|
|
|
@ -93,17 +93,24 @@ class PluginModelAdapter(Plugin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_plugins(cls):
|
def get_plugins(cls):
|
||||||
plugins = []
|
plugins = []
|
||||||
for instance in cls.model.objects.filter(is_active=True):
|
for related_instance in cls.model.objects.filter(is_active=True):
|
||||||
attributes = {
|
attributes = {
|
||||||
'instance': instance,
|
'related_instance': related_instance,
|
||||||
'verbose_name': instance.verbose_name
|
'verbose_name': related_instance.verbose_name
|
||||||
}
|
}
|
||||||
plugins.append(type('PluginAdapter', (cls,), attributes))
|
plugins.append(type('PluginAdapter', (cls,), attributes))
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin(cls, name):
|
||||||
|
# don't cache, since models can change
|
||||||
|
for plugin in cls.get_plugins():
|
||||||
|
if name == plugin.get_name():
|
||||||
|
return plugin
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls):
|
def get_name(cls):
|
||||||
return getattr(cls.instance, cls.name_field)
|
return getattr(cls.related_instance, cls.name_field)
|
||||||
|
|
||||||
|
|
||||||
class PluginMount(type):
|
class PluginMount(type):
|
||||||
|
|
Loading…
Reference in New Issue