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?
# 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), ...

View File

@ -90,9 +90,11 @@ def action_to_view(action, modeladmin):
def change_url(obj):
opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
return reverse(view_name, args=(obj.pk,))
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

View File

@ -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
)

View File

@ -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']):

View File

@ -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:

View File

@ -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

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,
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 == '' ) {

View File

@ -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."))

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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")
)

View File

@ -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."))