Random fixes

This commit is contained in:
Marc Aymerich 2015-03-27 19:50:54 +00:00
parent 882c03a416
commit 124124da6c
34 changed files with 432 additions and 238 deletions

51
TODO.md
View File

@ -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
from django.db import DEFAULT_DB_ALIAS
from orchestra.apps.databases.models import Database
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?? * document service help things: discount/refound/compensation effect and metric table
* Document metric interpretation help_text
* document plugin serialization, data_serializer?
* bill line managemente, remove, undo (only when possible), move, copy, paste
* budgets: no undo feature
* webapps/saas delete related db by id not name !! type!=Mysql * Autocomplete admin fields like <site_name>.phplist... with js
* autoexpand mailbox.filter according to filtering options
* 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

View File

@ -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

View File

@ -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 """

View File

@ -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 {

View File

@ -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')

View File

@ -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))

View File

@ -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
@ -105,7 +105,15 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA
if db_field.name == 'description': if db_field.name == 'description':
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)

View File

@ -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()

View File

@ -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')

View File

@ -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)

View File

@ -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.")

View File

@ -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):

View File

@ -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)

View File

@ -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"

View File

@ -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"))

View File

@ -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)

View File

@ -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

View File

@ -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>&nbsp;databaseuser.type == 'MYSQL'</tt><br>" "<tt>&nbsp;databaseuser.type == 'MYSQL'</tt><br>"
"<tt>&nbsp;miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))</tt><br>" "<tt>&nbsp;miscellaneous.active and str(miscellaneous.identifier).endswith(('.org', '.net', '.com'))</tt><br>"
"<tt>&nbsp;contractedplan.plan.name == 'association_fee''</tt><br>" "<tt>&nbsp;contractedplan.plan.name == 'association_fee''</tt><br>"
"<tt>&nbsp;instance.active</tt>")) "<tt>&nbsp;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")),

View File

@ -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:

View File

@ -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")))

View File

@ -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(),
} }

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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,

View File

@ -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,
}

View File

@ -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://&lt;domain.lan&gt;/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://&lt;domain.lan&gt;/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

View File

@ -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

View File

@ -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])

View File

@ -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):

View File

@ -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/'),
))

View File

@ -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

View File

@ -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):