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), ...
|
# 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 --dev
|
||||||
deploy.sh and deploy-dev.sh autoupgrade
|
deploy.sh and deploy-dev.sh autoupgrade
|
||||||
|
@ -401,7 +396,6 @@ orchestra home autocomplete
|
||||||
short URLS: https://github.com/rsvp/gitio
|
short URLS: https://github.com/rsvp/gitio
|
||||||
|
|
||||||
link backend help text variables to settings/#var_name
|
link backend help text variables to settings/#var_name
|
||||||
saas changelist domain: add <br>custom domain<img>
|
|
||||||
|
|
||||||
$ sudo python manage.py startservices
|
$ sudo python manage.py startservices
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
|
@ -409,3 +403,18 @@ Traceback (most recent call last):
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
ImportError: No module named django.core.management
|
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):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
url = change_url(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.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
account_link.allow_tags = True
|
||||||
account_link.admin_order_field = 'account__username'
|
account_link.admin_order_field = 'account__username'
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
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.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.utils import apps
|
from orchestra.utils import apps
|
||||||
|
from orchestra.utils.html import get_on_site_link
|
||||||
|
|
||||||
from .actions import view_zone, edit_records, set_soa
|
from .actions import view_zone, edit_records, set_soa
|
||||||
from .filters import TopDomainListFilter
|
from .filters import TopDomainListFilter
|
||||||
|
@ -84,22 +84,12 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if websites:
|
if websites:
|
||||||
links = []
|
links = []
|
||||||
for website in websites:
|
for website in websites:
|
||||||
context = {
|
site_link = get_on_site_link(website.get_absolute_url())
|
||||||
'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
|
|
||||||
admin_url = change_url(website)
|
admin_url = change_url(website)
|
||||||
link = '<a href="%s">%s %s</a>' % (admin_url, website.name, site_link)
|
link = '<a href="%s">%s %s</a>' % (admin_url, website.name, site_link)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
context = {
|
site_link = get_on_site_link('http://%s' % domain.name)
|
||||||
'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
|
|
||||||
return _("No website %s") % site_link
|
return _("No website %s") % site_link
|
||||||
display_websites.admin_order_field = 'websites__name'
|
display_websites.admin_order_field = 'websites__name'
|
||||||
display_websites.short_description = _("Websites")
|
display_websites.short_description = _("Websites")
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
from orchestra.admin.actions import disable
|
from orchestra.admin.actions import disable
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.plugins.admin import SelectPluginAdminMixin
|
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 .models import SaaS
|
||||||
from .services import SoftwareService
|
from .services import SoftwareService
|
||||||
|
|
||||||
|
|
||||||
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
|
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active')
|
list_display = ('name', 'service', 'display_url', 'account_link', 'is_active')
|
||||||
list_filter = ('service', 'is_active')
|
list_filter = ('service', 'is_active', CustomURLListFilter)
|
||||||
search_fields = ('name', 'account__username')
|
search_fields = ('name', 'account__username')
|
||||||
change_readonly_fields = ('service',)
|
change_readonly_fields = ('service',)
|
||||||
plugin = SoftwareService
|
plugin = SoftwareService
|
||||||
|
@ -21,12 +26,33 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi
|
||||||
plugin_title = 'Software as a Service'
|
plugin_title = 'Software as a Service'
|
||||||
actions = (disable, list_accounts)
|
actions = (disable, list_accounts)
|
||||||
|
|
||||||
def display_site_domain(self, saas):
|
def display_url(self, saas):
|
||||||
site_domain = saas.get_site_domain()
|
site_domain = saas.get_site_domain()
|
||||||
return '<a href="http://%s">%s</a>' % (site_domain, site_domain)
|
site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
|
||||||
display_site_domain.short_description = _("Site domain")
|
links = [site_link]
|
||||||
display_site_domain.allow_tags = True
|
if saas.custom_url and isinstalled('orchestra.contrib.websites'):
|
||||||
display_site_domain.admin_order_field = 'name'
|
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)
|
admin.site.register(SaaS, SaaSAdmin)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import crypt
|
import crypt
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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
|
echo 'admin:%(password)s:admin:%(email)s:admin,user' >> %(users_path)s
|
||||||
fi""") % context
|
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):
|
def delete(self, saas):
|
||||||
context = self.get_context(saas)
|
context = self.get_context(saas)
|
||||||
self.append("rm -fr %(app_path)s" % context)
|
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):
|
def get_context(self, saas):
|
||||||
context = super(DokuWikiMuBackend, self).get_context(saas)
|
context = super(DokuWikiMuBackend, self).get_context(saas)
|
||||||
|
domain = saas.get_site_domain()
|
||||||
context.update({
|
context.update({
|
||||||
'template': settings.SAAS_DOKUWIKI_TEMPLATE_PATH,
|
'template': settings.SAAS_DOKUWIKI_TEMPLATE_PATH,
|
||||||
'farm_path': settings.SAAS_DOKUWIKI_FARM_PATH,
|
'farm_path': os.path.normpath(settings.SAAS_DOKUWIKI_FARM_PATH),
|
||||||
'app_path': os.path.join(settings.SAAS_DOKUWIKI_FARM_PATH, saas.get_site_domain()),
|
'app_path': os.path.join(settings.SAAS_DOKUWIKI_FARM_PATH, domain),
|
||||||
'user': settings.SAAS_DOKUWIKI_USER,
|
'user': settings.SAAS_DOKUWIKI_USER,
|
||||||
'group': settings.SAAS_DOKUWIKI_GROUP,
|
'group': settings.SAAS_DOKUWIKI_GROUP,
|
||||||
'email': saas.account.email,
|
'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)
|
password = getattr(saas, 'password', None)
|
||||||
salt = random_ascii(8)
|
salt = random_ascii(8)
|
||||||
context.update({
|
context.update({
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -80,6 +81,23 @@ class MoodleMuBackend(ServiceController):
|
||||||
EOF
|
EOF
|
||||||
fi""") % context
|
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):
|
def delete(self, saas):
|
||||||
context = self.get_context(saas)
|
context = self.get_context(saas)
|
||||||
|
@ -112,6 +130,7 @@ class MoodleMuBackend(ServiceController):
|
||||||
| su %(user)s --shell /bin/bash -c 'crontab'
|
| su %(user)s --shell /bin/bash -c 'crontab'
|
||||||
""") % context
|
""") % context
|
||||||
)
|
)
|
||||||
|
self.delete_site_map(context)
|
||||||
|
|
||||||
def get_context(self, saas):
|
def get_context(self, saas):
|
||||||
context = {
|
context = {
|
||||||
|
@ -127,6 +146,8 @@ class MoodleMuBackend(ServiceController):
|
||||||
'db_host': settings.SAAS_MOODLE_DB_HOST,
|
'db_host': settings.SAAS_MOODLE_DB_HOST,
|
||||||
'email': saas.account.email,
|
'email': saas.account.email,
|
||||||
'password': getattr(saas, 'password', None),
|
'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({
|
context.update({
|
||||||
'crontab': settings.SAAS_MOODLE_CRONTAB % context,
|
'crontab': settings.SAAS_MOODLE_CRONTAB % context,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -12,6 +14,8 @@ from .. import settings
|
||||||
class WordpressMuBackend(ServiceController):
|
class WordpressMuBackend(ServiceController):
|
||||||
"""
|
"""
|
||||||
Creates a wordpress site on a WordPress MultiSite installation.
|
Creates a wordpress site on a WordPress MultiSite installation.
|
||||||
|
|
||||||
|
You should point it to the database server
|
||||||
"""
|
"""
|
||||||
verbose_name = _("Wordpress multisite")
|
verbose_name = _("Wordpress multisite")
|
||||||
model = 'saas.SaaS'
|
model = 'saas.SaaS'
|
||||||
|
@ -117,10 +121,58 @@ class WordpressMuBackend(ServiceController):
|
||||||
|
|
||||||
def save(self, saas):
|
def save(self, saas):
|
||||||
self.append(self.create_blog, 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):
|
def delete(self, saas):
|
||||||
self.append(self.delete_blog, 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):
|
class WordpressMuTraffic(ApacheTrafficByHost):
|
||||||
__doc__ = ApacheTrafficByHost.__doc__
|
__doc__ = ApacheTrafficByHost.__doc__
|
||||||
|
|
|
@ -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 import forms
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.core import validators
|
from orchestra.core import validators
|
||||||
from orchestra.forms.widgets import SpanWidget
|
from orchestra.forms.widgets import SpanWidget
|
||||||
from orchestra.plugins.forms import PluginDataForm
|
from orchestra.plugins.forms import PluginDataForm
|
||||||
|
@ -20,6 +22,17 @@ class SaaSBaseForm(PluginDataForm):
|
||||||
self.is_change = bool(self.instance and self.instance.pk)
|
self.is_change = bool(self.instance and self.instance.pk)
|
||||||
if self.is_change:
|
if self.is_change:
|
||||||
site_domain = self.instance.get_site_domain()
|
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:
|
else:
|
||||||
site_domain = self.plugin.site_domain
|
site_domain = self.plugin.site_domain
|
||||||
context = {
|
context = {
|
||||||
|
|
|
@ -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. "))
|
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."))
|
||||||
|
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)
|
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
|
# 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):
|
def clean(self):
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
self.name = self.name.lower()
|
self.name = self.name.lower()
|
||||||
|
self.service_instance.clean()
|
||||||
self.data = self.service_instance.clean_data()
|
self.data = self.service_instance.clean_data()
|
||||||
|
|
||||||
def get_site_domain(self):
|
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 .options import SoftwareService
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
@ -7,3 +12,13 @@ class DokuWikiService(SoftwareService):
|
||||||
verbose_name = "Dowkuwiki"
|
verbose_name = "Dowkuwiki"
|
||||||
icon = 'orchestra/icons/apps/Dokuwiki.png'
|
icon = 'orchestra/icons/apps/Dokuwiki.png'
|
||||||
site_domain = settings.SAAS_DOKUWIKI_DOMAIN
|
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()
|
||||||
|
|
|
@ -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'
|
description_field = 'site_name'
|
||||||
icon = 'orchestra/icons/apps/Moodle.png'
|
icon = 'orchestra/icons/apps/Moodle.png'
|
||||||
site_domain = settings.SAAS_MOODLE_DOMAIN
|
site_domain = settings.SAAS_MOODLE_DOMAIN
|
||||||
|
allow_custom_url = settings.SAAS_MOODLE_ALLOW_CUSTOM_URL
|
||||||
db_name = settings.SAAS_MOODLE_DB_NAME
|
db_name = settings.SAAS_MOODLE_DB_NAME
|
||||||
db_user = settings.SAAS_MOODLE_DB_USER
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra import plugins
|
from orchestra import plugins
|
||||||
from orchestra.contrib.databases.models import Database, DatabaseUser
|
from orchestra.contrib.databases.models import Database, DatabaseUser
|
||||||
from orchestra.contrib.orchestration import Operation
|
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.functional import cached
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class
|
||||||
|
|
||||||
|
from . import helpers
|
||||||
from .. import settings
|
from .. import settings
|
||||||
from ..forms import SaaSPasswordForm
|
from ..forms import SaaSPasswordForm
|
||||||
|
|
||||||
|
|
||||||
class SoftwareService(plugins.Plugin):
|
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
|
form = SaaSPasswordForm
|
||||||
site_domain = None
|
site_domain = None
|
||||||
has_custom_domain = False
|
has_custom_domain = False
|
||||||
icon = 'orchestra/icons/apps.png'
|
icon = 'orchestra/icons/apps.png'
|
||||||
class_verbose_name = _("Software as a Service")
|
class_verbose_name = _("Software as a Service")
|
||||||
plugin_field = 'service'
|
plugin_field = 'service'
|
||||||
|
allow_custom_url = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@cached
|
@cached
|
||||||
|
@ -38,6 +51,16 @@ class SoftwareService(plugins.Plugin):
|
||||||
}
|
}
|
||||||
return self.site_domain % context
|
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):
|
def clean_data(self):
|
||||||
data = super(SoftwareService, self).clean_data()
|
data = super(SoftwareService, self).clean_data()
|
||||||
if not self.instance.pk:
|
if not self.instance.pk:
|
||||||
|
@ -57,11 +80,58 @@ class SoftwareService(plugins.Plugin):
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
return data
|
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):
|
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):
|
def delete(self):
|
||||||
pass
|
if isinstalled('orchestra.contrib.websites'):
|
||||||
|
self.delete_directive()
|
||||||
|
|
||||||
def get_related(self):
|
def get_related(self):
|
||||||
return []
|
return []
|
||||||
|
@ -112,6 +182,7 @@ class DBSoftwareService(SoftwareService):
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
super(DBSoftwareService, self).save()
|
||||||
account = self.get_account()
|
account = self.get_account()
|
||||||
# Database
|
# Database
|
||||||
db_name = self.get_db_name()
|
db_name = self.get_db_name()
|
||||||
|
|
|
@ -70,6 +70,7 @@ class PHPListService(DBSoftwareService):
|
||||||
change_form = PHPListChangeForm
|
change_form = PHPListChangeForm
|
||||||
icon = 'orchestra/icons/apps/Phplist.png'
|
icon = 'orchestra/icons/apps/Phplist.png'
|
||||||
site_domain = settings.SAAS_PHPLIST_DOMAIN
|
site_domain = settings.SAAS_PHPLIST_DOMAIN
|
||||||
|
allow_custom_url = settings.SAAS_PHPLIST_ALLOW_CUSTOM_URL
|
||||||
db_name = settings.SAAS_PHPLIST_DB_NAME
|
db_name = settings.SAAS_PHPLIST_DB_NAME
|
||||||
db_user = settings.SAAS_PHPLIST_DB_USER
|
db_user = settings.SAAS_PHPLIST_DB_USER
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ class PHPListService(DBSoftwareService):
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
super(PHPListService, self).save()
|
||||||
account = self.get_account()
|
account = self.get_account()
|
||||||
# Mailbox
|
# Mailbox
|
||||||
mailbox_name = self.get_mailbox_name()
|
mailbox_name = self.get_mailbox_name()
|
||||||
|
@ -108,6 +110,7 @@ class PHPListService(DBSoftwareService):
|
||||||
})
|
})
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
super(PHPListService, self).save()
|
||||||
account = self.get_account()
|
account = self.get_account()
|
||||||
# delete Mailbox (database will be deleted by ORM's cascade behaviour
|
# delete Mailbox (database will be deleted by ORM's cascade behaviour
|
||||||
mailbox_name = self.instance.data.get('mailbox_name') or self.get_mailbox_name()
|
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'
|
icon = 'orchestra/icons/apps/WordPress.png'
|
||||||
change_readonly_fileds = ('email',)
|
change_readonly_fileds = ('email',)
|
||||||
site_domain = settings.SAAS_WORDPRESS_DOMAIN
|
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
|
# 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',
|
SAAS_WORDPRESS_LOG_PATH = Setting('SAAS_WORDPRESS_LOG_PATH',
|
||||||
'',
|
'',
|
||||||
help_text=_('Filesystem path for the webserver access logs.<br>'
|
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),
|
'%(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
|
# 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',
|
SAAS_DOKUWIKI_TEMPLATE_PATH = Setting('SAAS_DOKUWIKI_TEMPLATE_PATH',
|
||||||
'/home/httpd/htdocs/wikifarm/template.tar.gz'
|
'/home/httpd/htdocs/wikifarm/template.tar.gz'
|
||||||
)
|
)
|
||||||
|
@ -90,6 +105,11 @@ SAAS_DOKUWIKI_LOG_PATH = Setting('SAAS_DOKUWIKI_LOG_PATH',
|
||||||
|
|
||||||
# Drupal
|
# 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',
|
SAAS_DRUPAL_SITES_PATH = Setting('WEBSITES_DRUPAL_SITES_PATH',
|
||||||
'/home/httpd/htdocs/drupal-mu/sites/%(site_name)s',
|
'/home/httpd/htdocs/drupal-mu/sites/%(site_name)s',
|
||||||
)
|
)
|
||||||
|
@ -97,6 +117,11 @@ SAAS_DRUPAL_SITES_PATH = Setting('WEBSITES_DRUPAL_SITES_PATH',
|
||||||
|
|
||||||
# PhpList
|
# 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',
|
SAAS_PHPLIST_DB_USER = Setting('SAAS_PHPLIST_DB_USER',
|
||||||
'phplist_mu',
|
'phplist_mu',
|
||||||
help_text=_("Needed for password changing support."),
|
help_text=_("Needed for password changing support."),
|
||||||
|
@ -201,6 +226,11 @@ SAAS_GITLAB_DOMAIN = Setting('SAAS_GITLAB_DOMAIN',
|
||||||
|
|
||||||
# Moodle
|
# 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',
|
SAAS_MOODLE_DB_USER = Setting('SAAS_MOODLE_DB_USER',
|
||||||
'moodle_mu',
|
'moodle_mu',
|
||||||
help_text=_("Needed for password changing support."),
|
help_text=_("Needed for password changing support."),
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
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.contrib.accounts.admin import AccountAdminMixin
|
||||||
from orchestra.forms.widgets import DynamicHelpTextSelect
|
from orchestra.forms.widgets import DynamicHelpTextSelect
|
||||||
from orchestra.plugins.admin import SelectPluginAdminMixin
|
from orchestra.plugins.admin import SelectPluginAdminMixin
|
||||||
|
from orchestra.utils.html import get_on_site_link
|
||||||
|
|
||||||
from .filters import HasWebsiteListFilter, PHPVersionListFilter
|
from .filters import HasWebsiteListFilter, PHPVersionListFilter
|
||||||
from .models import WebApp, WebAppOption
|
from .models import WebApp, WebAppOption
|
||||||
|
@ -65,12 +65,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
|
||||||
def display_websites(self, webapp):
|
def display_websites(self, webapp):
|
||||||
websites = []
|
websites = []
|
||||||
for content in webapp.content_set.all():
|
for content in webapp.content_set.all():
|
||||||
context = {
|
site_link = get_on_site_link(content.get_absolute_url())
|
||||||
'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
|
|
||||||
website = content.website
|
website = content.website
|
||||||
admin_url = change_url(website)
|
admin_url = change_url(website)
|
||||||
name = "%s on %s" % (website.name, content.path)
|
name = "%s on %s" % (website.name, content.path)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class HasWebsiteListFilter(SimpleListFilter):
|
class HasWebsiteListFilter(SimpleListFilter):
|
||||||
title = _("Has website")
|
title = _("website")
|
||||||
parameter_name = 'has_website'
|
parameter_name = 'has_website'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
|
|
|
@ -95,7 +95,8 @@ class WebApp(models.Model):
|
||||||
class WebAppOption(models.Model):
|
class WebAppOption(models.Model):
|
||||||
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
|
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
|
||||||
related_name='options')
|
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)
|
value = models.CharField(_("value"), max_length=256)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -175,7 +175,7 @@ class SecEngine(SecRuleRemove):
|
||||||
class WordPressSaaS(SiteDirective):
|
class WordPressSaaS(SiteDirective):
|
||||||
name = 'wordpress-saas'
|
name = 'wordpress-saas'
|
||||||
verbose_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
|
group = SiteDirective.SAAS
|
||||||
regex = r'^/[^ ]*$'
|
regex = r'^/[^ ]*$'
|
||||||
unique_value = True
|
unique_value = True
|
||||||
|
@ -185,10 +185,16 @@ class WordPressSaaS(SiteDirective):
|
||||||
class DokuWikiSaaS(WordPressSaaS):
|
class DokuWikiSaaS(WordPressSaaS):
|
||||||
name = 'dokuwiki-saas'
|
name = 'dokuwiki-saas'
|
||||||
verbose_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):
|
class DrupalSaaS(WordPressSaaS):
|
||||||
name = 'drupal-saas'
|
name = 'drupal-saas'
|
||||||
verbose_name = "Drupdal 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')
|
location = form.cleaned_data.get('path')
|
||||||
if location is not None:
|
if location is not None:
|
||||||
locations.add(normurlpath(location))
|
locations.add(normurlpath(location))
|
||||||
directives = []
|
|
||||||
|
|
||||||
values = defaultdict(list)
|
values = defaultdict(list)
|
||||||
for form in self.forms:
|
for form in self.forms:
|
||||||
website = form.instance
|
wdirective = form.instance
|
||||||
directive = form.cleaned_data
|
directive = form.cleaned_data
|
||||||
if directive.get('name') is not None:
|
if directive.get('name') is not None:
|
||||||
try:
|
try:
|
||||||
website.directive_instance.validate_uniqueness(directive, values, locations)
|
wdirective.directive_instance.validate_uniqueness(directive, values, locations)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
for k,v in err.error_dict.items():
|
for k,v in err.error_dict.items():
|
||||||
form.add_error(k, v)
|
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.WordPressSaaS',
|
||||||
'orchestra.contrib.websites.directives.DokuWikiSaaS',
|
'orchestra.contrib.websites.directives.DokuWikiSaaS',
|
||||||
'orchestra.contrib.websites.directives.DrupalSaaS',
|
'orchestra.contrib.websites.directives.DrupalSaaS',
|
||||||
|
'orchestra.contrib.websites.directives.MoodleSaaS',
|
||||||
),
|
),
|
||||||
# lazy loading
|
# lazy loading
|
||||||
choices=lambda : ((d.get_class_path(), d.get_class_path()) for d in websites.directives.SiteDirective.get_plugins()),
|
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:
|
else:
|
||||||
plugin = self.plugin.get(self.plugin_value)()
|
plugin = self.plugin.get(self.plugin_value)()
|
||||||
self.form = plugin.get_form()
|
self.form = plugin.get_form()
|
||||||
|
self.plugin_instance = plugin
|
||||||
return super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs)
|
return super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
|
@ -65,7 +66,7 @@ class SelectPluginAdminMixin(object):
|
||||||
if not plugin_value and request.method == 'POST':
|
if not plugin_value and request.method == 'POST':
|
||||||
# HACK baceuse django add_preserved_filters removes extising queryargs
|
# HACK baceuse django add_preserved_filters removes extising queryargs
|
||||||
value = re.search(r"%s=([^&^']+)[&']" % self.plugin_field,
|
value = re.search(r"%s=([^&^']+)[&']" % self.plugin_field,
|
||||||
request.META.get('HTTP_REFERER', ''))
|
request.META.get('HTTP_REFERER', ''))
|
||||||
if value:
|
if value:
|
||||||
plugin_value = value.groups()[0]
|
plugin_value = value.groups()[0]
|
||||||
return plugin_value
|
return plugin_value
|
||||||
|
@ -83,8 +84,8 @@ class SelectPluginAdminMixin(object):
|
||||||
'title': _("Add new %s") % plugin.verbose_name,
|
'title': _("Add new %s") % plugin.verbose_name,
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url,
|
return super(SelectPluginAdminMixin, self).add_view(
|
||||||
extra_context=context)
|
request, form_url=form_url, extra_context=context)
|
||||||
return redirect('./select-plugin/?%s' % request.META['QUERY_STRING'])
|
return redirect('./select-plugin/?%s' % request.META['QUERY_STRING'])
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
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,
|
'title': _("Change %s") % plugin.verbose_name,
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(SelectPluginAdminMixin, self).change_view(request, object_id,
|
return super(SelectPluginAdminMixin, self).change_view(
|
||||||
form_url=form_url, extra_context=context)
|
request, object_id, form_url=form_url, extra_context=context)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if not change:
|
if not change:
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.utils.sys import run
|
from orchestra.utils.sys import run
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,3 +25,12 @@ def html_to_pdf(html, pagination=False):
|
||||||
--margin-top 20 - - \
|
--margin-top 20 - - \
|
||||||
""") % context
|
""") % context
|
||||||
return run(cmd, stdin=html.encode('utf-8')).stdout
|
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 New Issue