Added support for SaaS service custom URL
This commit is contained in:
parent
0f603181ff
commit
95a6a0c37d
21
TODO.md
21
TODO.md
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import crypt
|
||||
import os
|
||||
import textwrap
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -43,21 +44,58 @@ 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)
|
||||
context.update({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__
|
||||
|
|
21
orchestra/contrib/saas/filters.py
Normal file
21
orchestra/contrib/saas/filters.py
Normal 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
|
||||
|
|
@ -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 = {
|
||||
|
|
24
orchestra/contrib/saas/migrations/0002_auto_20151001_0923.py
Normal file
24
orchestra/contrib/saas/migrations/0002_auto_20151001_0923.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
130
orchestra/contrib/saas/services/helpers.py
Normal file
130
orchestra/contrib/saas/services/helpers.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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 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):
|
||||
pass
|
||||
# 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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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):
|
||||
|
@ -65,7 +66,7 @@ class SelectPluginAdminMixin(object):
|
|||
if not plugin_value and request.method == 'POST':
|
||||
# HACK baceuse django add_preserved_filters removes extising queryargs
|
||||
value = re.search(r"%s=([^&^']+)[&']" % self.plugin_field,
|
||||
request.META.get('HTTP_REFERER', ''))
|
||||
request.META.get('HTTP_REFERER', ''))
|
||||
if value:
|
||||
plugin_value = value.groups()[0]
|
||||
return plugin_value
|
||||
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue