Added support for SaaS service custom URL

This commit is contained in:
Marc Aymerich 2015-10-01 16:02:26 +00:00
parent 0f603181ff
commit 95a6a0c37d
26 changed files with 518 additions and 53 deletions

21
TODO.md
View File

@ -387,11 +387,6 @@ Case
# Modsecurity rules template by cms (wordpress, joomla, dokuwiki (973337 973338 973347 958057), ...
# saas custom domains support (maybe a new form field with custom url? autoconfigure websites?)
custom_url form field and validate/create/delete related website
SAAS_PHPLIST_ALLOW_CUSTOM_URL = False
deploy --dev
deploy.sh and deploy-dev.sh autoupgrade
@ -401,7 +396,6 @@ orchestra home autocomplete
short URLS: https://github.com/rsvp/gitio
link backend help text variables to settings/#var_name
saas changelist domain: add <br>custom domain<img>
$ sudo python manage.py startservices
Traceback (most recent call last):
@ -409,3 +403,18 @@ Traceback (most recent call last):
from django.core.management import execute_from_command_line
ImportError: No module named django.core.management
autocomplete; on the form header and type="search"
To latest developers to post on this thread: I implemented the workaround I described in comment #14 nearly three months ago, and it has worked perfectly since then. While we would all prefer that "autocomplete=off" function properly at all times, it still functions properly if you include in your form an input element with any other autocomplete value.
I simply added this code to my layout:
<div style="display: none;">
<input type="text" id="PreventChromeAutocomplete" name="PreventChromeAutocomplete" autocomplete="address-level4" />
</div>
Once I did this, all of my "autocomplete=off" elements were respected by Chrome.
<input type="password" name="password" value="" style="display: none" />
http://makandracards.com/makandra/24933-chrome-34+-firefox-38+-ie11+-ignore-autocomplete-off

View File

@ -180,7 +180,7 @@ class AccountAdminMixin(object):
def account_link(self, instance):
account = instance.account if instance.pk else self.account
url = change_url(account)
return '<a href="%s">%s</a>' % (url, str(account))
return '<a href="%s">%s</a>' % (url, account)
account_link.short_description = _("account")
account_link.allow_tags = True
account_link.admin_order_field = 'account__username'

View File

@ -1,7 +1,6 @@
from django import forms
from django.contrib import admin
from django.db.models.functions import Concat, Coalesce
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
@ -9,6 +8,7 @@ from orchestra.admin.utils import admin_link, change_url
from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.utils import apps
from orchestra.utils.html import get_on_site_link
from .actions import view_zone, edit_records, set_soa
from .filters import TopDomainListFilter
@ -84,22 +84,12 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
if websites:
links = []
for website in websites:
context = {
'title': _("View on site"),
'url': website.get_absolute_url(),
'image': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
}
site_link = '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
site_link = get_on_site_link(website.get_absolute_url())
admin_url = change_url(website)
link = '<a href="%s">%s %s</a>' % (admin_url, website.name, site_link)
links.append(link)
return '<br>'.join(links)
context = {
'title': _("View on site"),
'url': 'http://%s' % domain.name,
'image': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
}
site_link = '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
site_link = get_on_site_link('http://%s' % domain.name)
return _("No website %s") % site_link
display_websites.admin_order_field = 'websites__name'
display_websites.short_description = _("Websites")

View File

@ -1,19 +1,24 @@
from django.contrib import admin
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.actions import disable
from orchestra.admin.utils import change_url
from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.plugins.admin import SelectPluginAdminMixin
from orchestra.utils.apps import isinstalled
from orchestra.utils.html import get_on_site_link
from .filters import CustomURLListFilter
from .models import SaaS
from .services import SoftwareService
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active')
list_filter = ('service', 'is_active')
list_display = ('name', 'service', 'display_url', 'account_link', 'is_active')
list_filter = ('service', 'is_active', CustomURLListFilter)
search_fields = ('name', 'account__username')
change_readonly_fields = ('service',)
plugin = SoftwareService
@ -21,12 +26,33 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi
plugin_title = 'Software as a Service'
actions = (disable, list_accounts)
def display_site_domain(self, saas):
def display_url(self, saas):
site_domain = saas.get_site_domain()
return '<a href="http://%s">%s</a>' % (site_domain, site_domain)
display_site_domain.short_description = _("Site domain")
display_site_domain.allow_tags = True
display_site_domain.admin_order_field = 'name'
site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
links = [site_link]
if saas.custom_url and isinstalled('orchestra.contrib.websites'):
try:
website = saas.service_instance.get_website()
except ObjectDoesNotExist:
warning = _("Related website directive does not exist for this custom URL.")
link = '<span style="color:red" title="%s">%s</span>' % (warning, saas.custom_url)
else:
website_link = get_on_site_link(saas.custom_url)
admin_url = change_url(website)
link = '<a title="Edit website" href="%s">%s %s</a>' % (
admin_url, saas.custom_url, website_link
)
links.append(link)
return '<br>'.join(links)
display_url.short_description = _("URL")
display_url.allow_tags = True
display_url.admin_order_field = 'name'
def get_fields(self, *args, **kwargs):
fields = super(SaaSAdmin, self).get_fields(*args, **kwargs)
if not self.plugin_instance.allow_custom_url:
return [field for field in fields if field != 'custom_url']
return fields
admin.site.register(SaaS, SaaSAdmin)

View File

@ -1,6 +1,7 @@
import crypt
import os
import textwrap
from urllib.parse import urlparse
from django.utils.translation import ugettext_lazy as _
@ -43,20 +44,57 @@ class DokuWikiMuBackend(ServiceController):
echo 'admin:%(password)s:admin:%(email)s:admin,user' >> %(users_path)s
fi""") % context
)
self.append(textwrap.dedent("""\
# Update custom domain link
find %(farm_path)s \\
-maxdepth 1 \\
-type l \\
-exec bash -c '
if [[ $(readlink {}) == "%(domain)s" && $(basename {}) != "%(custom_domain)s" ]]; then
rm {}
fi' \;\
""") % context
)
if context['custom_domain']:
self.append(textwrap.dedent("""\
if [[ ! -e %(farm_path)s/%(custom_domain)s ]]; then
ln -s %(domain)s %(farm_path)s/%(custom_domain)s
chown -h %(user)s:%(group) %(farm_path)s/%(custom_domain)s
fi""") % context
)
def delete(self, saas):
context = self.get_context(saas)
self.append("rm -fr %(app_path)s" % context)
self.append(textwrap.dedent("""\
# Delete custom domain link
find %(farm_path)s \\
-maxdepth 1 \\
-type l \\
-exec bash -c '
if [[ $(readlink {}) == "%(domain)s" ]]; then
rm {}
fi' \;\
""") % context
)
def get_context(self, saas):
context = super(DokuWikiMuBackend, self).get_context(saas)
domain = saas.get_site_domain()
context.update({
'template': settings.SAAS_DOKUWIKI_TEMPLATE_PATH,
'farm_path': settings.SAAS_DOKUWIKI_FARM_PATH,
'app_path': os.path.join(settings.SAAS_DOKUWIKI_FARM_PATH, saas.get_site_domain()),
'farm_path': os.path.normpath(settings.SAAS_DOKUWIKI_FARM_PATH),
'app_path': os.path.join(settings.SAAS_DOKUWIKI_FARM_PATH, domain),
'user': settings.SAAS_DOKUWIKI_USER,
'group': settings.SAAS_DOKUWIKI_GROUP,
'email': saas.account.email,
'custom_url': saas.custom_url,
'domain': domain,
})
if saas.custom_url:
custom_url = urlparse(saas.custom_url)
context.update({
'custom_domain': custom_url.netloc,
})
password = getattr(saas, 'password', None)
salt = random_ascii(8)

View File

