Added support for moodle SaaS and disable form autocomplition

This commit is contained in:
Marc Aymerich 2015-09-30 13:22:17 +00:00
parent 835a4ab872
commit be8f830ebb
14 changed files with 285 additions and 58 deletions

View File

@ -379,7 +379,7 @@ Case
# Don't enforce one contact per account? remove account.email in favour of contacts? # Don't enforce one contact per account? remove account.email in favour of contacts?
# Mailer: mark as sent # Mailer: mark as sent
# Mailer: download attachments
# Deprecate orchestra start/stop/restart services management commands? # 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), ... # Modsecurity rules template by cms (wordpress, joomla, dokuwiki (973337 973338 973347 958057), ...

View File

@ -90,9 +90,11 @@ def action_to_view(action, modeladmin):
def change_url(obj): def change_url(obj):
if obj is not None:
opts = obj._meta opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name) view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
return reverse(view_name, args=(obj.pk,)) return reverse(view_name, args=(obj.pk,))
raise NoReverseMatch
@admin_field @admin_field

View File

@ -85,7 +85,7 @@ class MySQLUserBackend(ServiceController):
context = self.get_context(user) context = self.get_context(user)
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
# Create user %(username)s # 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";'\ mysql -e 'UPDATE mysql.user SET Password="%(password)s" WHERE User="%(username)s";'\
""") % context """) % context
) )

View File

@ -12,7 +12,8 @@ from .models import DatabaseUser, Database
class DatabaseUserCreationForm(forms.ModelForm): class DatabaseUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_("Password"), required=False, 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, password2 = forms.CharField(label=_("Password confirmation"), required=False,
widget=forms.PasswordInput, widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification.")) help_text=_("Enter the same password as above, for verification."))
@ -57,6 +58,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
if DatabaseUser.objects.filter(username=username).exists(): if DatabaseUser.objects.filter(username=username).exists():
raise ValidationError("Provided username already exists.") raise ValidationError("Provided username already exists.")
return username
def clean_password2(self): def clean_password2(self):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
@ -79,7 +81,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
def clean(self): def clean(self):
cleaned_data = super(DatabaseCreationForm, self).clean() cleaned_data = super(DatabaseCreationForm, self).clean()
if 'user' in cleaned_data and 'username' in cleaned_data: 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']: if cleaned_data['user'] and self.cleaned_data['username']:
raise ValidationError(msg) raise ValidationError(msg)
elif not (cleaned_data['username'] or cleaned_data['user']): elif not (cleaned_data['username'] or cleaned_data['user']):

View File

@ -49,7 +49,7 @@ class MetricStorageInline(admin.TabularInline):
class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( 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_registered_on', 'display_billed_until', 'display_cancelled_on',
'display_metric' 'display_metric'
) )
@ -78,6 +78,12 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_registered_on = admin_date('registered_on') display_registered_on = admin_date('registered_on')
display_cancelled_on = admin_date('cancelled_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): def content_object_link(self, order):
if order.content_object: if order.content_object:
try: try:

View File

@ -151,9 +151,10 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
lines = [] lines = []
counter = 0 counter = 0
# Because of values_list this query doesn't benefit from prefetch_related # Because of values_list this query doesn't benefit from prefetch_related
tx_ids = process.transactions.values_list('id', flat=True) for trans in process.transactions.only('id', 'state'):
for tx_id in tx_ids: color = STATE_COLORS.get(trans.state, 'black')
ids.append(str(tx_id)) state = trans.get_state_display()
ids.append('<span style="color:%s" title="%s">%i</span>' % (color, state, trans.id))
counter += 1 counter += 1
if counter > 10: if counter > 10:
counter = 0 counter = 0

View 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

View File

@ -19,6 +19,9 @@ class PhpListSaaSBackend(ServiceController):
The site is created by means of creating a new database per phpList site, The site is created by means of creating a new database per phpList site,
but all sites share the same code. 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 <tt>// config/config.php
$site = getenv("SITE"); $site = getenv("SITE");
if ( $site == '' ) { if ( $site == '' ) {

View File

@ -49,7 +49,7 @@ class SaaSPasswordForm(SaaSBaseForm):
"service's password, but you can change the password using " "service's password, but you can change the password using "
"<a href=\"password/\">this form</a>.")) "<a href=\"password/\">this form</a>."))
password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password], password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password],
widget=forms.PasswordInput) widget=forms.PasswordInput(attrs={'autocomplete': 'off'}))
password2 = forms.CharField(label=_("Password confirmation"), password2 = forms.CharField(label=_("Password confirmation"),
widget=forms.PasswordInput, widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification.")) help_text=_("Enter the same password as above, for verification."))

View File

@ -1,16 +1,24 @@
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.forms.widgets import SpanWidget
from .. import settings
from ..forms import SaaSPasswordForm from ..forms import SaaSPasswordForm
from .options import SoftwareService from .options import SoftwareService
class MoodleForm(SaaSPasswordForm): class MoodleForm(SaaSPasswordForm):
email = forms.EmailField(label=_("Email")) admin_username = forms.CharField(label=_("Admin username"), required=False,
widget=SpanWidget(display='admin'))
class MoodleService(SoftwareService): class MoodleService(SoftwareService):
name = 'moodle'
verbose_name = "Moodle" verbose_name = "Moodle"
form = MoodleForm form = MoodleForm
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
db_name = settings.SAAS_MOODLE_DB_NAME
db_user = settings.SAAS_MOODLE_DB_USER

View File

@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError
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.orchestration import Operation from orchestra.contrib.orchestration import Operation
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
@ -64,3 +65,58 @@ class SoftwareService(plugins.Plugin):
def get_related(self): def get_related(self):
return [] 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

View File

@ -5,13 +5,12 @@ from django.db.models import Q
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.databases.models import Database, DatabaseUser
from orchestra.contrib.mailboxes.models import Mailbox from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.forms.widgets import SpanWidget from orchestra.forms.widgets import SpanWidget
from .. import settings from .. import settings
from ..forms import SaaSPasswordForm from ..forms import SaaSPasswordForm
from .options import SoftwareService from .options import DBSoftwareService
class PHPListForm(SaaSPasswordForm): class PHPListForm(SaaSPasswordForm):
@ -64,26 +63,15 @@ class PHPListChangeForm(PHPListForm):
original=mailbox.name, display=mailbox_link) original=mailbox.name, display=mailbox_link)
class PHPListService(SoftwareService): class PHPListService(DBSoftwareService):
name = 'phplist' name = 'phplist'
verbose_name = "phpList" verbose_name = "phpList"
form = PHPListForm form = PHPListForm
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
db_name = settings.SAAS_PHPLIST_DB_NAME
def get_db_name(self): db_user = settings.SAAS_PHPLIST_DB_USER
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
def get_mailbox_name(self): def get_mailbox_name(self):
context = { context = {
@ -92,32 +80,11 @@ class PHPListService(SoftwareService):
} }
return settings.SAAS_PHPLIST_BOUNCES_MAILBOX_NAME % context 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): def validate(self):
super(PHPListService, self).validate() super(PHPListService, self).validate()
create = not self.instance.pk create = not self.instance.pk
if create: if create:
account = self.get_account() 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 # Validate mailbox
mailbox = Mailbox(name=self.get_mailbox_name(), account=account) mailbox = Mailbox(name=self.get_mailbox_name(), account=account)
try: try:
@ -129,13 +96,6 @@ class PHPListService(SoftwareService):
def save(self): def save(self):
account = self.get_account() 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
mailbox_name = self.get_mailbox_name() mailbox_name = self.get_mailbox_name()
mailbox, mb_created = account.mailboxes.get_or_create(name=mailbox_name) mailbox, mb_created = account.mailboxes.get_or_create(name=mailbox_name)

View File

@ -197,3 +197,54 @@ SAAS_GITLAB_DOMAIN = Setting('SAAS_GITLAB_DOMAIN',
'gitlab.{}'.format(ORCHESTRA_BASE_DOMAIN), 'gitlab.{}'.format(ORCHESTRA_BASE_DOMAIN),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.", 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")
)

View File

@ -20,7 +20,8 @@ class UserCreationForm(forms.ModelForm):
'duplicate_username': _("A user with that username already exists."), 'duplicate_username': _("A user with that username already exists."),
} }
password1 = forms.CharField(label=_("Password"), 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"), password2 = forms.CharField(label=_("Password confirmation"),
widget=forms.PasswordInput, widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification.")) help_text=_("Enter the same password as above, for verification."))