Added support for moodle SaaS and disable form autocomplition
This commit is contained in:
parent
835a4ab872
commit
be8f830ebb
3
TODO.md
3
TODO.md
|
@ -379,7 +379,7 @@ Case
|
|||
# Don't enforce one contact per account? remove account.email in favour of contacts?
|
||||
|
||||
# Mailer: mark as sent
|
||||
|
||||
# Mailer: download attachments
|
||||
|
||||
# Deprecate orchestra start/stop/restart services management commands?
|
||||
|
||||
|
@ -387,3 +387,4 @@ Case
|
|||
|
||||
|
||||
# Modsecurity rules template by cms (wordpress, joomla, dokuwiki (973337 973338 973347 958057), ...
|
||||
|
||||
|
|
|
@ -90,9 +90,11 @@ def action_to_view(action, modeladmin):
|
|||
|
||||
|
||||
def change_url(obj):
|
||||
if obj is not None:
|
||||
opts = obj._meta
|
||||
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
||||
return reverse(view_name, args=(obj.pk,))
|
||||
raise NoReverseMatch
|
||||
|
||||
|
||||
@admin_field
|
||||
|
|
|
@ -85,7 +85,7 @@ class MySQLUserBackend(ServiceController):
|
|||
context = self.get_context(user)
|
||||
self.append(textwrap.dedent("""\
|
||||
# Create user %(username)s
|
||||
mysql -e 'CREATE USER "%(username)s"@"%(host)s";' || true
|
||||
mysql -e 'CREATE USER "%(username)s"@"%(host)s";' || true # User already exists
|
||||
mysql -e 'UPDATE mysql.user SET Password="%(password)s" WHERE User="%(username)s";'\
|
||||
""") % context
|
||||
)
|
||||
|
|
|
@ -12,7 +12,8 @@ from .models import DatabaseUser, Database
|
|||
|
||||
class DatabaseUserCreationForm(forms.ModelForm):
|
||||
password1 = forms.CharField(label=_("Password"), required=False,
|
||||
widget=forms.PasswordInput, validators=[validate_password])
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||
validators=[validate_password])
|
||||
password2 = forms.CharField(label=_("Password confirmation"), required=False,
|
||||
widget=forms.PasswordInput,
|
||||
help_text=_("Enter the same password as above, for verification."))
|
||||
|
@ -57,6 +58,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
|||
username = self.cleaned_data.get('username')
|
||||
if DatabaseUser.objects.filter(username=username).exists():
|
||||
raise ValidationError("Provided username already exists.")
|
||||
return username
|
||||
|
||||
def clean_password2(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
|
@ -79,7 +81,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
|||
def clean(self):
|
||||
cleaned_data = super(DatabaseCreationForm, self).clean()
|
||||
if 'user' in cleaned_data and 'username' in cleaned_data:
|
||||
msg = _("Use existing user or create a new one?")
|
||||
msg = _("Use existing user or create a new one? you have provided both.")
|
||||
if cleaned_data['user'] and self.cleaned_data['username']:
|
||||
raise ValidationError(msg)
|
||||
elif not (cleaned_data['username'] or cleaned_data['user']):
|
||||
|
|
|
@ -49,7 +49,7 @@ class MetricStorageInline(admin.TabularInline):
|
|||
|
||||
class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'id', 'service_link', 'account_link', 'content_object_link',
|
||||
'display_description', 'service_link', 'account_link', 'content_object_link',
|
||||
'display_registered_on', 'display_billed_until', 'display_cancelled_on',
|
||||
'display_metric'
|
||||
)
|
||||
|
@ -78,6 +78,12 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
display_registered_on = admin_date('registered_on')
|
||||
display_cancelled_on = admin_date('cancelled_on')
|
||||
|
||||
def display_description(self, order):
|
||||
return order.description[:64]
|
||||
display_description.short_description = _("Description")
|
||||
display_description.allow_tags = True
|
||||
display_description.admin_order_field = 'description'
|
||||
|
||||
def content_object_link(self, order):
|
||||
if order.content_object:
|
||||
try:
|
||||
|
|
|
@ -151,9 +151,10 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
lines = []
|
||||
counter = 0
|
||||
# Because of values_list this query doesn't benefit from prefetch_related
|
||||
tx_ids = process.transactions.values_list('id', flat=True)
|
||||
for tx_id in tx_ids:
|
||||
ids.append(str(tx_id))
|
||||
for trans in process.transactions.only('id', 'state'):
|
||||
color = STATE_COLORS.get(trans.state, 'black')
|
||||
state = trans.get_state_display()
|
||||
ids.append('<span style="color:%s" title="%s">%i</span>' % (color, state, trans.id))
|
||||
counter += 1
|
||||
if counter > 10:
|
||||
counter = 0
|
||||
|
|
136
orchestra/contrib/saas/backends/moodle.py
Normal file
136
orchestra/contrib/saas/backends/moodle.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
import os
|
||||
import textwrap
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.contrib.orchestration import ServiceController, replace
|
||||
|
||||
from .. import settings
|
||||
|
||||
|
||||
class MoodleMuBackend(ServiceController):
|
||||
"""
|
||||
Creates a Moodle site on a Moodle multisite installation
|
||||
|
||||
// config.php
|
||||
$site_map = array(
|
||||
// "<HTTP_HOST>" => ["<SITE_NAME>", "<WWWROOT>"],
|
||||
);
|
||||
|
||||
wwwroot = "https://{$site}-courses.pangea.org";
|
||||
$site = getenv("SITE");
|
||||
if ( $site == '' ) {
|
||||
http_host = $_SERVER['HTTP_HOST'];
|
||||
if (array_key_exists($http_host, $site_map)) {
|
||||
$site = $site_map[$http_host][0];
|
||||
$wwwroot = $site_map[$http_host][1];
|
||||
} elseif (strpos($http_host, '-courses.') !== false) {
|
||||
$site = array_shift((explode("-courses.", $http_host)));
|
||||
} else {
|
||||
$site = array_shift((explode(".", $http_host)));
|
||||
}
|
||||
}
|
||||
$CFG->prefix = "${site}_";
|
||||
$CFG->wwwroot = $wwwroot;
|
||||
$CFG->dataroot = "/home/pangea/moodledata/{$site}/";
|
||||
"""
|
||||
verbose_name = _("Moodle multisite")
|
||||
model = 'saas.SaaS'
|
||||
default_route_match = "saas.service == 'moodle'"
|
||||
|
||||
def save(self, webapp):
|
||||
context = self.get_context(webapp)
|
||||
self.append(textwrap.dedent("""\
|
||||
mkdir -p %(moodledata_path)s
|
||||
chown %(user)s:%(user)s %(moodledata_path)s
|
||||
export SITE=%(site_name)s
|
||||
CHANGE_PASSWORD=0
|
||||
php %(moodle_path)s/admin/cli/install_database.php \\
|
||||
--fullname="%(site_name)s" \\
|
||||
--shortname="%(site_name)s" \\
|
||||
--adminpass="%(password)s" \\
|
||||
--adminemail="%(email)s" \\
|
||||
--non-interactive \\
|
||||
--agree-license \\
|
||||
--allow-unstable || CHANGE_PASSWORD=1
|
||||
""") % context
|
||||
)
|
||||
if context['password']:
|
||||
self.append(textwrap.dedent("""\
|
||||
mysql \\
|
||||
--host="%(db_host)s" \\
|
||||
--user="%(db_user)s" \\
|
||||
--password="%(db_pass)s" \\
|
||||
--execute='UPDATE %(site_name)s_user
|
||||
SET password=MD5("%(password)s")
|
||||
WHERE username="admin"' \\
|
||||
%(db_name)s
|
||||
""") % context
|
||||
)
|
||||
if context['crontab']:
|
||||
context['escaped_crontab'] = context['crontab'].replace('$', '\\$')
|
||||
self.append(textwrap.dedent("""\
|
||||
# Configuring Moodle crontabs
|
||||
if ! crontab -u %(user)s -l | grep 'Moodle:"%(site_name)s"' > /dev/null; then
|
||||
cat << EOF | su %(user)s --shell /bin/bash -c 'crontab'
|
||||
$(crontab -u %(user)s -l)
|
||||
|
||||
# %(banner)s - Moodle:"%(site_name)s"
|
||||
%(escaped_crontab)s
|
||||
EOF
|
||||
fi""") % context
|
||||
)
|
||||
|
||||
def delete(self, saas):
|
||||
context = self.get_context(saas)
|
||||
self.append(textwrap.dedent("""
|
||||
rm -rf %(moodledata_path)s
|
||||
# Delete tables with prefix %(site_name)s
|
||||
mysql -Nrs \\
|
||||
--host="%(db_host)s" \\
|
||||
--user="%(db_user)s" \\
|
||||
--password="%(db_pass)s" \\
|
||||
--execute='SET GROUP_CONCAT_MAX_LEN=10000;
|
||||
SET @tbls = (SELECT GROUP_CONCAT(TABLE_NAME)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = "%(db_name)s"
|
||||
AND TABLE_NAME LIKE "%(site_name)s_%%");
|
||||
SET @delStmt = CONCAT("DROP TABLE ", @tbls);
|
||||
-- SELECT @delStmt;
|
||||
PREPARE stmt FROM @delStmt;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;' \\
|
||||
%(db_name)s
|
||||
""") % context
|
||||
)
|
||||
if context['crontab']:
|
||||
context['crontab_regex'] = '\\|'.join(context['crontab'].splitlines())
|
||||
context['crontab_regex'] = context['crontab_regex'].replace('*', '\\*')
|
||||
self.append(textwrap.dedent("""\
|
||||
crontab -u %(user)s -l \\
|
||||
| grep -v 'Moodle:"%(site_name)s"\\|%(crontab_regex)s' \\
|
||||
| su %(user)s --shell /bin/bash -c 'crontab'
|
||||
""") % context
|
||||
)
|
||||
|
||||
def get_context(self, saas):
|
||||
context = {
|
||||
'banner': self.get_banner(),
|
||||
'name': saas.name,
|
||||
'site_name': saas.name,
|
||||
'full_name': "%s course" % saas.name.capitalize(),
|
||||
'moodle_path': settings.SAAS_MOODLE_PATH,
|
||||
'user': settings.SAAS_MOODLE_SYSTEMUSER,
|
||||
'db_user': settings.SAAS_MOODLE_DB_USER,
|
||||
'db_pass': settings.SAAS_MOODLE_DB_PASS,
|
||||
'db_name': settings.SAAS_MOODLE_DB_NAME,
|
||||
'db_host': settings.SAAS_MOODLE_DB_HOST,
|
||||
'email': saas.account.email,
|
||||
'password': getattr(saas, 'password', None),
|
||||
}
|
||||
context.update({
|
||||
'crontab': settings.SAAS_MOODLE_CRONTAB % context,
|
||||
'db_name': context['db_name'] % context,
|
||||
'moodledata_path': settings.SAAS_MOODLE_DATA_PATH % context,
|
||||
})
|
||||
return context
|
|
@ -19,6 +19,9 @@ class PhpListSaaSBackend(ServiceController):
|
|||
The site is created by means of creating a new database per phpList site,
|
||||
but all sites share the same code.
|
||||
|
||||
Different databases are used instead of prefixes because php-list reacts by launching
|
||||
the installation process.
|
||||
|
||||
<tt>// config/config.php
|
||||
$site = getenv("SITE");
|
||||
if ( $site == '' ) {
|
||||
|
|
|
@ -49,7 +49,7 @@ class SaaSPasswordForm(SaaSBaseForm):
|
|||
"service's password, but you can change the password using "
|
||||
"<a href=\"password/\">this form</a>."))
|
||||
password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password],
|
||||
widget=forms.PasswordInput)
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}))
|
||||
password2 = forms.CharField(label=_("Password confirmation"),
|
||||
widget=forms.PasswordInput,
|
||||
help_text=_("Enter the same password as above, for verification."))
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.forms.widgets import SpanWidget
|
||||
|
||||
from .. import settings
|
||||
from ..forms import SaaSPasswordForm
|
||||
from .options import SoftwareService
|
||||
|
||||
|
||||
class MoodleForm(SaaSPasswordForm):
|
||||
email = forms.EmailField(label=_("Email"))
|
||||
admin_username = forms.CharField(label=_("Admin username"), required=False,
|
||||
widget=SpanWidget(display='admin'))
|
||||
|
||||
|
||||
class MoodleService(SoftwareService):
|
||||
name = 'moodle'
|
||||
verbose_name = "Moodle"
|
||||
form = MoodleForm
|
||||
description_field = 'site_name'
|
||||
icon = 'orchestra/icons/apps/Moodle.png'
|
||||
site_domain = settings.SAAS_MOODLE_DOMAIN
|
||||
db_name = settings.SAAS_MOODLE_DB_NAME
|
||||
db_user = settings.SAAS_MOODLE_DB_USER
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError
|
|||
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.utils.functional import cached
|
||||
from orchestra.utils.python import import_class
|
||||
|
@ -64,3 +65,58 @@ class SoftwareService(plugins.Plugin):
|
|||
|
||||
def get_related(self):
|
||||
return []
|
||||
|
||||
|
||||
class DBSoftwareService(SoftwareService):
|
||||
db_name = None
|
||||
db_user = None
|
||||
|
||||
def get_db_name(self):
|
||||
context = {
|
||||
'name': self.instance.name,
|
||||
'site_name': self.instance.name,
|
||||
}
|
||||
db_name = self.db_name % context
|
||||
# Limit for mysql database names
|
||||
return db_name[:65]
|
||||
|
||||
def get_db_user(self):
|
||||
return self.db_user
|
||||
|
||||
@cached
|
||||
def get_account(self):
|
||||
account_model = self.instance._meta.get_field_by_name('account')[0]
|
||||
return account_model.rel.to.objects.get_main()
|
||||
|
||||
def validate(self):
|
||||
super(DBSoftwareService, self).validate()
|
||||
create = not self.instance.pk
|
||||
if create:
|
||||
account = self.get_account()
|
||||
# Validated Database
|
||||
db_user = self.get_db_user()
|
||||
try:
|
||||
DatabaseUser.objects.get(username=db_user)
|
||||
except DatabaseUser.DoesNotExist:
|
||||
raise ValidationError(
|
||||
_("Global database user for PHPList '%(db_user)s' does not exists.") % {
|
||||
'db_user': db_user
|
||||
}
|
||||
)
|
||||
db = Database(name=self.get_db_name(), account=account)
|
||||
try:
|
||||
db.full_clean()
|
||||
except ValidationError as e:
|
||||
raise ValidationError({
|
||||
'name': e.messages,
|
||||
})
|
||||
|
||||
def save(self):
|
||||
account = self.get_account()
|
||||
# Database
|
||||
db_name = self.get_db_name()
|
||||
db_user = self.get_db_user()
|
||||
db, db_created = account.databases.get_or_create(name=db_name, type=Database.MYSQL)
|
||||
user = DatabaseUser.objects.get(username=db_user)
|
||||
db.users.add(user)
|
||||
self.instance.database_id = db.pk
|
||||
|
|
|
@ -5,13 +5,12 @@ from django.db.models import Q
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.contrib.databases.models import Database, DatabaseUser
|
||||
from orchestra.contrib.mailboxes.models import Mailbox
|
||||
from orchestra.forms.widgets import SpanWidget
|
||||
|
||||
from .. import settings
|
||||
from ..forms import SaaSPasswordForm
|
||||
from .options import SoftwareService
|
||||
from .options import DBSoftwareService
|
||||
|
||||
|
||||
class PHPListForm(SaaSPasswordForm):
|
||||
|
@ -64,26 +63,15 @@ class PHPListChangeForm(PHPListForm):
|
|||
original=mailbox.name, display=mailbox_link)
|
||||
|
||||
|
||||
class PHPListService(SoftwareService):
|
||||
class PHPListService(DBSoftwareService):
|
||||
name = 'phplist'
|
||||
verbose_name = "phpList"
|
||||
form = PHPListForm
|
||||
change_form = PHPListChangeForm
|
||||
icon = 'orchestra/icons/apps/Phplist.png'
|
||||
site_domain = settings.SAAS_PHPLIST_DOMAIN
|
||||
|
||||
def get_db_name(self):
|
||||
context = {
|
||||
'name': self.instance.name,
|
||||
'site_name': self.instance.name,
|
||||
}
|
||||
return settings.SAAS_PHPLIST_DB_NAME % context
|
||||
db_name = 'phplist_mu_%s' % self.instance.name
|
||||
# Limit for mysql database names
|
||||
return db_name[:65]
|
||||
|
||||
def get_db_user(self):
|
||||
return settings.SAAS_PHPLIST_DB_USER
|
||||
db_name = settings.SAAS_PHPLIST_DB_NAME
|
||||
db_user = settings.SAAS_PHPLIST_DB_USER
|
||||
|
||||
def get_mailbox_name(self):
|
||||
context = {
|
||||
|
@ -92,32 +80,11 @@ class PHPListService(SoftwareService):
|
|||
}
|
||||
return settings.SAAS_PHPLIST_BOUNCES_MAILBOX_NAME % context
|
||||
|
||||
def get_account(self):
|
||||
account_model = self.instance._meta.get_field_by_name('account')[0]
|
||||
return account_model.rel.to.objects.get_main()
|
||||
|
||||
def validate(self):
|
||||
super(PHPListService, self).validate()
|
||||
create = not self.instance.pk
|
||||
if create:
|
||||
account = self.get_account()
|
||||
# Validated Database
|
||||
db_user = self.get_db_user()
|
||||
try:
|
||||
DatabaseUser.objects.get(username=db_user)
|
||||
except DatabaseUser.DoesNotExist:
|
||||
raise ValidationError(
|
||||
_("Global database user for PHPList '%(db_user)s' does not exists.") % {
|
||||
'db_user': db_user
|
||||
}
|
||||
)
|
||||
db = Database(name=self.get_db_name(), account=account)
|
||||
try:
|
||||
db.full_clean()
|
||||
except ValidationError as e:
|
||||
raise ValidationError({
|
||||
'name': e.messages,
|
||||
})
|
||||
# Validate mailbox
|
||||
mailbox = Mailbox(name=self.get_mailbox_name(), account=account)
|
||||
try:
|
||||
|
@ -129,13 +96,6 @@ class PHPListService(SoftwareService):
|
|||
|
||||
def save(self):
|
||||
account = self.get_account()
|
||||
# Database
|
||||
db_name = self.get_db_name()
|
||||
db_user = self.get_db_user()
|
||||
db, db_created = account.databases.get_or_create(name=db_name, type=Database.MYSQL)
|
||||
user = DatabaseUser.objects.get(username=db_user)
|
||||
db.users.add(user)
|
||||
self.instance.database_id = db.pk
|
||||
# Mailbox
|
||||
mailbox_name = self.get_mailbox_name()
|
||||
mailbox, mb_created = account.mailboxes.get_or_create(name=mailbox_name)
|
||||
|
|
|
@ -197,3 +197,54 @@ SAAS_GITLAB_DOMAIN = Setting('SAAS_GITLAB_DOMAIN',
|
|||
'gitlab.{}'.format(ORCHESTRA_BASE_DOMAIN),
|
||||
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
|
||||
)
|
||||
|
||||
|
||||
# Moodle
|
||||
|
||||
SAAS_MOODLE_DB_USER = Setting('SAAS_MOODLE_DB_USER',
|
||||
'moodle_mu',
|
||||
help_text=_("Needed for password changing support."),
|
||||
)
|
||||
|
||||
SAAS_MOODLE_DB_PASS = Setting('SAAS_MOODLE_DB_PASS',
|
||||
'secret',
|
||||
help_text=_("Needed for password changing support."),
|
||||
)
|
||||
|
||||
SAAS_MOODLE_DB_NAME = Setting('SAAS_MOODLE_DB_NAME',
|
||||
'moodle_mu',
|
||||
help_text=_("Needed for password changing support."),
|
||||
)
|
||||
|
||||
SAAS_MOODLE_DB_HOST = Setting('SAAS_MOODLE_DB_HOST',
|
||||
'loclahost',
|
||||
help_text=_("Needed for password changing support."),
|
||||
)
|
||||
|
||||
SAAS_MOODLE_DOMAIN = Setting('SAAS_MOODLE_DOMAIN',
|
||||
'%(site_name)s.courses.{}'.format(ORCHESTRA_BASE_DOMAIN),
|
||||
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
|
||||
)
|
||||
|
||||
SAAS_MOODLE_PATH = Setting('SAAS_MOODLE_PATH',
|
||||
'/var/www/moodle-mu',
|
||||
help_text=_("Filesystem path to the Moodle source code installed on the server. "
|
||||
"Used by <tt>SAAS_MOODLE_CRONTAB</tt>.")
|
||||
)
|
||||
|
||||
SAAS_MOODLE_DATA_PATH = Setting('SAAS_MOODLE_DATA_PATH',
|
||||
'/var/moodledata/%(site_name)s',
|
||||
help_text=_("Filesystem path to the Moodle source code installed on the server. "
|
||||
"Used by <tt>SAAS_MOODLE_CRONTAB</tt>.")
|
||||
)
|
||||
|
||||
SAAS_MOODLE_SYSTEMUSER = Setting('SAAS_MOODLE_SYSTEMUSER',
|
||||
'root',
|
||||
help_text=_("System user running Moodle on the server."
|
||||
"Used by <tt>SAAS_MOODLE_CRONTAB</tt>.")
|
||||
)
|
||||
|
||||
SAAS_MOODLE_CRONTAB = Setting('SAAS_MOODLE_CRONTAB',
|
||||
'*/15 * * * * export SITE="%(site_name)s"; php %(moodle_path)s/admin/cli/cron.php >/dev/null',
|
||||
help_text=_("Left blank if you don't want crontab to be configured")
|
||||
)
|
||||
|
|
|
@ -20,7 +20,8 @@ class UserCreationForm(forms.ModelForm):
|
|||
'duplicate_username': _("A user with that username already exists."),
|
||||
}
|
||||
password1 = forms.CharField(label=_("Password"),
|
||||
widget=forms.PasswordInput, validators=[validate_password])
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||
validators=[validate_password])
|
||||
password2 = forms.CharField(label=_("Password confirmation"),
|
||||
widget=forms.PasswordInput,
|
||||
help_text=_("Enter the same password as above, for verification."))
|
||||
|
|
Loading…
Reference in a new issue