@ -1,5 +1,6 @@
import os
import textwrap
from urllib.parse import urlparse
from django.utils.translation import ugettext_lazy as _
@ -80,6 +81,23 @@ class MoodleMuBackend(ServiceController):
EOF
fi""") % context
)
self.delete_site_map(context)
if context['custom_url']:
self.insert_site_map(context)
def delete_site_map(self, context):
self.append(textwrap.dedent("""\
sed -i '/^\s*"[^\s]*"\s*=>\s*\["%(site_name)s",\s*".*/d' %(moodle_path)s/config.php
""") % context
)
def insert_site_map(self, context):
self.append(textwrap.dedent("""\
regex='\s*\$site_map\s+=\s+array\('
newline=' "%(custom_domain)s" => ["%(site_name)s", "%(custom_url)s"], // %(banner)s'
sed -i -r "s#$regex#\$site_map = array(\\n$newline#" %(moodle_path)s/config.php
""") % context
)
def delete(self, saas):
context = self.get_context(saas)
@ -112,6 +130,7 @@ class MoodleMuBackend(ServiceController):
| su %(user)s --shell /bin/bash -c 'crontab'
""") % context
)
self.delete_site_map(context)
def get_context(self, saas):
context = {
@ -127,6 +146,8 @@ class MoodleMuBackend(ServiceController):
'db_host': settings.SAAS_MOODLE_DB_HOST,
'email': saas.account.email,
'password': getattr(saas, 'password', None),
'custom_url': saas.custom_url,
'custom_domain': urlparse(saas.custom_url).netloc if saas.custom_url else None,
}
context.update({
'crontab': settings.SAAS_MOODLE_CRONTAB % context,

View File

@ -1,4 +1,6 @@
import re
import textwrap
from urllib.parse import urlparse
import requests
from django.utils.translation import ugettext_lazy as _
@ -12,6 +14,8 @@ from .. import settings
class WordpressMuBackend(ServiceController):
"""
Creates a wordpress site on a WordPress MultiSite installation.
You should point it to the database server
"""
verbose_name = _("Wordpress multisite")
model = 'saas.SaaS'
@ -117,10 +121,58 @@ class WordpressMuBackend(ServiceController):
def save(self, saas):
self.append(self.create_blog, saas)
context = self.get_context(saas)
self.append(textwrap.dedent("""
# Update custom URL mapping
existing=( $(mysql -Nrs %(db_name)s --execute='
SELECT b.blog_id, b.domain, m.domain, b.path
FROM wp_domain_mapping AS m, wp_blogs AS b
WHERE m.blog_id = b.blog_id AND m.active AND b.domain = "%(domain)s";') )
if [[ ${existing[0]} != '' ]]; then
if [[ "%(custom_domain)s" == "" ]]; then
mysql %(db_name)s --execute="
DELETE wp_domain_mapping AS m, wp_blogs AS b
WHERE m.blog_id = b.blog_id AND m.active AND b.domain = '%(domain)s';
UPDATE wp_blogs
SET path='/'
WHERE blog_id=${existing[0]};"
elif [[ "${existing[2]}" != "%(custom_domain)s" || "${existing[3]}" != "%(custom_path)s" ]]; then
mysql %(db_name)s --execute='
UPDATE wp_domain_mapping as m, wp_blogs as b
SET m.domain = "%(custom_domain)s", b.path = "%(custom_path)s"
WHERE m.blog_id = b.blog_id AND m.active AND b.domain = "%(domain)s";'
fi
else
blog=( $(mysql -Nrs %(db_name)s --execute='
SELECT blog_id, path FROM wp_blogs WHERE domain = "%(domain)s";') )
mysql %(db_name)s --execute='
INSERT INTO wp_domain_mapping
VALUES (blog_id, domain, active) ($blog_id, "%(custom_domain)s", 1);'
if [[ "${blog[1]}" != "%(custom_path)s" ]]; then
mysql %(db_name)s --execute="
UPDATE wp_blogs
SET path='%(custom_path)s'
WHERE blog_id=${blog[0]};"
fi
fi""") % context
)
def delete(self, saas):
self.append(self.delete_blog, saas)
def get_context(self, saas):
domain = saas.get_site_domain()
context = {
'db_name': settings.SAAS_WORDPRESS_DB_NAME,
'domain': domain,
}
if saas.custom_url:
custom_url = urlparse(saas.custom_url)
context.update({
'custom_domain': custom_url.netloc,
'custom_path': custom_url.path,
})
return context
class WordpressMuTraffic(ApacheTrafficByHost):
__doc__ = ApacheTrafficByHost.__doc__

View File

@ -0,0 +1,21 @@
from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _
class CustomURLListFilter(SimpleListFilter):
title = _("custom URL")
parameter_name = 'has_custom_url'
def lookups(self, request, model_admin):
return (
('True', _("True")),
('False', _("False")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.exclude(custom_url='')
elif self.value() == 'False':
return queryset.filter(custom_url='')
return queryset

View File

@ -1,7 +1,9 @@
from django import forms
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import change_url
from orchestra.core import validators
from orchestra.forms.widgets import SpanWidget
from orchestra.plugins.forms import PluginDataForm
@ -20,6 +22,17 @@ class SaaSBaseForm(PluginDataForm):
self.is_change = bool(self.instance and self.instance.pk)
if self.is_change:
site_domain = self.instance.get_site_domain()
if self.instance.custom_url:
try:
website = self.instance.service_instance.get_website()
except ObjectDoesNotExist:
link = ('<br><span style="color:red"><b>Warning:</b> '
'Related website directive does not exist for %s URL !</span>' %
self.instance.custom_url)
else:
url = change_url(website)
link = '<br>Related website: <a href="%s">%s</a>' % (url, website.name)
self.fields['custom_url'].help_text += link
else:
site_domain = self.plugin.site_domain
context = {

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('saas', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='saas',
name='custom_url',
field=models.URLField(verbose_name='custom URL', blank=True, help_text='Optional and alternative URL for accessing this service instance. A related website will be automatically configured if needed.'),
),
migrations.AlterField(
model_name='saas',
name='service',
field=models.CharField(choices=[('bscw', 'BSCW'), ('dokuwiki', 'Dowkuwiki'), ('drupal', 'Drupal'), ('gitlab', 'GitLab'), ('moodle', 'Moodle'), ('seafile', 'SeaFile'), ('wordpress', 'WordPress'), ('phplist', 'phpList')], verbose_name='service', max_length=32),
),
]

View File

@ -32,6 +32,10 @@ class SaaS(models.Model):
help_text=_("Designates whether this service should be treated as active. "))
data = JSONField(_("data"), default={},
help_text=_("Extra information dependent of each service."))
custom_url = models.URLField(_("custom URL"), blank=True,
help_text=_("Optional and alternative URL for accessing this service instance. "
"i.e. <tt>https://wiki.mydomain/doku/</tt><br>"
"A related website will be automatically configured if needed."))
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
@ -68,6 +72,7 @@ class SaaS(models.Model):
def clean(self):
if not self.pk:
self.name = self.name.lower()
self.service_instance.clean()
self.data = self.service_instance.clean_data()
def get_site_domain(self):

View File

@ -1,3 +1,8 @@
from urllib.parse import urlparse
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from .options import SoftwareService
from .. import settings
@ -7,3 +12,13 @@ class DokuWikiService(SoftwareService):
verbose_name = "Dowkuwiki"
icon = 'orchestra/icons/apps/Dokuwiki.png'
site_domain = settings.SAAS_DOKUWIKI_DOMAIN
allow_custom_url = settings.SAAS_DOKUWIKI_ALLOW_CUSTOM_URL
def clean(self):
if self.allow_custom_url and self.instance.custom_url:
url = urlparse(self.instance.custom_url)
if url.path and url.path != '/':
raise ValidationError({
'custom_url': _("Support for specific URL paths (%s) is not implemented.") % url.path
})
super(DokuWikiService, self).clean()

View File

@ -0,0 +1,130 @@
from collections import defaultdict
from urllib.parse import urlparse
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.websites.models import Website, WebsiteDirective, Content
from orchestra.contrib.websites.utils import normurlpath
from orchestra.contrib.websites.validators import validate_domain_protocol
from orchestra.utils.python import AttrDict
def full_clean(obj, exclude=None):
try:
obj.full_clean(exclude=exclude)
except ValidationError as e:
raise ValidationError({
'custom_url': _("Error validating related %s: %s") % (type(obj).__name__, e),
})
def clean_custom_url(saas):
instance = saas.instance
instance.custom_url = instance.custom_url.strip()
url = urlparse(instance.custom_url)
if not url.path:
instance.custom_url += '/'
url = urlparse(instance.custom_url)
try:
protocol, valid_protocols = saas.PROTOCOL_MAP[url.scheme]
except KeyError:
raise ValidationError({
'custom_url': _("%s scheme not supported (http/https)") % url.scheme,
})
account = instance.account
# get or create website
try:
website = Website.objects.get(
protocol__in=valid_protocols,
domains__name=url.netloc,
account=account,
)
except Website.DoesNotExist:
# get or create domain
Domain = Website.domains.field.rel.to
try:
domain = Domain.objects.get(name=url.netloc)
except Domain.DoesNotExist:
raise ValidationError({
'custom_url': _("Domain %s does not exist.") % url.netloc,
})
if domain.account != account:
raise ValidationError({
'custom_url': _("Domain %s does not belong to account %s, it's from %s.") %
(url.netloc, account, domain.account),
})
# Create new website for custom_url
website = Website(name=url.netloc , protocol=protocol, account=account)
full_clean(website)
try:
validate_domain_protocol(website, domain, protocol)
except ValidationError as e:
raise ValidationError({
'custom_url': _("Error validating related %s: %s") % (type(website).__name__, e),
})
# get or create directive
try:
directive = website.directives.get(name=saas.get_directive_name())
except WebsiteDirective.DoesNotExist:
directive = WebsiteDirective(name=saas.get_directive_name(), value=url.path)
if not directive.pk or directive.value != url.path:
directive.value = url.path
if website.pk:
directive.website = website
full_clean(directive)
# Adaptation of orchestra.websites.forms.WebsiteDirectiveInlineFormSet.clean()
locations = set(
Content.objects.filter(website=website).values_list('path', flat=True)
)
values = defaultdict(list)
for wdirective in WebsiteDirective.objects.filter(website=website).exclude(pk=directive.pk):
fdirective = AttrDict({
'name': wdirective.name,
'value': wdirective.value
})
try:
wdirective.directive_instance.validate_uniqueness(fdirective, values, locations)
except ValidationError as err:
raise ValidationError({
'custom_url': _("Another directive with this URL path exists (%s)." % err)
})
else:
full_clean(directive, exclude=('website',))
return directive
def create_or_update_directive(saas):
instance = saas.instance
url = urlparse(instance.custom_url)
protocol, valid_protocols = saas.PROTOCOL_MAP[url.scheme]
account = instance.account
# get or create website
try:
website = Website.objects.get(
protocol__in=valid_protocols,
domains__name=url.netloc,
account=account,
)
except Website.DoesNotExist:
Domain = Website.domains.field.rel.to
domain = Domain.objects.get(name=url.netloc)
# Create new website for custom_url
website = Website(name=url.netloc , protocol=protocol, account=account)
website.save()
website.domains.add(domain)
# get or create directive
try:
directive = website.directives.get(name=saas.get_directive_name())
except WebsiteDirective.DoesNotExist:
directive = WebsiteDirective(name=saas.get_directive_name(), value=url.path)
if not directive.pk or directive.value != url.path:
directive.value = url.path
directive.website = website
directive.save()
return directive
def update_directive(saas):
saas.instance.custom_url = saas.instance.custom_url.strip()
url = urlparse(saas.instance.custom_url)

View File

@ -20,5 +20,6 @@ class MoodleService(SoftwareService):
description_field = 'site_name'
icon = 'orchestra/icons/apps/Moodle.png'
site_domain = settings.SAAS_MOODLE_DOMAIN
allow_custom_url = settings.SAAS_MOODLE_ALLOW_CUSTOM_URL
db_name = settings.SAAS_MOODLE_DB_NAME
db_user = settings.SAAS_MOODLE_DB_USER

View File

@ -1,23 +1,36 @@
from django.core.exceptions import ValidationError
from urllib.parse import urlparse
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from orchestra import plugins
from orchestra.contrib.databases.models import Database, DatabaseUser
from orchestra.contrib.orchestration import Operation
from orchestra.contrib.websites.models import Website, WebsiteDirective
from orchestra.utils.apps import isinstalled
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
from . import helpers
from .. import settings
from ..forms import SaaSPasswordForm
class SoftwareService(plugins.Plugin):
PROTOCOL_MAP = {
'http': (Website.HTTP, (Website.HTTP, Website.HTTP_AND_HTTPS)),
'https': (Website.HTTPS_ONLY, (Website.HTTPS, Website.HTTP_AND_HTTPS, Website.HTTPS_ONLY)),
}
name = None
verbose_name = None
form = SaaSPasswordForm
site_domain = None
has_custom_domain = False
icon = 'orchestra/icons/apps.png'
class_verbose_name = _("Software as a Service")
plugin_field = 'service'
allow_custom_url = False
@classmethod
@cached
@ -38,6 +51,16 @@ class SoftwareService(plugins.Plugin):
}
return self.site_domain % context
def clean(self):
if self.allow_custom_url:
if self.instance.custom_url:
if isinstalled('orchestra.contrib.websites'):
helpers.clean_custom_url(self)
elif self.instance.custom_url:
raise ValidationError({
'custom_url': _("Custom URL not allowed for this service."),
})
def clean_data(self):
data = super(SoftwareService, self).clean_data()
if not self.instance.pk:
@ -57,11 +80,58 @@ class SoftwareService(plugins.Plugin):
raise ValidationError(errors)
return data
def save(self):
def get_directive_name(self):
return '%s-saas' % self.name
def get_directive(self, *args):
if not args:
instance = self.instance
else:
instance = args[0]
url = urlparse(instance.custom_url)
account = instance.account
return WebsiteDirective.objects.get(
name=self.get_directive_name(),
value=url.path,
website__protocol__in=self.PROTOCOL_MAP[url.scheme][1],
website__domains__name=url.netloc,
website__account=account,
)
def get_website(self):
url = urlparse(self.instance.custom_url)
account = self.instance.account
return Website.objects.get(
protocol__in=self.PROTOCOL_MAP[url.scheme][1],
domains__name=url.netloc,
account=account,
directives__name=self.get_directive_name(),
directives__value=url.path,
)
def create_or_update_directive(self):
return helpers.create_or_update_directive(self)
def delete_directive(self):
try:
old = type(self.instance).objects.get(pk=self.instance.pk)
directive = self.get_directive(old)
except ObjectDoesNotExist:
pass
else:
directive.delete()
def save(self):
# pre instance.save()
if isinstalled('orchestra.contrib.websites'):
if self.instance.custom_url:
self.create_or_update_directive()
elif self.instance.pk:
self.delete_directive()
def delete(self):
pass
if isinstalled('orchestra.contrib.websites'):
self.delete_directive()
def get_related(self):
return []
@ -112,6 +182,7 @@ class DBSoftwareService(SoftwareService):
})
def save(self):
super(DBSoftwareService, self).save()
account = self.get_account()
# Database
db_name = self.get_db_name()

View File

@ -70,6 +70,7 @@ class PHPListService(DBSoftwareService):
change_form = PHPListChangeForm
icon = 'orchestra/icons/apps/Phplist.png'
site_domain = settings.SAAS_PHPLIST_DOMAIN
allow_custom_url = settings.SAAS_PHPLIST_ALLOW_CUSTOM_URL
db_name = settings.SAAS_PHPLIST_DB_NAME
db_user = settings.SAAS_PHPLIST_DB_USER
@ -95,6 +96,7 @@ class PHPListService(DBSoftwareService):
})
def save(self):
super(PHPListService, self).save()
account = self.get_account()
# Mailbox
mailbox_name = self.get_mailbox_name()
@ -108,6 +110,7 @@ class PHPListService(DBSoftwareService):
})
def delete(self):
super(PHPListService, self).save()
account = self.get_account()
# delete Mailbox (database will be deleted by ORM's cascade behaviour
mailbox_name = self.instance.data.get('mailbox_name') or self.get_mailbox_name()

View File

@ -33,3 +33,4 @@ class WordPressService(SoftwareService):
icon = 'orchestra/icons/apps/WordPress.png'
change_readonly_fileds = ('email',)
site_domain = settings.SAAS_WORDPRESS_DOMAIN
allow_custom_url = settings.SAAS_WORDPRESS_ALLOW_CUSTOM_URL

View File

@ -33,6 +33,11 @@ SAAS_TRAFFIC_IGNORE_HOSTS = Setting('SAAS_TRAFFIC_IGNORE_HOSTS',
# WordPress
SAAS_WORDPRESS_ALLOW_CUSTOM_URL = Setting('SAAS_WORDPRESS_ALLOW_CUSTOM_URL',
True,
help_text=_("Whether allow custom URL to be specified or not."),
)
SAAS_WORDPRESS_LOG_PATH = Setting('SAAS_WORDPRESS_LOG_PATH',
'',
help_text=_('Filesystem path for the webserver access logs.<br>'
@ -52,9 +57,19 @@ SAAS_WORDPRESS_DOMAIN = Setting('SAAS_WORDPRESS_DOMAIN',
'%(site_name)s.blogs.{}'.format(ORCHESTRA_BASE_DOMAIN),
)
SAAS_WORDPRESS_DB_NAME = Setting('SAAS_WORDPRESS_DB_NAME',
'wordpressmu',
help_text=_("Needed for domain mapping when <tt>SAAS_WORDPRESS_ALLOW_CUSTOM_URL</tt> is enabled."),
)
# DokuWiki
SAAS_DOKUWIKI_ALLOW_CUSTOM_URL = Setting('SAAS_DOKUWIKI_ALLOW_CUSTOM_URL',
True,
help_text=_("Whether allow custom URL to be specified or not."),
)
SAAS_DOKUWIKI_TEMPLATE_PATH = Setting('SAAS_DOKUWIKI_TEMPLATE_PATH',
'/home/httpd/htdocs/wikifarm/template.tar.gz'
)
@ -90,6 +105,11 @@ SAAS_DOKUWIKI_LOG_PATH = Setting('SAAS_DOKUWIKI_LOG_PATH',
# Drupal
SAAS_DRUPAL_ALLOW_CUSTOM_URL = Setting('SAAS_DRUPAL_ALLOW_CUSTOM_URL',
True,
help_text=_("Whether allow custom URL to be specified or not."),
)
SAAS_DRUPAL_SITES_PATH = Setting('WEBSITES_DRUPAL_SITES_PATH',
'/home/httpd/htdocs/drupal-mu/sites/%(site_name)s',
)
@ -97,6 +117,11 @@ SAAS_DRUPAL_SITES_PATH = Setting('WEBSITES_DRUPAL_SITES_PATH',
# PhpList
SAAS_PHPLIST_ALLOW_CUSTOM_URL = Setting('SAAS_PHPLIST_ALLOW_CUSTOM_URL',
False,
help_text=_("Whether allow custom URL to be specified or not."),
)
SAAS_PHPLIST_DB_USER = Setting('SAAS_PHPLIST_DB_USER',
'phplist_mu',
help_text=_("Needed for password changing support."),
@ -201,6 +226,11 @@ SAAS_GITLAB_DOMAIN = Setting('SAAS_GITLAB_DOMAIN',
# Moodle
SAAS_MOODLE_ALLOW_CUSTOM_URL = Setting('SAAS_MOODLE_ALLOW_CUSTOM_URL',
True,
help_text=_("Whether allow custom URL to be specified or not."),
)
SAAS_MOODLE_DB_USER = Setting('SAAS_MOODLE_DB_USER',
'moodle_mu',
help_text=_("Needed for password changing support."),

View File

@ -1,7 +1,6 @@
from django import forms
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.templatetags.static import static
from django.utils.encoding import force_text
from django.utils.translation import ugettext, ugettext_lazy as _
@ -11,6 +10,7 @@ from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect
from orchestra.plugins.admin import SelectPluginAdminMixin
from orchestra.utils.html import get_on_site_link
from .filters import HasWebsiteListFilter, PHPVersionListFilter
from .models import WebApp, WebAppOption
@ -65,12 +65,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
def display_websites(self, webapp):
websites = []
for content in webapp.content_set.all():
context = {
'title': _("View on site"),
'url': content.get_absolute_url(),
'image': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
}
site_link = '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
site_link = get_on_site_link(content.get_absolute_url())
website = content.website
admin_url = change_url(website)
name = "%s on %s" % (website.name, content.path)

View File

@ -5,7 +5,7 @@ from . import settings
class HasWebsiteListFilter(SimpleListFilter):
title = _("Has website")
title = _("website")
parameter_name = 'has_website'
def lookups(self, request, model_admin):

View File

@ -95,7 +95,8 @@ class WebApp(models.Model):
class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
related_name='options')
name = models.CharField(_("name"), max_length=128, choices=AppType.get_group_options_choices())
name = models.CharField(_("name"), max_length=128,
choices=AppType.get_group_options_choices())
value = models.CharField(_("value"), max_length=256)
class Meta:

View File

@ -175,7 +175,7 @@ class SecEngine(SecRuleRemove):
class WordPressSaaS(SiteDirective):
name = 'wordpress-saas'
verbose_name = "WordPress SaaS"
help_text = _("URL-path for mounting wordpress multisite.")
help_text = _("URL-path for mounting WordPress multisite.")
group = SiteDirective.SAAS
regex = r'^/[^ ]*$'
unique_value = True
@ -185,10 +185,16 @@ class WordPressSaaS(SiteDirective):
class DokuWikiSaaS(WordPressSaaS):
name = 'dokuwiki-saas'
verbose_name = "DokuWiki SaaS"
help_text = _("URL-path for mounting wordpress multisite.")
help_text = _("URL-path for mounting DokuWiki multisite.")
class DrupalSaaS(WordPressSaaS):
name = 'drupal-saas'
verbose_name = "Drupdal SaaS"
help_text = _("URL-path for mounting wordpress multisite.")
help_text = _("URL-path for mounting Drupal multisite.")
class MoodleSaaS(WordPressSaaS):
name = 'moodle-saas'
verbose_name = "Moodle SaaS"
help_text = _("URL-path for mounting Moodle multisite.")

View File

@ -36,15 +36,14 @@ class WebsiteDirectiveInlineFormSet(forms.models.BaseInlineFormSet):
location = form.cleaned_data.get('path')
if location is not None:
locations.add(normurlpath(location))
directives = []
values = defaultdict(list)
for form in self.forms:
website = form.instance
wdirective = form.instance
directive = form.cleaned_data
if directive.get('name') is not None:
try:
website.directive_instance.validate_uniqueness(directive, values, locations)
wdirective.directive_instance.validate_uniqueness(directive, values, locations)
except ValidationError as err:
for k,v in err.error_dict.items():
form.add_error(k, v)

View File

@ -58,6 +58,7 @@ WEBSITES_ENABLED_DIRECTIVES = Setting('WEBSITES_ENABLED_DIRECTIVES',
'orchestra.contrib.websites.directives.WordPressSaaS',
'orchestra.contrib.websites.directives.DokuWikiSaaS',
'orchestra.contrib.websites.directives.DrupalSaaS',
'orchestra.contrib.websites.directives.MoodleSaaS',
),
# lazy loading
choices=lambda : ((d.get_class_path(), d.get_class_path()) for d in websites.directives.SiteDirective.get_plugins()),

View File

@ -20,6 +20,7 @@ class SelectPluginAdminMixin(object):
else:
plugin = self.plugin.get(self.plugin_value)()
self.form = plugin.get_form()
self.plugin_instance = plugin
return super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs)
def get_fields(self, request, obj=None):
@ -83,8 +84,8 @@ class SelectPluginAdminMixin(object):
'title': _("Add new %s") % plugin.verbose_name,
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url,
extra_context=context)
return super(SelectPluginAdminMixin, self).add_view(
request, form_url=form_url, extra_context=context)
return redirect('./select-plugin/?%s' % request.META['QUERY_STRING'])
def change_view(self, request, object_id, form_url='', extra_context=None):
@ -94,8 +95,8 @@ class SelectPluginAdminMixin(object):
'title': _("Change %s") % plugin.verbose_name,
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).change_view(request, object_id,
form_url=form_url, extra_context=context)
return super(SelectPluginAdminMixin, self).change_view(
request, object_id, form_url=form_url, extra_context=context)
def save_model(self, request, obj, form, change):
if not change:

View File

@ -1,5 +1,8 @@
import textwrap
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
from orchestra.utils.sys import run
@ -22,3 +25,12 @@ def html_to_pdf(html, pagination=False):
--margin-top 20 - - \
""") % context
return run(cmd, stdin=html.encode('utf-8')).stdout
def get_on_site_link(url):
context = {
'title': _("View on site"),
'url': url,
'image': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
}
return '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context