├── mdk
├── __init__.py
├── scripts
│ ├── less.sh
│ ├── makecourse.sh
│ ├── setup.sh
│ ├── README.rst
│ ├── tokens.php
│ ├── version.php
│ ├── external_functions.php
│ ├── setupsecurity.sh
│ ├── dev.php
│ ├── mindev.php
│ ├── enrol.php
│ ├── webservices.php
│ ├── undev.php
│ └── users.php
├── version.py
├── exceptions.py
├── commands
│ ├── __init__.py
│ ├── uninstall.py
│ ├── remove.py
│ ├── purge.py
│ ├── fix.py
│ ├── install.py
│ ├── run.py
│ ├── upgrade.py
│ ├── update.py
│ ├── info.py
│ ├── precheck.py
│ ├── backup.py
│ ├── alias.py
│ ├── phpunit.py
│ ├── css.py
│ ├── pull.py
│ ├── tracker.py
│ ├── push.py
│ ├── rebase.py
│ ├── init.py
│ ├── create.py
│ └── js.py
├── __main__.py
├── js.py
├── ci.py
├── command.py
├── phpunit.py
├── scripts.py
├── css.py
├── tools.py
├── backup.py
└── fetch.py
├── .gitignore
├── MANIFEST.in
├── requirements.txt
├── extra
├── apache.conf
├── welcome.php
├── goto_instance.bash_completion
└── goto_instance
├── mdk.py
└── setup.py
/mdk/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | config.json
3 | *.pyc
4 | .idea
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | include *.rst
3 | include mdk/config-dist.json
4 | include mdk/scripts/*
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | keyring>=3.5
2 | jenkinsapi>=0.3.2
3 | MySQL-python>=1.2.3
4 | psycopg2>=2.4.5
5 | requests>=2.3.0
6 | watchdog>=0.8.0
7 | pyodbc>=4.0.21
8 |
--------------------------------------------------------------------------------
/mdk/scripts/less.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Compile the Less files of Bootstrap base.
4 | #
5 | # Deprecated script, please refer to the CSS command.
6 |
7 | echo "This script is deprecated, please use:"
8 | echo " mdk css --compile"
9 | echo ""
10 |
11 | mdk css --compile
12 |
--------------------------------------------------------------------------------
/mdk/scripts/makecourse.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Creates a course.
4 |
5 | PHP=`mdk config show php`
6 | I="$RANDOM"
7 | SHORTNAME="MDK101-$I"
8 | FULLNAME="Moodle Development $I"
9 | SIZE="S"
10 | CLI="admin/tool/generator/cli/maketestcourse.php"
11 |
12 | if [ ! -e "$CLI" ]; then
13 | echo "Cannot create a course: the CLI script to create test courses could not be found."
14 | exit 1
15 | fi
16 |
17 | $PHP $CLI --shortname="$SHORTNAME" --fullname="$FULLNAME" --size=$SIZE --bypasscheck
18 |
--------------------------------------------------------------------------------
/mdk/scripts/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Minimal set-up for development.
4 | #
5 | # This turns on the minimal development options. Then creates some users,
6 | # make a course and enrol the users in it.
7 |
8 | echo "Setting up mindev mode..."
9 | mdk run mindev > /dev/null 2>&1
10 |
11 | echo "Creating a bunch of users..."
12 | mdk run users > /dev/null 2>&1
13 |
14 | echo "Creating a course..."
15 | COURSENAME=`mdk run makecourse 2> /dev/null | grep 'http'`
16 | if [ -n "$COURSENAME" ]; then
17 | echo " $COURSENAME"
18 | fi
19 |
20 | echo "Enrolling users in the course..."
21 | mdk run enrol > /dev/null 2>&1
22 |
--------------------------------------------------------------------------------
/extra/apache.conf:
--------------------------------------------------------------------------------
1 | # Uncomment the following line if you want any server name /m to point to moodle-sdk.
2 | # (Do not forget to update your config.json file accordingly.)
3 | # Alias /m /var/lib/moodle-sdk/www
4 |
5 | ServerName moodle-sdk
6 |
7 | DocumentRoot /var/lib/moodle-sdk/www
8 |
9 | Options Indexes FollowSymLinks MultiViews
10 | AllowOverride All
11 | Order allow,deny
12 | Allow from all
13 |
14 |
15 | ErrorLog ${APACHE_LOG_DIR}/error-moodle-sdk.log
16 | LogLevel notice
17 |
18 | CustomLog ${APACHE_LOG_DIR}/access-moodle-sdk.log combined
19 |
--------------------------------------------------------------------------------
/mdk/version.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2012 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | __version__ = "1.7.4"
26 |
--------------------------------------------------------------------------------
/mdk/scripts/README.rst:
--------------------------------------------------------------------------------
1 | Custom scripts
2 | ==============
3 |
4 | This directory is meant to host scripts to be run on an instance. They are called using the command ``run``.
5 |
6 | The format of the script is recognised using its extension.
7 |
8 | Formats
9 | -------
10 |
11 | ### PHP
12 |
13 | PHP scripts will be executed from the web directory of an instance. They will be executed as any other CLI script.
14 |
15 | ### Shell
16 |
17 | Shell scripts will be executed from the web directory of an instance. They will be made executable and run on their own. If you need to access information about the instance from within the shell script, retrieve it using `mdk info`.
18 |
19 | Directories
20 | -----------
21 |
22 | The scripts are looked for in each of the following directories until found:
23 | - (Setting dirs.moodle)/scripts
24 | - ~/.moodle-sdk/scripts
25 | - /etc/moodle-sdk/scripts
26 | - /path/to/moodle-sdk/scripts
--------------------------------------------------------------------------------
/mdk/scripts/tokens.php:
--------------------------------------------------------------------------------
1 | get_recordset_sql("
10 | SELECT t.*, u.username, s.shortname AS sshortname, s.name AS sname
11 | FROM {external_tokens} t
12 | JOIN {user} u
13 | ON t.userid = u.id
14 | JOIN {external_services} s
15 | ON t.externalserviceid = s.id
16 | ORDER BY sname ASC, username ASC
17 | ");
18 |
19 | mtrace(sprintf("%s %13s %20s", 'User ID', 'Username', 'Token'));
20 | mtrace('');
21 |
22 | $lastexternalserviceid = null;
23 | $format = "[%' 5d] %' 16s: %s";
24 | foreach ($tokens as $token) {
25 | if ($lastexternalserviceid != $token->externalserviceid) {
26 | $title = sprintf("%s [%s]", $token->sname, $token->sshortname);
27 | $lastexternalserviceid && mtrace('');
28 | mtrace($title);
29 | mtrace(str_repeat('-', strlen($title)));
30 | $lastexternalserviceid = $token->externalserviceid;
31 | }
32 |
33 | mtrace(sprintf($format, $token->userid, $token->username, $token->token));
34 | }
35 | $tokens->close();
36 |
--------------------------------------------------------------------------------
/extra/welcome.php:
--------------------------------------------------------------------------------
1 | .
19 | *
20 | * http://github.com/FMCorz/mdk
21 | */
22 |
23 | echo "
Moodle Development Kit";
24 | echo "";
25 |
26 | $path = getcwd();
27 | $dirs = scandir($path);
28 | foreach ($dirs as $dir) {
29 | if ($dir == '.' || $dir == '..' || !is_dir($path . '/' . $dir)) {
30 | continue;
31 | }
32 | print "- $dir
";
33 | }
34 |
35 | echo "
";
36 |
--------------------------------------------------------------------------------
/mdk/scripts/version.php:
--------------------------------------------------------------------------------
1 | libdir.'/clilib.php');
6 | require("$CFG->dirroot/version.php");
7 |
8 | cli_separator();
9 | cli_heading('Resetting all version numbers');
10 |
11 | $manager = core_plugin_manager::instance();
12 |
13 | // Purge caches to make sure we have the fresh information about versions.
14 | $manager::reset_caches();
15 | $configcache = cache::make('core', 'config');
16 | $configcache->purge();
17 |
18 | $plugininfo = $manager->get_plugins();
19 | foreach ($plugininfo as $type => $plugins) {
20 | foreach ($plugins as $name => $plugin) {
21 | if ($plugin->get_status() !== core_plugin_manager::PLUGIN_STATUS_DOWNGRADE) {
22 | continue;
23 | }
24 |
25 | $frankenstyle = sprintf("%s_%s", $type, $name);
26 |
27 | mtrace("Updating {$frankenstyle} from {$plugin->versiondb} to {$plugin->versiondisk}");
28 | $DB->set_field('config_plugins', 'value', $plugin->versiondisk, array('name' => 'version', 'plugin' => $frankenstyle));
29 | }
30 | }
31 |
32 | // Check that the main version hasn't changed.
33 | if ((float) $CFG->version !== $version) {
34 | set_config('version', $version);
35 | mtrace("Updated main version from {$CFG->version} to {$version}");
36 | }
37 |
38 | // Purge relevant caches again.
39 | $manager::reset_caches();
40 | $configcache->purge();
41 |
--------------------------------------------------------------------------------
/mdk.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | """
26 | This file executes MDK as a package.
27 |
28 | It is intended to be used by those who are not using MDK as a package,
29 | they can make this file executable and execute it directly.
30 |
31 | Is also provides backwards compatibility to those who had set up MDK manually
32 | by cloning the repository and linked to mdk.py as an executable.
33 |
34 | Please note that using this method is not advised, using `python -m mdk` or the
35 | executable installed with the package is recommended.
36 | """
37 |
38 | import runpy
39 | a = runpy.run_module('mdk', None, '__main__')
40 |
--------------------------------------------------------------------------------
/extra/goto_instance.bash_completion:
--------------------------------------------------------------------------------
1 | #
2 | # Moodle Development Kit
3 | #
4 | # Copyright (c) 2013 Frédéric Massart - FMCorz.net
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 | #
19 | # http://github.com/FMCorz/mdk
20 | #
21 |
22 | # This file defines the functions the Bash completion of extra/goto_instance.
23 | #
24 | # This file has to be loaded after the goto_instance one, so it cannot be placed
25 | # into /etc/bash_completion.d.
26 |
27 | function _gt() {
28 | local BIN CUR OPTS
29 | BIN="mdk"
30 | CUR="${COMP_WORDS[COMP_CWORD]}"
31 | OPTS=""
32 | if [[ "${COMP_CWORD}" == 1 ]]; then
33 | OPTS=$($BIN info -ln 2> /dev/null)
34 | fi
35 | COMPREPLY=( $(compgen -W "${OPTS}" -- ${CUR}) )
36 | return 0
37 | }
38 |
39 | if [[ -n "$(type -t gt)" ]]; then
40 | complete -F _gt gt
41 | complete -F _gt gtd
42 | fi
43 |
--------------------------------------------------------------------------------
/mdk/exceptions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2012 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 |
26 | class BackupDirectoryExistsException(Exception):
27 | pass
28 |
29 |
30 | class BackupDBExistsException(Exception):
31 | pass
32 |
33 |
34 | class BackupDBEngineNotSupported(Exception):
35 | pass
36 |
37 |
38 | class ConflictInScriptName(Exception):
39 | pass
40 |
41 |
42 | class CreateException(Exception):
43 | pass
44 |
45 |
46 | class DisplayCommandHelp(Exception):
47 | pass
48 |
49 |
50 | class InstallException(Exception):
51 | pass
52 |
53 |
54 | class ScriptNotFound(Exception):
55 | pass
56 |
57 |
58 | class UnsupportedScript(Exception):
59 | pass
60 |
61 |
62 | class UpgradeNotAllowed(Exception):
63 | pass
64 |
--------------------------------------------------------------------------------
/mdk/commands/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 |
26 | def getCommand(cmd):
27 | """Lazy loading of a command class. Millseconds saved, hurray!"""
28 | cls = cmd.capitalize() + 'Command'
29 | return getattr(getattr(getattr(__import__('mdk.%s.%s' % ('commands', cmd)), 'commands'), cmd), cls)
30 |
31 | commandsList = [
32 | 'alias',
33 | 'backport',
34 | 'backup',
35 | 'behat',
36 | 'config',
37 | 'create',
38 | 'css',
39 | 'doctor',
40 | 'fix',
41 | 'info',
42 | 'init',
43 | 'install',
44 | 'js',
45 | 'phpunit',
46 | 'plugin',
47 | 'precheck',
48 | 'pull',
49 | 'purge',
50 | 'push',
51 | 'rebase',
52 | 'remove',
53 | 'run',
54 | 'tracker',
55 | 'uninstall',
56 | 'update',
57 | 'upgrade'
58 | ]
59 |
--------------------------------------------------------------------------------
/mdk/scripts/external_functions.php:
--------------------------------------------------------------------------------
1 | .
16 |
17 | /**
18 | * Script to refresh the services and external functions.
19 | *
20 | * @package mdk
21 | * @copyright 2015 Frédéric Massart - FMCorz.net
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 | */
24 |
25 | define('CLI_SCRIPT', true);
26 | require('config.php');
27 | require($CFG->libdir . '/upgradelib.php');
28 |
29 | mtrace('Updating services of core');
30 | external_update_descriptions('moodle');
31 |
32 | $plugintypes = core_component::get_plugin_types();
33 | foreach ($plugintypes as $plugintype => $dir) {
34 | $plugins = core_component::get_plugin_list($plugintype);
35 | foreach ($plugins as $plugin => $dir) {
36 | $component = $plugintype . '_' . $plugin;
37 | mtrace('Updating services of ' . $component);
38 | external_update_descriptions($component);
39 | }
40 | }
41 |
42 | if (function_exists('external_update_services')) {
43 | external_update_services();
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/extra/goto_instance:
--------------------------------------------------------------------------------
1 | #
2 | # Moodle Development Kit
3 | #
4 | # Copyright (c) 2013 Frédéric Massart - FMCorz.net
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 | #
19 | # http://github.com/FMCorz/mdk
20 | #
21 |
22 | # This file defines a function to quickly go to into an MDK instance.
23 | #
24 | # To install on Ubuntu, source this file into your ~/.bashrc file:
25 | #
26 | # if [ -f /usr/share/moodle-sdk/extra/goto_instance ]; then
27 | # . /usr/share/moodle-sdk/extra/goto_instance
28 | # . /usr/share/moodle-sdk/extra/goto_instance.bash_completion
29 | # fi
30 | #
31 | # Then source ~/.bashrc:
32 | #
33 | # source ~/.bashrc
34 | #
35 |
36 | # Go to instance directory.
37 | function gt() {
38 | DIR=`mdk config show dirs.www`
39 | eval DIR="$DIR/$1"
40 | if [[ ! -d $DIR ]]; then
41 | echo "Could not resolve path"
42 | return
43 | fi
44 | cd "$DIR"
45 | }
46 |
47 | # Go to instance data directory.
48 | function gtd() {
49 | DIR=`mdk config show dirs.storage`
50 | DATADIR=`mdk config show dataDir`
51 | eval DIR="$DIR/$1/$DATADIR"
52 | if [[ ! -d $DIR ]]; then
53 | echo "Could not resolve path"
54 | return
55 | fi
56 | cd $DIR
57 | }
--------------------------------------------------------------------------------
/mdk/scripts/setupsecurity.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Script to setup a clone for testing against the security repository
4 | #
5 | # It removes existing origin remote and adds the security repository in
6 | # read-only mode. It also adds a git hook to prevent pushes. The purpose is to
7 | # prevent as much as possible that security issues are released to public
8 | # repositories.
9 |
10 | set -e
11 |
12 | GIT=`mdk config show git`
13 | REPOURL=`mdk config show remotes.security`
14 | ORIGINREMOTE=`mdk config show upstreamRemote`
15 | DIRROOT=`mdk info -v path`
16 |
17 | # Remove origin remote.
18 | echo "Deleting current origin remote..."
19 | $GIT remote rm $ORIGINREMOTE
20 |
21 | echo "Adding security repository remote as origin..."
22 | ${GIT} remote add $ORIGINREMOTE $REPOURL
23 |
24 | # Pushes to an unexisting URL.
25 | ${GIT} remote set-url --push $ORIGINREMOTE no-pushes-allowed
26 |
27 |
28 | # Git hook to prevent all pushes in case people is adding other remotes anyway.
29 | content="#!/bin/sh
30 |
31 | >&2 echo \"Sorry, pushes are not allowed. This clone is not supposed to be used
32 | to push stuff as you may accidentally push security patches to public repos.\"
33 | exit 1"
34 |
35 | hookfile="$DIRROOT/.git/hooks/pre-push"
36 |
37 | if [ -f "$hookfile" ]; then
38 | existingcontent=$(cat $hookfile)
39 |
40 | if [[ "$content" != "$existingcontent" ]]; then
41 | # Error if there is a hook we don't know about.
42 | echo "Error: Security repository setup adds a pre-push hook to "\
43 | "prevent accidental pushes to public repositories. You already have a "\
44 | "pre-push hook, please delete it or back it up and merge it once "\
45 | "security repository setup concluded."
46 | exit 1
47 | fi
48 | else
49 | # Create the file.
50 | echo "Adding a git hook to prevent pushes from this clone..."
51 | cat > $hookfile << EOL
52 | $content
53 | EOL
54 | chmod +x $hookfile
55 | fi
56 |
57 | exit 0
58 |
--------------------------------------------------------------------------------
/mdk/commands/uninstall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 |
28 |
29 | class UninstallCommand(Command):
30 |
31 | _description = 'Uninstall a Moodle instance'
32 | _arguments = [
33 | (
34 | ['name'],
35 | {
36 | 'default': None,
37 | 'help': 'name of the instance',
38 | 'metavar': 'name',
39 | 'nargs': '?'
40 | }
41 | ),
42 | (
43 | ['-y'],
44 | {
45 | 'action': 'store_true',
46 | 'dest': 'do',
47 | 'help': 'do not ask for confirmation'
48 | }
49 | )
50 | ]
51 |
52 | def run(self, args):
53 |
54 | M = self.Wp.resolve(args.name)
55 | if not M:
56 | raise Exception('This is not a Moodle instance')
57 | elif not M.isInstalled():
58 | logging.info('This instance is not installed')
59 | return
60 |
61 | if not args.do:
62 | confirm = raw_input('Are you sure? (Y/n) ')
63 | if confirm != 'Y':
64 | logging.info('Aborting...')
65 | return
66 |
67 | logging.info('Uninstalling %s...' % M.get('identifier'))
68 | M.uninstall()
69 | logging.info('Done.')
70 |
--------------------------------------------------------------------------------
/mdk/scripts/dev.php:
--------------------------------------------------------------------------------
1 | .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 |
28 |
29 | class RemoveCommand(Command):
30 |
31 | _arguments = [
32 | (
33 | ['name'],
34 | {
35 | 'help': 'name of the instance'
36 | }
37 | ),
38 | (
39 | ['-y'],
40 | {
41 | 'action': 'store_true',
42 | 'dest': 'do',
43 | 'help': 'do not ask for confirmation'
44 | }
45 | ),
46 | (
47 | ['-f'],
48 | {
49 | 'action': 'store_true',
50 | 'dest': 'force',
51 | 'help': 'force and do not ask for confirmation'
52 | }
53 | )
54 | ]
55 | _description = 'Completely remove an instance'
56 |
57 | def run(self, args):
58 |
59 | try:
60 | M = self.Wp.get(args.name)
61 | except:
62 | raise Exception('This is not a Moodle instance')
63 |
64 | if not args.do and not args.force:
65 | confirm = raw_input('Are you sure? (Y/n) ')
66 | if confirm != 'Y':
67 | logging.info('Aborting...')
68 | return
69 |
70 | logging.info('Removing %s...' % args.name)
71 | try:
72 | self.Wp.delete(args.name)
73 | except OSError:
74 | raise Exception('Error while deleting the instance.\n' +
75 | 'This is probably a permission issue.\n' +
76 | 'Run: sudo chmod -R 0777 %s' % self.Wp.getPath(args.name))
77 |
78 | logging.info('Instance removed')
79 |
--------------------------------------------------------------------------------
/mdk/scripts/mindev.php:
--------------------------------------------------------------------------------
1 | .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | from setuptools import setup, find_packages
27 |
28 | # Load version number.
29 | execfile('mdk/version.py')
30 |
31 | # Get the long description from the relevant file.
32 | longDescription = ''
33 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f:
34 | longDescription = f.read()
35 |
36 | # Load the requirements.
37 | requirements = []
38 | with open('requirements.txt') as f:
39 | requirements = f.readlines()
40 |
41 | # Get the content of the scripts folder.
42 | scripts = []
43 | for f in os.listdir(os.path.join(os.path.dirname(__file__), 'mdk', 'scripts')):
44 | if f == 'README.rst':
45 | continue
46 | scripts.append('scripts/%s' % (f))
47 |
48 | setup(
49 | name='moodle-sdk',
50 | version=__version__,
51 | description='Moodle Development Kit',
52 | long_description=longDescription,
53 | license='MIT',
54 |
55 | url='https://github.com/FMCorz/mdk',
56 | author='Frédéric Massart',
57 | author_email='fred@fmcorz.net',
58 | classifiers=[
59 | 'Development Status :: 6 - Mature',
60 | 'Intended Audience :: Developers',
61 | 'License :: OSI Approved :: MIT License',
62 | 'Natural Language :: English',
63 | 'Operating System :: MacOS',
64 | 'Operating System :: POSIX :: Linux',
65 | 'Programming Language :: Python :: 2.7',
66 | 'Topic :: Education',
67 | 'Topic :: Software Development',
68 | 'Topic :: Utilities'
69 | ],
70 | keywords='mdk moodle moodle-sdk',
71 |
72 | packages=find_packages(),
73 | package_data={'mdk': ['config-dist.json'] + scripts},
74 | install_requires=requirements,
75 | include_package_data=True,
76 |
77 | entry_points={
78 | 'console_scripts': [
79 | 'mdk = mdk.__main__:main'
80 | ]
81 | }
82 | )
83 |
--------------------------------------------------------------------------------
/mdk/scripts/enrol.php:
--------------------------------------------------------------------------------
1 | libdir . '/accesslib.php');
16 | require_once($CFG->dirroot . '/enrol/manual/lib.php');
17 |
18 | function mdk_get_enrol_instance($courseid) {
19 | global $DB;
20 | static $coursecache = array();
21 | if (!isset($coursecache[$courseid])) {
22 | $coursecache[$courseid] = $DB->get_record('enrol', array('courseid' => $courseid, 'enrol' => 'manual'));
23 | if (!$coursecache[$courseid]) {
24 | mtrace("Could not find manual enrolment method for course {$courseid}.");
25 | }
26 | }
27 | return $coursecache[$courseid];
28 | }
29 |
30 | function mdk_get_role($username) {
31 | static $rolecache = array();
32 | $letter = substr($username, 0, 1);
33 | switch ($letter) {
34 | case 's':
35 | $archetype = 'student';
36 | break;
37 | case 't':
38 | $archetype = 'editingteacher';
39 | break;
40 | case 'm':
41 | $archetype = 'manager';
42 | break;
43 | default:
44 | return false;
45 | }
46 | if (!isset($rolecache[$archetype])) {
47 | $role = get_archetype_roles($archetype);
48 | $rolecache[$archetype] = reset($role);
49 | }
50 | return $rolecache[$archetype];
51 | }
52 |
53 | $sql = "SELECT id, username
54 | FROM {user}
55 | WHERE (username LIKE 's%'
56 | OR username LIKE 't%'
57 | OR username LIKE 'm%')
58 | AND deleted = 0
59 | AND username NOT LIKE 'tool_generator_%'";
60 | $users = $DB->get_recordset_sql($sql, array());
61 | $courses = $DB->get_records_select('course', 'id > ?', array(1), '', 'id, startdate');
62 | $plugin = new enrol_manual_plugin();
63 |
64 | foreach ($users as $user) {
65 | mtrace('Enrolling ' . $user->username);
66 | $role = mdk_get_role($user->username);
67 | if (!$role) {
68 | continue;
69 | }
70 | foreach ($courses as $course) {
71 | $instance = mdk_get_enrol_instance($course->id);
72 | if (!$instance) {
73 | continue;
74 | }
75 | // Enrol the day before the course startdate, because if we create a course today its default
76 | // startdate is tomorrow, and we would never realise why the enrolments do not work.
77 | $plugin->enrol_user($instance, $user->id, $role->id, $course->startdate - 86400, 0);
78 | }
79 | }
80 |
81 | $users->close();
82 |
--------------------------------------------------------------------------------
/mdk/commands/purge.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 |
28 |
29 | class PurgeCommand(Command):
30 |
31 | _arguments = [
32 | (
33 | ['-a', '--all'],
34 | {
35 | 'action': 'store_true',
36 | 'dest': 'all',
37 | 'help': 'purge the cache on each instance'
38 | }
39 | ),
40 | (
41 | ['-i', '--integration'],
42 | {
43 | 'action': 'store_true',
44 | 'dest': 'integration',
45 | 'help': 'purge the cache on integration instances'
46 | }
47 | ),
48 | (
49 | ['-s', '--stable'],
50 | {
51 | 'action': 'store_true',
52 | 'dest': 'stable',
53 | 'help': 'purge the cache on stable instances'
54 | }
55 | ),
56 | (
57 | ['-m', '--manual'],
58 | {
59 | 'action': 'store_true',
60 | 'dest': 'manual',
61 | 'help': 'perform a manual deletion of some cache in dataroot before executing the CLI script'
62 | }
63 | ),
64 | (
65 | ['names'],
66 | {
67 | 'default': None,
68 | 'help': 'name of the instances',
69 | 'metavar': 'names',
70 | 'nargs': '*'
71 | }
72 | )
73 | ]
74 | _description = 'Purge the cache of an instance'
75 |
76 | def run(self, args):
77 |
78 | # Resolving instances
79 | names = args.names
80 | if args.all:
81 | names = self.Wp.list()
82 | elif args.integration or args.stable:
83 | names = self.Wp.list(integration=args.integration, stable=args.stable)
84 |
85 | # Doing stuff
86 | Mlist = self.Wp.resolveMultiple(names)
87 | if len(Mlist) < 1:
88 | raise Exception('No instances to work on. Exiting...')
89 |
90 | for M in Mlist:
91 | logging.info('Purging cache on %s' % (M.get('identifier')))
92 |
93 | try:
94 | M.purge(manual=args.manual)
95 | except Exception as e:
96 | logging.error('Could not purge cache: %s' % e)
97 | else:
98 | logging.debug('Cache purged!')
99 |
100 | logging.info('')
101 |
102 | logging.info('Done.')
103 |
--------------------------------------------------------------------------------
/mdk/scripts/webservices.php:
--------------------------------------------------------------------------------
1 | libdir.'/testing/generator/data_generator.php');
9 | require_once($CFG->libdir.'/accesslib.php');
10 | require_once($CFG->libdir.'/externallib.php');
11 | require_once($CFG->dirroot.'/webservice/lib.php');
12 |
13 | // We don't really need to be admin, except to be able to see the generated tokens
14 | // in the admin settings page, while logged in as admin.
15 | cron_setup_user();
16 |
17 | // Enable the Web Services.
18 | set_config('enablewebservices', 1);
19 |
20 | // Enable mobile web services.
21 | set_config('enablemobilewebservice', 1);
22 |
23 | // Enable Web Services documentation.
24 | set_config('enablewsdocumentation', 1);
25 |
26 | // Enable each protocol.
27 | set_config('webserviceprotocols', 'amf,rest,soap,xmlrpc');
28 |
29 | // Create the Web Service user.
30 | $user = $DB->get_record('user', array('username' => 'testtete'));
31 | if (!$user) {
32 | $user = new stdClass();
33 | $user->username = 'testtete';
34 | $user->firstname = 'Web';
35 | $user->lastname = 'Service';
36 | $user->password = 'test';
37 |
38 | $dg = new testing_data_generator();
39 | $user = $dg->create_user($user);
40 | }
41 |
42 | // Create a role for Web Services with all permissions.
43 | if (!$roleid = $DB->get_field('role', 'id', array('shortname' => 'testtete'))) {
44 | $roleid = create_role('Web Service', 'testtete', 'MDK: All permissions given by default.', '');
45 | }
46 | $context = context_system::instance();
47 | set_role_contextlevels($roleid, array($context->contextlevel));
48 | role_assign($roleid, $user->id, $context->id);
49 | if (method_exists($context, 'get_capabilities')) {
50 | $capabilities = $context->get_capabilities();
51 | } else{
52 | $capabilities = fetch_context_capabilities($context);
53 | }
54 | foreach ($capabilities as $capability) {
55 | assign_capability($capability->name, CAP_ALLOW, $roleid, $context->id, true);
56 | }
57 | $context->mark_dirty();
58 |
59 | // Create a new service with all functions for the user.
60 | $webservicemanager = new webservice();
61 | if (!$service = $DB->get_record('external_services', array('shortname' => 'mdk_all'))) {
62 | $service = new stdClass();
63 | $service->name = 'MDK: All functions';
64 | $service->shortname = 'mdk_all';
65 | $service->enabled = 1;
66 | $service->restrictedusers = 1;
67 | $service->downloadfiles = 1;
68 | $service->uploadfiles = 1;
69 | $service->id = $webservicemanager->add_external_service($service);
70 | }
71 | $functions = $webservicemanager->get_not_associated_external_functions($service->id);
72 | foreach ($functions as $function) {
73 | $webservicemanager->add_external_function_to_service($function->name, $service->id);
74 | }
75 | if (!$webservicemanager->get_ws_authorised_user($service->id, $user->id)) {
76 | $adduser = new stdClass();
77 | $adduser->externalserviceid = $service->id;
78 | $adduser->userid = $user->id;
79 | $webservicemanager->add_ws_authorised_user($adduser);
80 | }
81 |
82 | // Generate a token for the user.
83 | if (!$token = $DB->get_field('external_tokens', 'token', array('userid' => $user->id, 'externalserviceid' => $service->id))) {
84 | $token = external_generate_token(EXTERNAL_TOKEN_PERMANENT, $service->id, $user->id, $context, 0, '');
85 | }
86 | mtrace('User \'webservice\' token: ' . $token);
87 |
--------------------------------------------------------------------------------
/mdk/commands/fix.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 |
26 | import logging
27 | from ..command import Command
28 |
29 |
30 | class FixCommand(Command):
31 |
32 | _arguments = [
33 | (
34 | ['issue'],
35 | {
36 | 'help': 'tracker issue or issue number'
37 | }
38 | ),
39 | (
40 | ['suffix'],
41 | {
42 | 'default': '',
43 | 'help': 'suffix of the branch',
44 | 'nargs': '?'
45 | }
46 | ),
47 | (
48 | ['--autofix'],
49 | {
50 | 'action': 'store_true',
51 | 'default': '',
52 | 'help': 'auto fix the bug related to the issue number'
53 | }
54 | ),
55 | (
56 | ['-n', '--name'],
57 | {
58 | 'default': None,
59 | 'help': 'name of the instance',
60 | 'metavar': 'name'
61 | }
62 | )
63 | ]
64 | _description = 'Creates a branch associated to an MDL issue'
65 |
66 | def run(self, args):
67 |
68 | # Loading instance
69 | M = self.Wp.resolve(args.name)
70 | if not M:
71 | raise Exception('This is not a Moodle instance')
72 |
73 | # Branch name
74 | branch = M.generateBranchName(args.issue, suffix=args.suffix)
75 |
76 | # Track
77 | track = '%s/%s' % (self.C.get('upstreamRemote'), M.get('stablebranch'))
78 |
79 | # Git repo
80 | repo = M.git()
81 |
82 | # Creating and checking out the new branch
83 | if not repo.hasBranch(branch):
84 | if not repo.createBranch(branch, track):
85 | raise Exception('Could not create branch %s' % branch)
86 |
87 | if not repo.checkout(branch):
88 | raise Exception('Error while checkout out branch %s' % branch)
89 |
90 | logging.info('Branch %s checked out' % branch)
91 |
92 | # Auto-fixing the bug
93 | if args.autofix:
94 | logging.info('Auto fixing bug, please wait...')
95 | from time import sleep
96 | sleep(3)
97 | logging.info('That\'s a tricky one! Bear with me.')
98 | sleep(3)
99 | logging.info('Almost there!')
100 | sleep(3)
101 | logging.info('...')
102 | sleep(3)
103 | logging.info('You didn\'t think I was serious, did you?')
104 | sleep(3)
105 | logging.info('Now get to work!')
106 |
--------------------------------------------------------------------------------
/mdk/commands/install.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | import logging
27 | from .. import db
28 | from ..command import Command
29 | from ..tools import mkdir
30 |
31 | DB = db.DB
32 |
33 |
34 | class InstallCommand(Command):
35 |
36 | _description = 'Install a Moodle instance'
37 |
38 | def __init__(self, *args, **kwargs):
39 | super(InstallCommand, self).__init__(*args, **kwargs)
40 | self._arguments = [
41 | (
42 | ['-e', '--engine'],
43 | {
44 | 'action': 'store',
45 | 'choices': ['mariadb', 'mysqli', 'pgsql', 'sqlsrv'],
46 | 'default': self.C.get('defaultEngine'),
47 | 'help': 'database engine to use',
48 | 'metavar': 'engine'
49 | }
50 | ),
51 | (
52 | ['-f', '--fullname'],
53 | {
54 | 'action': 'store',
55 | 'help': 'full name of the instance',
56 | 'metavar': 'fullname'
57 | }
58 | ),
59 | (
60 | ['-r', '--run'],
61 | {
62 | 'action': 'store',
63 | 'help': 'scripts to run after installation',
64 | 'metavar': 'run',
65 | 'nargs': '*'
66 | }
67 | ),
68 | (
69 | ['name'],
70 | {
71 | 'default': None,
72 | 'help': 'name of the instance',
73 | 'metavar': 'name',
74 | 'nargs': '?'
75 | })
76 | ]
77 |
78 | def run(self, args):
79 |
80 | name = args.name
81 | engine = args.engine
82 | fullname = args.fullname
83 |
84 | M = self.Wp.resolve(name)
85 | if not M:
86 | raise Exception('This is not a Moodle instance')
87 |
88 | name = M.get('identifier')
89 | dataDir = self.Wp.getPath(name, 'data')
90 | if not os.path.isdir(dataDir):
91 | mkdir(dataDir, 0777)
92 |
93 | kwargs = {
94 | 'engine': engine,
95 | 'fullname': fullname,
96 | 'dataDir': dataDir,
97 | 'wwwroot': self.Wp.getUrl(name)
98 | }
99 | M.install(**kwargs)
100 |
101 | # Running scripts
102 | if M.isInstalled() and type(args.run) == list:
103 | for script in args.run:
104 | logging.info('Running script \'%s\'' % (script))
105 | try:
106 | M.runScript(script)
107 | except Exception as e:
108 | logging.warning('Error while running the script: %s' % e)
109 |
--------------------------------------------------------------------------------
/mdk/scripts/undev.php:
--------------------------------------------------------------------------------
1 | libdir . '/adminlib.php');
10 |
11 | function mdk_set_config($name, $value, $plugin = null) {
12 | set_config($name, $value, $plugin);
13 | $value = is_bool($value) ? (int) $value : $value;
14 |
15 | if ($plugin) {
16 | // Make a fancy name.
17 | $name = "$plugin/$name";
18 | }
19 | mtrace("Setting $name to $value");
20 | }
21 |
22 | // Load all the settings.
23 | if (class_exists('\core\session\manager')) {
24 | \core\session\manager::set_user(get_admin());
25 | } else {
26 | session_set_user(get_admin());
27 | }
28 | $adminroot = admin_get_root();
29 |
30 |
31 | // Debugging settings.
32 | $settingspage = $adminroot->locate('debugging', true);
33 | $settings = $settingspage->settings;
34 |
35 | // Set developer level.
36 | $default = $settings->debug->get_defaultsetting();
37 | mdk_set_config('debug', $default);
38 |
39 | // Display debug messages.
40 | $default = $settings->debugdisplay->get_defaultsetting();
41 | mdk_set_config('debugdisplay', $default);
42 |
43 | // Debug the performance.
44 | $default = $settings->perfdebug->get_defaultsetting();
45 | mdk_set_config('perfdebug', $default);
46 |
47 | // Debug the information of the page.
48 | $default = $settings->debugpageinfo->get_defaultsetting();
49 | mdk_set_config('debugpageinfo', $default);
50 |
51 |
52 | // Site policies settings.
53 | $settingspage = $adminroot->locate('sitepolicies', true);
54 | $settings = $settingspage->settings;
55 |
56 | // Any kind of password is allowed.
57 | $default = $settings->passwordpolicy->get_defaultsetting();
58 | mdk_set_config('passwordpolicy', $default);
59 |
60 | // Allow web cron.
61 | $default = $settings->cronclionly->get_defaultsetting();
62 | mdk_set_config('cronclionly', $default);
63 |
64 |
65 | // Theme settings.
66 | $settingspage = $adminroot->locate('themesettings', true);
67 | $settings = $settingspage->settings;
68 |
69 | // Allow themes to be changed from the URL.
70 | $default = $settings->allowthemechangeonurl->get_defaultsetting();
71 | mdk_set_config('allowthemechangeonurl', $default);
72 |
73 | // Enable designer mode.
74 | $default = $settings->themedesignermode->get_defaultsetting();
75 | mdk_set_config('themedesignermode', $default);
76 |
77 |
78 | // Language settings.
79 | $settingspage = $adminroot->locate('langsettings', true);
80 | $settings = $settingspage->settings;
81 |
82 | // Restore core_string_manager application caching.
83 | $default = $settings->langstringcache->get_defaultsetting();
84 | mdk_set_config('langstringcache', $default);
85 |
86 |
87 | // Javascript settings.
88 | $settingspage = $adminroot->locate('ajax', true);
89 | $settings = $settingspage->settings;
90 |
91 | // Do not cache JavaScript.
92 | $default = $settings->cachejs->get_defaultsetting();
93 | mdk_set_config('cachejs', $default);
94 |
95 | // Do not use YUI combo loading.
96 | $default = $settings->yuicomboloading->get_defaultsetting();
97 | mdk_set_config('yuicomboloading', $default);
98 |
99 | // Restore modintro for conciencious devs.
100 | $resources = array('book', 'folder', 'imscp', 'page', 'resource', 'url');
101 | foreach ($resources as $r) {
102 | $settingpage = $adminroot->locate('modsetting' . $r, true);
103 | $settings = $settingpage->settings;
104 | if (isset($settings->requiremodintro)) {
105 | $default = $settings->requiremodintro->get_defaultsetting();
106 | mdk_set_config('requiremodintro', $default, $r);
107 | }
108 | }
109 |
110 | // Cache templates.
111 | mdk_set_config('cachetemplates', 1);
112 |
--------------------------------------------------------------------------------
/mdk/commands/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 | from ..scripts import Scripts
28 |
29 |
30 | class RunCommand(Command):
31 |
32 | _arguments = [
33 | (
34 | ['-l', '--list'],
35 | {
36 | 'action': 'store_true',
37 | 'dest': 'list',
38 | 'help': 'list the available scripts'
39 | }
40 | ),
41 | (
42 | ['-a', '--all'],
43 | {
44 | 'action': 'store_true',
45 | 'dest': 'all',
46 | 'help': 'runs the script on each instance'
47 | }
48 | ),
49 | (
50 | ['-i', '--integration'],
51 | {
52 | 'action': 'store_true',
53 | 'dest': 'integration',
54 | 'help': 'runs the script on integration instances'
55 | }
56 | ),
57 | (
58 | ['-s', '--stable'],
59 | {
60 | 'action': 'store_true',
61 | 'dest': 'stable',
62 | 'help': 'runs the script on stable instances'
63 | }
64 | ),
65 | (
66 | ['-g', '--arguments'],
67 | {
68 | 'help': 'a list of arguments to pass to the script. Use --arguments="--list of --arguments" if you need to use dashes. Otherwise add -- after the argument list.',
69 | 'metavar': 'arguments',
70 | 'nargs': '+'
71 | }
72 | ),
73 | (
74 | ['script'],
75 | {
76 | 'nargs': '?',
77 | 'help': 'the name of the script to run'
78 | }
79 | ),
80 | (
81 | ['names'], {
82 | 'default': None,
83 | 'help': 'name of the instances',
84 | 'nargs': '*'
85 | }
86 | )
87 | ]
88 | _description = 'Run a script on a Moodle instance'
89 |
90 | def run(self, args):
91 |
92 | # Printing existing scripts
93 | if args.list:
94 | scripts = Scripts.list()
95 | for script in sorted(scripts.keys()):
96 | print u'%s (%s)' % (script, scripts[script])
97 | return
98 |
99 | # Trigger error when script is missing
100 | if not args.script:
101 | self.argumentError('missing script name')
102 |
103 | # Resolving instances
104 | names = args.names
105 | if args.all:
106 | names = self.Wp.list()
107 | elif args.integration or args.stable:
108 | names = self.Wp.list(integration=args.integration, stable=args.stable)
109 |
110 | # Doing stuff
111 | Mlist = self.Wp.resolveMultiple(names)
112 | if len(Mlist) < 1:
113 | raise Exception('No instances to work on. Exiting...')
114 |
115 | for M in Mlist:
116 | logging.info('Running \'%s\' on \'%s\'' % (args.script, M.get('identifier')))
117 | try:
118 | M.runScript(args.script, stderr=None, stdout=None, arguments=args.arguments)
119 | except Exception as e:
120 | logging.warning('Error while running the script on %s' % M.get('identifier'))
121 | logging.debug(e)
122 | else:
123 | logging.info('')
124 |
125 | logging.info('Done.')
126 |
--------------------------------------------------------------------------------
/mdk/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | def main():
26 |
27 | import sys
28 | import argparse
29 | import os
30 | import re
31 | import logging
32 | from .command import CommandRunner
33 | from .commands import getCommand, commandsList
34 | from .config import Conf
35 | from .tools import process
36 | from .version import __version__
37 |
38 | C = Conf()
39 |
40 | try:
41 | debuglevel = getattr(logging, C.get('debug').upper())
42 | except AttributeError:
43 | debuglevel = logging.INFO
44 |
45 | # Set logging levels.
46 | logging.basicConfig(format='%(message)s', level=debuglevel)
47 | logging.getLogger('requests').setLevel(logging.WARNING) # Reset logging level of 'requests' module.
48 | logging.getLogger('keyring.backend').setLevel(logging.WARNING)
49 |
50 | availaliases = [str(x) for x in C.get('aliases').keys()]
51 | choices = sorted(commandsList + availaliases)
52 |
53 | parser = argparse.ArgumentParser(description='Moodle Development Kit', add_help=False)
54 | parser.add_argument('-h', '--help', action='store_true', help='show this help message and exit')
55 | parser.add_argument('-l', '--list', action='store_true', help='list the available commands')
56 | parser.add_argument('-v', '--version', action='store_true', help='display the current version')
57 | parser.add_argument(*['--%s'%f.decode('base64') for f in ('aWNhbnRyZWFj', 'aWNhbnRyZWFk')], dest='asdf', action='store_true', help=argparse.SUPPRESS)
58 | parser.add_argument('command', metavar='command', nargs='?', help='command to call', choices=choices)
59 | parser.add_argument('args', metavar='arguments', nargs=argparse.REMAINDER, help='arguments of the command')
60 | parsedargs = parser.parse_args()
61 |
62 | cmd = parsedargs.command
63 | args = parsedargs.args
64 |
65 | # There is no command, what do we do?
66 | if not cmd:
67 | if parsedargs.version:
68 | print 'MDK version %s' % __version__
69 | elif parsedargs.asdf:
70 | print 'U29ycnkgRGF2ZSwgTURLIGNhbm5vdCBoZWxwIHlvdSB3aXRoIHRoYXQuLi4='.decode('base64')
71 | elif parsedargs.list:
72 | for c in sorted(commandsList):
73 | print '{0:<15} {1}'.format(c, getCommand(c)._description)
74 | else:
75 | parser.print_help()
76 | sys.exit(0)
77 |
78 | # Looking up for an alias
79 | alias = C.get('aliases.%s' % cmd)
80 | if alias != None:
81 | if alias.startswith('!'):
82 | cmd = alias[1:]
83 | i = 0
84 | # Replace $1, $2, ... with passed arguments
85 | for arg in args:
86 | i += 1
87 | cmd = cmd.replace('$%d' % i, arg)
88 | # Remove unknown $[0-9]
89 | cmd = re.sub(r'\$[0-9]', '', cmd)
90 | result = process(cmd, stdout=None, stderr=None)
91 | sys.exit(result[0])
92 | else:
93 | cmd = alias.split(' ')[0]
94 | args = alias.split(' ')[1:] + args
95 |
96 | cls = getCommand(cmd)
97 | Cmd = cls(C)
98 | Runner = CommandRunner(Cmd)
99 | try:
100 | Runner.run(args, prog='%s %s' % (os.path.basename(sys.argv[0]), cmd))
101 | except Exception as e:
102 | import traceback
103 | info = sys.exc_info()
104 | logging.error('%s: %s', e.__class__.__name__, e)
105 | logging.debug(''.join(traceback.format_tb(info[2])))
106 | sys.exit(1)
107 |
108 | if __name__ == "__main__":
109 | main()
110 |
--------------------------------------------------------------------------------
/mdk/commands/upgrade.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import sys
26 | import logging
27 | from ..command import Command
28 | from ..exceptions import UpgradeNotAllowed
29 |
30 | class UpgradeCommand(Command):
31 |
32 | _arguments = [
33 | (
34 | ['-a', '--all'],
35 | {
36 | 'action': 'store_true',
37 | 'dest': 'all',
38 | 'help': 'upgrade each instance'
39 | }
40 | ),
41 | (
42 | ['-i', '--integration'],
43 | {
44 | 'action': 'store_true',
45 | 'dest': 'integration',
46 | 'help': 'upgrade integration instances'
47 | }
48 | ),
49 | (
50 | ['-s', '--stable'],
51 | {
52 | 'action': 'store_true',
53 | 'dest': 'stable',
54 | 'help': 'upgrade stable instances'
55 | }
56 | ),
57 | (
58 | ['-n', '--no-checkout'],
59 | {
60 | 'action': 'store_true',
61 | 'dest': 'nocheckout',
62 | 'help': 'do not checkout the stable branch before upgrading'
63 | }
64 | ),
65 | (
66 | ['-u', '--update'],
67 | {
68 | 'action': 'store_true',
69 | 'dest': 'update',
70 | 'help': 'update the instance before running the upgrade script'
71 | }
72 | ),
73 | (
74 | ['names'],
75 | {
76 | 'default': None,
77 | 'help': 'name of the instances',
78 | 'metavar': 'names',
79 | 'nargs': '*'
80 | }
81 | )
82 | ]
83 | _description = 'Run the Moodle upgrade script'
84 |
85 | def run(self, args):
86 |
87 | names = args.names
88 | if args.all:
89 | names = self.Wp.list()
90 | elif args.integration or args.stable:
91 | names = self.Wp.list(integration=args.integration, stable=args.stable)
92 |
93 | Mlist = self.Wp.resolveMultiple(names)
94 | if len(Mlist) < 1:
95 | raise Exception('No instances to work on. Exiting...')
96 |
97 | # Updating cache if required
98 | if args.update:
99 | print 'Updating cached repositories'
100 | self.Wp.updateCachedClones(verbose=False)
101 |
102 | errors = []
103 |
104 | for M in Mlist:
105 | if args.update:
106 | logging.info('Updating %s...' % M.get('identifier'))
107 | try:
108 | M.update()
109 | except Exception as e:
110 | errors.append(M)
111 | logging.warning('Error during update. Skipping...')
112 | logging.debug(e)
113 | continue
114 | logging.info('Upgrading %s...' % M.get('identifier'))
115 |
116 | try:
117 | M.upgrade(args.nocheckout)
118 | except UpgradeNotAllowed as e:
119 | logging.info('Skipping upgrade of %s (not allowed)' % (M.get('identifier')))
120 | logging.debug(e)
121 | except Exception as e:
122 | errors.append(M)
123 | logging.warning('Error during the upgrade of %s' % M.get('identifier'))
124 | logging.debug(e)
125 | logging.info('')
126 | logging.info('Done.')
127 |
128 | if errors and len(Mlist) > 1:
129 | logging.info('')
130 | logging.warning('/!\ Some errors occurred on the following instances:')
131 | for M in errors:
132 | logging.warning('- %s' % M.get('identifier'))
133 | # TODO Do not use sys.exit() but handle error code
134 | sys.exit(1)
135 |
--------------------------------------------------------------------------------
/mdk/commands/update.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import sys
26 | import logging
27 | from ..command import Command
28 | from ..exceptions import UpgradeNotAllowed
29 |
30 |
31 | class UpdateCommand(Command):
32 |
33 | _arguments = [
34 | (
35 | ['-a', '--all'],
36 | {
37 | 'action': 'store_true',
38 | 'dest': 'all',
39 | 'help': 'update each instance'
40 | }
41 | ),
42 | (
43 | ['-c', '--cached'],
44 | {
45 | 'action': 'store_true',
46 | 'help': 'only update the cached (mirrored) repositories'
47 | }
48 | ),
49 | (
50 | ['-i', '--integration'],
51 | {
52 | 'action': 'store_true',
53 | 'dest': 'integration',
54 | 'help': 'update integration instances'
55 | }
56 | ),
57 | (
58 | ['-s', '--stable'],
59 | {
60 | 'action': 'store_true',
61 | 'dest': 'stable',
62 | 'help': 'update stable instances'
63 | }
64 | ),
65 | (
66 | ['-u', '--upgrade'],
67 | {
68 | 'action': 'store_true',
69 | 'dest': 'upgrade',
70 | 'help': 'upgrade the instance after successful update'
71 | }
72 | ),
73 | (
74 | ['names'],
75 | {
76 | 'default': None,
77 | 'help': 'name of the instances',
78 | 'metavar': 'names',
79 | 'nargs': '*'
80 | }
81 | )
82 | ]
83 | _description = 'Update the instance from remote'
84 |
85 | def run(self, args):
86 |
87 | if args.cached:
88 | self.updateCached()
89 | return
90 |
91 | # Updating instances
92 | names = args.names
93 | if args.all:
94 | names = self.Wp.list()
95 | elif args.integration or args.stable:
96 | names = self.Wp.list(integration=args.integration, stable=args.stable)
97 |
98 | Mlist = self.Wp.resolveMultiple(names)
99 | if len(Mlist) < 1:
100 | raise Exception('No instances to work on. Exiting...')
101 |
102 | self.updateCached()
103 |
104 | errors = []
105 |
106 | for M in Mlist:
107 | logging.info('Updating %s...' % M.get('identifier'))
108 | try:
109 | M.update()
110 | except Exception as e:
111 | errors.append(M)
112 | logging.warning('Error during the update of %s' % M.get('identifier'))
113 | logging.debug(e)
114 | else:
115 | if args.upgrade:
116 | try:
117 | M.upgrade()
118 | except UpgradeNotAllowed as e:
119 | logging.info('Skipping upgrade of %s (not allowed)' % (M.get('identifier')))
120 | logging.debug(e)
121 | except Exception as e:
122 | errors.append(M)
123 | logging.warning('Error during the upgrade of %s' % M.get('identifier'))
124 | pass
125 | logging.info('')
126 | logging.info('Done.')
127 |
128 | if errors and len(Mlist) > 1:
129 | logging.info('')
130 | logging.warning('/!\ Some errors occurred on the following instances:')
131 | for M in errors:
132 | logging.warning('- %s' % M.get('identifier'))
133 | # Remove sys.exit and handle error code
134 | sys.exit(1)
135 |
136 | def updateCached(self):
137 | # Updating cache
138 | print 'Updating cached repositories'
139 | self.Wp.updateCachedClones(verbose=False)
140 |
--------------------------------------------------------------------------------
/mdk/js.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | from .tools import process
28 | from .config import Conf
29 | from .plugins import PluginManager
30 |
31 | C = Conf()
32 |
33 |
34 | class Js(object):
35 | """Class wrapping JS related functions"""
36 |
37 | _M = None
38 |
39 | def __init__(self, M):
40 | self._M = M
41 |
42 | def shift(self, subsystemOrPlugin=None, module=None):
43 | """Runs shifter"""
44 | path = self.getYUISrcPath(subsystemOrPlugin, module=module)
45 | if not os.path.isdir(path):
46 | raise ValueError("The directory '%s' was not found" % (path))
47 |
48 | paths = []
49 | if module:
50 | paths.append(path)
51 |
52 | else:
53 | dirs = os.listdir(path)
54 | for d in dirs:
55 | if os.path.isdir(os.path.join(path, d, 'js')):
56 | paths.append(os.path.join(path, d))
57 |
58 | shifter = Shifter(path)
59 | for path in paths:
60 | readablePath = path.replace(self._M.get('path'), '')
61 | logging.info('Shifting in %s' % readablePath)
62 | shifter.setCwd(path)
63 | shifter.compile()
64 |
65 | def document(self, outdir):
66 | """Runs documentator"""
67 |
68 | # TODO We should be able to generate outdir from here, using the workplace.
69 | path = self._M.get('path')
70 | documentor = Documentor(path, outdir)
71 | documentor.compile()
72 |
73 | def getYUISrcPath(self, subsystemOrPlugin, module=None):
74 | """Returns the path to the module, or the component"""
75 |
76 | try:
77 | path = PluginManager.getSubsystemDirectory(subsystemOrPlugin, M=self._M)
78 | except ValueError:
79 | (pluginType, name) = PluginManager.getTypeAndName(subsystemOrPlugin)
80 | path = PluginManager.getTypeDirectory(pluginType, M=self._M)
81 | # An exception will be thrown here if we do not find the plugin or component, that is fine.
82 | path = os.path.join(path, name)
83 |
84 | path = os.path.join(path, 'yui', 'src')
85 | if module:
86 | path = os.path.join(path, module)
87 |
88 | return path
89 |
90 |
91 | class Shifter(object):
92 |
93 | _cwd = None
94 |
95 | def __init__(self, cwd=None):
96 | self.setCwd(cwd)
97 |
98 | def compile(self):
99 | """Runs the shifter command in cwd"""
100 | executable = C.get('shifter')
101 | if not executable or not os.path.isfile(executable):
102 | raise Exception('Could not find executable path %s' % (executable))
103 |
104 | cmd = [executable]
105 | (code, out, err) = process(cmd, cwd=self._cwd)
106 | if code != 0:
107 | raise ShifterCompileFailed('Error during shifting at %s' % (self._cwd))
108 |
109 | def setCwd(self, cwd):
110 | self._cwd = cwd
111 |
112 |
113 | class Documentor(object):
114 |
115 | _cwd = None
116 | _outdir = None
117 |
118 | def __init__(self, cwd=None, outdir=None):
119 | self.setCwd(cwd)
120 | self.setOutdir(outdir)
121 |
122 | def compile(self):
123 | """Runs the yuidoc command in cwd"""
124 | executable = C.get('yuidoc')
125 | if not executable or not os.path.isfile(executable):
126 | raise Exception('Could not find executable path %s' % (executable))
127 |
128 | cmd = [executable, '--outdir', self._outdir]
129 |
130 | (code, out, err) = process(cmd, cwd=self._cwd)
131 | if code != 0:
132 | raise YuidocCompileFailed('Error whilst generating documentation')
133 |
134 | def setCwd(self, cwd):
135 | self._cwd = cwd
136 |
137 | def setOutdir(self, outdir):
138 | self._outdir = outdir
139 |
140 |
141 | class YuidocCompileFailed(Exception):
142 | pass
143 | class ShifterCompileFailed(Exception):
144 | pass
145 |
--------------------------------------------------------------------------------
/mdk/commands/info.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 |
28 |
29 | class InfoCommand(Command):
30 |
31 | _arguments = [
32 | (
33 | ['-e', '--edit'],
34 | {
35 | 'dest': 'edit',
36 | 'help': 'value to set to the variable (--var). This value will be set in the config file of the instance. Prepend the value with i: or b: to set as int or boolean. DO NOT use names used by MDK (identifier, stablebranch, ...).',
37 | 'metavar': 'value',
38 | 'nargs': '?'
39 | }
40 | ),
41 | (
42 | ['-i', '--integration'],
43 | {
44 | 'action': 'store_true',
45 | 'dest': 'integration',
46 | 'help': 'used with --list, only display integration instances'
47 | }
48 | ),
49 | (
50 | ['-l', '--list'],
51 | {
52 | 'action': 'store_true',
53 | 'dest': 'list',
54 | 'help': 'list the instances'
55 | }
56 | ),
57 | (
58 | ['-n', '--name-only'],
59 | {
60 | 'action': 'store_true',
61 | 'dest': 'nameonly',
62 | 'help': 'used with --list, only display instances name'
63 | }
64 | ),
65 | (
66 | ['-s', '--stable'],
67 | {
68 | 'action': 'store_true',
69 | 'dest': 'stable',
70 | 'help': 'used with --list, only display stable instances'
71 | }
72 | ),
73 | (
74 | ['-v', '--var'],
75 | {
76 | 'default': None,
77 | 'help': 'variable to output or edit',
78 | 'metavar': 'var',
79 | 'nargs': '?'
80 | }
81 | ),
82 | (
83 | ['name'],
84 | {
85 | 'default': None,
86 | 'help': 'name of the instance',
87 | 'metavar': 'name',
88 | 'nargs': '?'
89 | }
90 | )
91 | ]
92 | _description = 'Information about a Moodle instance'
93 |
94 | def run(self, args):
95 | # List the instances
96 | if args.list:
97 | if args.integration != False or args.stable != False:
98 | l = self.Wp.list(integration=args.integration, stable=args.stable)
99 | else:
100 | l = self.Wp.list()
101 | l.sort()
102 | for i in l:
103 | if not args.nameonly:
104 | M = self.Wp.get(i)
105 | print '{0:<25}'.format(i), M.get('release')
106 | else:
107 | print i
108 |
109 | # Loading instance
110 | else:
111 | M = self.Wp.resolve(args.name)
112 | if not M:
113 | raise Exception('This is not a Moodle instance')
114 |
115 | # Printing/Editing variable.
116 | if args.var != None:
117 | # Edit a value.
118 | if args.edit != None:
119 | val = args.edit
120 | if val.startswith('b:'):
121 | val = True if val[2:].lower() in ['1', 'true'] else False
122 | elif val.startswith('i:'):
123 | try:
124 | val = int(val[2:])
125 | except ValueError:
126 | # Not a valid int, let's consider it a string.
127 | pass
128 | M.updateConfig(args.var, val)
129 | logging.info('Set $CFG->%s to %s on %s' % (args.var, str(val), M.get('identifier')))
130 | else:
131 | print M.get(args.var)
132 |
133 | # Printing info
134 | else:
135 | infos = M.info()
136 | for key in sorted(infos.keys()):
137 | print '{0:<20}: {1}'.format(key, infos[key])
138 |
--------------------------------------------------------------------------------
/mdk/ci.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from jenkinsapi import jenkins
27 | from jenkinsapi.custom_exceptions import JenkinsAPIException, TimeOut
28 | from jenkinsapi.utils.crumb_requester import CrumbRequester
29 | from .config import Conf
30 |
31 | C = Conf()
32 |
33 |
34 | class CI(object):
35 | """Wrapper for Jenkins"""
36 |
37 | SUCCESS = 'S'
38 | FAILURE = 'F'
39 | ERROR = 'E'
40 | WARNING = 'W'
41 |
42 | _jenkins = None
43 | url = None
44 | token = None
45 |
46 | def __init__(self, url=None, token=None, load=True):
47 | self.url = url or C.get('ci.url')
48 | self.token = token or C.get('ci.token')
49 | if load:
50 | self.load()
51 |
52 | @property
53 | def jenkins(self):
54 | """The Jenkins object"""
55 | return self._jenkins
56 |
57 | def load(self):
58 | """Loads the Jenkins object"""
59 |
60 | # Resets the logging level.
61 | logger = logging.getLogger('jenkinsapi.job')
62 | logger.setLevel(logging.WARNING)
63 | logger = logging.getLogger('jenkinsapi.build')
64 | logger.setLevel(logging.WARNING)
65 |
66 | # Loads the jenkins object.
67 | self._jenkins = jenkins.Jenkins(self.url, requester=CrumbRequester(baseurl=self.url))
68 |
69 | def precheckRemoteBranch(self, remote, branch, integrateto, issue=None):
70 | """Runs the precheck job and returns the outcome"""
71 | params = {
72 | 'remote': remote,
73 | 'branch': branch,
74 | 'integrateto': integrateto
75 | }
76 | if issue:
77 | params['issue'] = issue
78 |
79 | job = self.jenkins.get_job('Precheck remote branch')
80 |
81 | try:
82 | invoke = job.invoke(build_params=params, securitytoken=self.token, delay=5, block=True)
83 | except TimeOut:
84 | raise CIException('The build has been in queue for too long. Aborting, please refer to: %s' % job.baseurl)
85 | except JenkinsAPIException:
86 | raise CIException('Failed to invoke the build, check your permissions.')
87 |
88 | build = invoke.get_build()
89 |
90 | logging.info('Waiting for the build to complete, please wait...')
91 | build.block_until_complete(3)
92 |
93 | # Checking the build
94 | outcome = CI.SUCCESS
95 | infos = {'url': build.baseurl}
96 |
97 | if build.is_good():
98 | logging.debug('Build complete, checking precheck results...')
99 |
100 | output = build.get_console()
101 | result = self.parseSmurfResult(output)
102 | if not result:
103 | outcome = CI.FAILURE
104 | else:
105 | outcome = result['smurf']['result']
106 | infos = dict(infos.items() + result.items())
107 |
108 | else:
109 | outcome = CI.FAILURE
110 |
111 | return (outcome, infos)
112 |
113 | def parseSmurfResult(self, output):
114 | """Parse the smurt result"""
115 | result = {}
116 |
117 | for line in output.splitlines():
118 | if not line.startswith('SMURFRESULT'):
119 | continue
120 |
121 | line = line.replace('SMURFRESULT: ', '')
122 | (smurf, rest) = line.split(':')
123 | elements = [smurf]
124 | elements.extend(rest.split(';'))
125 | for element in elements:
126 | data = element.split(',')
127 |
128 | errors = int(data[2])
129 | warnings = int(data[3])
130 |
131 | if errors > 0:
132 | outcome = CI.ERROR
133 | elif warnings > 0:
134 | outcome = CI.WARNING
135 | else:
136 | outcome = CI.SUCCESS
137 |
138 | result[data[0]] = {
139 | 'errors': errors,
140 | 'warnings': warnings,
141 | 'result': outcome
142 | }
143 |
144 | break
145 |
146 | return result
147 |
148 |
149 | class CIException(Exception):
150 | pass
151 |
--------------------------------------------------------------------------------
/mdk/command.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import argparse
26 | import sys
27 |
28 |
29 | class Command(object):
30 | """Represents a command"""
31 |
32 | _arguments = [
33 | (
34 | ['foo'],
35 | {
36 | 'help': 'I\'m an argument'
37 | }
38 | ),
39 | (
40 | ['-b', '--bar'],
41 | {
42 | 'action': 'store_true',
43 | 'help': 'I\'m a flag'
44 | }
45 | )
46 | ]
47 | _description = 'Undocumented command'
48 |
49 | __C = None
50 | __Wp = None
51 |
52 | def __init__(self, config):
53 | self.__C = config
54 |
55 | def argumentError(self, message):
56 | raise CommandArgumentError(message)
57 |
58 | @property
59 | def arguments(self):
60 | return self._arguments
61 |
62 | @property
63 | def C(self):
64 | return self.__C
65 |
66 | @property
67 | def description(self):
68 | return self._description
69 |
70 | def run(self, args):
71 | return True
72 |
73 | @property
74 | def Wp(self):
75 | if not self.__Wp:
76 | from .workplace import Workplace
77 | self.__Wp = Workplace()
78 | return self.__Wp
79 |
80 |
81 | class CommandArgumentError(Exception):
82 | """Exception when a command sends an argument error"""
83 | pass
84 |
85 |
86 | class CommandArgumentFormatter(argparse.HelpFormatter):
87 | """Custom argument formatter"""
88 |
89 | def _get_help_string(self, action):
90 | help = action.help
91 | if '%(default)' not in action.help:
92 | forbiddentypes = ['_StoreTrueAction', '_StoreFalseAction']
93 | if action.__class__.__name__ not in forbiddentypes and action.default is not argparse.SUPPRESS:
94 | defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
95 | if action.option_strings or action.nargs in defaulting_nargs:
96 | help += ' (default: %(default)s)'
97 | return help
98 |
99 |
100 | class CommandArgumentParser(argparse.ArgumentParser):
101 | """Custom argument parser"""
102 |
103 | def error(self, message):
104 | self.print_help(sys.stderr)
105 | self.exit(2, '\n%s: error: %s\n' % (self.prog, message))
106 |
107 |
108 | class CommandRunner(object):
109 | """Executes a command"""
110 |
111 | def __init__(self, command):
112 | self._command = command
113 |
114 | @property
115 | def command(self):
116 | return self._command
117 |
118 | def run(self, sysargs=sys.argv, prog=None):
119 | parser = CommandArgumentParser(description=self.command.description, prog=prog,
120 | formatter_class=CommandArgumentFormatter)
121 | for argument in self.command.arguments:
122 | args = argument[0]
123 | kwargs = argument[1]
124 | if 'sub-commands' in kwargs:
125 | subs = kwargs['sub-commands']
126 | del kwargs['sub-commands']
127 | subparsers = parser.add_subparsers(**kwargs)
128 | for name, sub in subs.items():
129 | subparser = subparsers.add_parser(name, **sub[0])
130 | defaults = {args[0]: name}
131 | subparser.set_defaults(**defaults)
132 | for subargument in sub[1]:
133 | sargs = subargument[0]
134 | skwargs = subargument[1]
135 | if skwargs.has_key('silent'):
136 | del skwargs['silent']
137 | skwargs['help'] = argparse.SUPPRESS
138 | subparser.add_argument(*sargs, **skwargs)
139 | else:
140 | if kwargs.has_key('silent'):
141 | del kwargs['silent']
142 | kwargs['help'] = argparse.SUPPRESS
143 | parser.add_argument(*args, **kwargs)
144 | args = parser.parse_args(sysargs)
145 |
146 | try:
147 | self.command.run(args)
148 | except CommandArgumentError as e:
149 | parser.error(e.message)
150 |
151 |
152 | if __name__ == "__main__":
153 | CommandRunner(Command()).run()
154 |
--------------------------------------------------------------------------------
/mdk/commands/precheck.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import sys
26 | import logging
27 | from .. import tools, jira
28 | from ..ci import CI, CIException
29 | from ..command import Command
30 |
31 |
32 | class PrecheckCommand(Command):
33 |
34 | _arguments = [
35 | (
36 | ['-b', '--branch'],
37 | {
38 | 'metavar': 'branch',
39 | 'help': 'the branch to pre-check. Defaults to the current branch.'
40 | }
41 | ),
42 | (
43 | ['-p', '--push'],
44 | {
45 | 'action': 'store_true',
46 | 'help': 'if set, the branch will be pushed to your default remote.'
47 | }
48 | ),
49 | (
50 | ['name'],
51 | {
52 | 'default': None,
53 | 'help': 'name of the instance',
54 | 'metavar': 'name',
55 | 'nargs': '?'
56 | }
57 | )
58 | ]
59 | _description = 'Pre-checks a branch on the CI server'
60 |
61 | FAILED = -1
62 |
63 | def run(self, args):
64 | M = self.Wp.resolve(args.name)
65 | if not M:
66 | raise Exception('This is not a Moodle instance')
67 |
68 | against = M.get('stablebranch')
69 | branch = args.branch or M.currentBranch()
70 | if branch == 'HEAD':
71 | raise Exception('Cannot pre-check the HEAD branch')
72 | elif branch == against:
73 | raise Exception('Cannot pre-check the stable branch')
74 |
75 | parsedbranch = tools.parseBranch(branch)
76 | if not parsedbranch:
77 | raise Exception('Could not parse the branch')
78 |
79 | issue = parsedbranch['issue']
80 |
81 | if args.push:
82 | J = jira.Jira()
83 | if J.isSecurityIssue('MDL-%s' % (issue)):
84 | raise Exception('Security issues cannot be pre-checked')
85 |
86 | remote = self.C.get('myRemote')
87 | logging.info('Pushing branch \'%s\' to remote \'%s\'', branch, remote)
88 | result = M.git().push(remote, branch)
89 | if result[0] != 0:
90 | raise Exception('Could not push the branch:\n %s' % result[2])
91 |
92 | ci = CI()
93 | try:
94 | # TODO Remove that ugly hack to get the read-only remote.
95 | logging.info('Invoking the build on the CI server...')
96 | (outcome, infos) = ci.precheckRemoteBranch(self.C.get('repositoryUrl'), branch, against, 'MDL-%s' % issue)
97 | except CIException as e:
98 | raise e
99 |
100 |
101 | if outcome == CI.FAILURE:
102 | logging.warning('Build failed, please refer to:\n %s', infos.get('url', '[Unknown URL]'))
103 | sys.exit(self.FAILED)
104 |
105 | elif outcome == CI.SUCCESS:
106 | logging.info('Precheck passed, good work!')
107 | sys.exit(0)
108 |
109 |
110 | # If we get here, that was a fail.
111 | if outcome == CI.ERROR:
112 | logging.info('Precheck FAILED with ERRORS.')
113 | else:
114 | logging.info('Precheck FAILED with WARNINGS.')
115 | logging.info('')
116 |
117 | mapping = {
118 | 'phplint': 'PHP Lint',
119 | 'phpcs': 'PHP coding style',
120 | 'js': 'Javascript',
121 | 'css': 'CSS',
122 | 'phpdoc': 'PHP Doc',
123 | 'commit': 'Commit message',
124 | 'savepoint': 'Update/Upgrade',
125 | 'thirdparty': 'Third party',
126 | 'grunt': 'Grunt',
127 | 'shifter': 'Shifter',
128 | 'travis': 'Travis CI',
129 | 'mustache': 'Mustache templates'
130 | }
131 |
132 | for key in mapping:
133 | details = infos.get(key, {})
134 | result = details.get('result', CI.SUCCESS) # At times we don't receive the result, in which case we assume success.
135 | symbol = ' ' if result == CI.SUCCESS else ('!' if result == CI.WARNING else 'X')
136 | print ' [{}] {:<20}({} errors, {} warnings)'.format(symbol, mapping.get(key, key), details.get('errors', '0'), details.get('warnings', '0'))
137 |
138 | print ''
139 | print 'More details at: %s' % infos.get('url')
140 | sys.exit(self.FAILED)
141 |
--------------------------------------------------------------------------------
/mdk/commands/backup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import time
26 | import logging
27 | from distutils.errors import DistutilsFileError
28 | from .. import backup
29 | from ..command import Command
30 | from ..exceptions import *
31 |
32 |
33 | class BackupCommand(Command):
34 |
35 | _arguments = [
36 | (
37 | ['-i', '--info'],
38 | {
39 | 'dest': 'info',
40 | 'help': 'lists all the information about a backup',
41 | 'metavar': 'backup'
42 | }
43 | ),
44 | (
45 | ['-l', '--list'],
46 | {
47 | 'action': 'store_true',
48 | 'dest': 'list',
49 | 'help': 'list the backups'
50 | },
51 | ),
52 | (
53 | ['-r', '--restore'],
54 | {
55 | 'dest': 'restore',
56 | 'help': 'restore a backup',
57 | 'metavar': 'backup'
58 | }
59 | ),
60 | (
61 | ['name'],
62 | {
63 | 'default': None,
64 | 'help': 'name of the instance',
65 | 'nargs': '?'
66 | }
67 | )
68 | ]
69 |
70 | _description = 'Backup a Moodle instance'
71 |
72 | def run(self, args):
73 | name = args.name
74 | BackupManager = backup.BackupManager()
75 |
76 | # List the backups
77 | if args.list:
78 | backups = BackupManager.list()
79 | for key in sorted(backups.keys()):
80 | B = backups[key]
81 | backuptime = time.ctime(B.get('backup_time'))
82 | print '{0:<25}: {1:<30} {2}'.format(key, B.get('release'), backuptime)
83 |
84 | # Displays backup information
85 | elif args.info:
86 | name = args.info
87 |
88 | # Resolve the backup
89 | if not name or not BackupManager.exists(name):
90 | raise Exception('This is not a valid backup')
91 |
92 | # Restore process
93 | B = BackupManager.get(name)
94 | infos = B.infos
95 | print 'Displaying information about %s' % name
96 | for key in sorted(infos.keys()):
97 | print '{0:<20}: {1}'.format(key, infos[key])
98 |
99 | # Restore
100 | elif args.restore:
101 | name = args.restore
102 |
103 | # Resolve the backup
104 | if not name or not BackupManager.exists(name):
105 | raise Exception('This is not a valid backup')
106 |
107 | # Restore process
108 | B = BackupManager.get(name)
109 |
110 | try:
111 | M = B.restore()
112 | except BackupDirectoryExistsException:
113 | raise Exception('Cannot restore an instance on an existing directory. Please remove %s first.' % B.get('identifier') +
114 | 'Run: moodle remove %s' % B.get('identifier'))
115 | except BackupDBExistsException:
116 | raise Exception('The database %s already exists. Please remove it first.' % B.get('dbname') +
117 | 'This command could help: moodle remove %s' % B.get('identifier'))
118 |
119 | # Loads M object and display information
120 | logging.info('')
121 | logging.info('Restored instance information')
122 | logging.info('')
123 | infos = M.info()
124 | for key in sorted(infos.keys()):
125 | print '{0:<20}: {1}'.format(key, infos[key])
126 | logging.info('')
127 |
128 | logging.info('Done.')
129 |
130 | # Backup the instance
131 | else:
132 | M = self.Wp.resolve(name)
133 | if not M:
134 | raise Exception('This is not a Moodle instance')
135 |
136 | try:
137 | BackupManager.create(M)
138 | except BackupDBEngineNotSupported:
139 | raise Exception('Does not support backup for the DB engine %s yet, sorry!' % M.get('dbtype'))
140 |
141 | except DistutilsFileError:
142 | raise Exception('Error while copying files. Check the permissions on the data directory.' +
143 | 'Or run: sudo chmod -R 0777 %s' % M.get('dataroot'))
144 |
145 | logging.info('Done.')
146 |
--------------------------------------------------------------------------------
/mdk/phpunit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | from .config import Conf
28 | from .tools import mkdir, process
29 |
30 | C = Conf()
31 |
32 |
33 | class PHPUnit(object):
34 | """Class wrapping PHPUnit functions"""
35 |
36 | _M = None
37 | _Wp = None
38 |
39 | def __init__(self, Wp, M):
40 | self._Wp = Wp
41 | self._M = M
42 |
43 | def getCommand(self, testcase=None, unittest=None, filter=None, coverage=None, testsuite=None, stopon=None):
44 | """Get the PHPUnit command"""
45 | cmd = []
46 | if self.usesComposer():
47 | cmd.append('vendor/bin/phpunit')
48 | else:
49 | cmd.append('phpunit')
50 |
51 | if coverage:
52 | cmd.append('--coverage-html')
53 | cmd.append(self.getCoverageDir())
54 |
55 | if stopon:
56 | for on in stopon:
57 | cmd.append('--stop-on-%s' % on)
58 |
59 | if testcase:
60 | cmd.append(testcase)
61 | elif unittest:
62 | cmd.append(unittest)
63 | elif filter:
64 | cmd.append('--filter="%s"' % filter)
65 | elif testsuite:
66 | cmd.append('--testsuite')
67 | cmd.append(testsuite)
68 |
69 | return cmd
70 |
71 | def getCoverageDir(self):
72 | """Get the Coverage directory, and create it if required"""
73 | return self.Wp.getExtraDir(self.M.get('identifier'), 'coverage')
74 |
75 | def getCoverageUrl(self):
76 | """Return the code coverage URL"""
77 | return self.Wp.getUrl(self.M.get('identifier'), extra='coverage')
78 |
79 | def init(self, force=False, prefix=None):
80 | """Initialise the PHPUnit environment"""
81 |
82 | if self.M.branch_compare(23, '<'):
83 | raise Exception('PHPUnit is only available from Moodle 2.3')
84 |
85 | # Set PHPUnit data root
86 | phpunit_dataroot = self.M.get('dataroot') + '_phpu'
87 | self.M.updateConfig('phpunit_dataroot', phpunit_dataroot)
88 | if not os.path.isdir(phpunit_dataroot):
89 | mkdir(phpunit_dataroot, 0777)
90 |
91 | # Set PHPUnit prefix
92 | currentPrefix = self.M.get('phpunit_prefix')
93 | phpunit_prefix = prefix or 'phpu_'
94 |
95 | if not currentPrefix or force:
96 | self.M.updateConfig('phpunit_prefix', phpunit_prefix)
97 | elif currentPrefix != phpunit_prefix and self.M.get('dbtype') != 'oci':
98 | # Warn that a prefix is already set and we did not change it.
99 | # No warning for Oracle as we need to set it to something else.
100 | logging.warning('PHPUnit prefix not changed, already set to \'%s\', expected \'%s\'.' % (currentPrefix, phpunit_prefix))
101 |
102 | result = (None, None, None)
103 | exception = None
104 | try:
105 | if force:
106 | result = self.M.cli('/admin/tool/phpunit/cli/util.php', args='--drop', stdout=None, stderr=None)
107 | result = self.M.cli('/admin/tool/phpunit/cli/init.php', stdout=None, stderr=None)
108 | except Exception as exception:
109 | pass
110 |
111 | if exception != None or result[0] > 0:
112 | if result[0] == 129:
113 | raise Exception('PHPUnit is not installed on your system')
114 | elif result[0] > 0:
115 | raise Exception('Something wrong with PHPUnit configuration')
116 | else:
117 | raise exception
118 |
119 | if C.get('phpunit.buildcomponentconfigs'):
120 | try:
121 | result = self.M.cli('/admin/tool/phpunit/cli/util.php', args='--buildcomponentconfigs', stdout=None, stderr=None)
122 | except Exception as exception:
123 | pass
124 |
125 | if exception != None or result[0] > 0:
126 | raise Exception('Unable to build distributed phpunit.xml files for each component')
127 | else:
128 | logging.info('Distributed phpunit.xml files built.')
129 |
130 | logging.info('PHPUnit ready!')
131 |
132 | def run(self, **kwargs):
133 | """Execute the command"""
134 | cmd = self.getCommand(**kwargs)
135 | return process(cmd, self.M.get('path'), None, None)
136 |
137 | def usesComposer(self):
138 | """Return whether or not the instance uses composer, the latter is considered installed"""
139 | return os.path.isfile(os.path.join(self.M.get('path'), 'composer.json'))
140 |
141 | @property
142 | def M(self):
143 | return self._M
144 |
145 | @property
146 | def Wp(self):
147 | return self._Wp
148 |
--------------------------------------------------------------------------------
/mdk/scripts/users.php:
--------------------------------------------------------------------------------
1 | libdir . '/filelib.php');
9 | require_once($CFG->libdir . '/gdlib.php');
10 |
11 | // True to download an avatar.
12 | define('MDK_AVATAR', true);
13 |
14 | // Random data.
15 | $CITIES = ['Perth', 'Brussels', 'London', 'Johannesburg', 'New York', 'Paris', 'Tokyo', 'Manila', 'São Paulo'];
16 | $COUNTRIES = ['AU', 'BE', 'UK', 'SA', 'US', 'FR', 'JP', 'PH', 'BR'];
17 | $DEPARTMENTS = ['Marketing', 'Development', 'Business', 'HR', 'Communication', 'Management'];
18 |
19 | // User generator.
20 | $generator = new mdk_randomapi_users_generator();
21 |
22 | // Fix admin user.
23 | $admin = $DB->get_record('user', array('username' => 'admin'));
24 | if ($admin && empty($admin->email)) {
25 | mtrace('Fill admin user\'s email');
26 | $admin->email = 'admin@example.com';
27 | $DB->update_record('user', $admin);
28 | }
29 |
30 | // Create all the users.
31 | foreach ($generator->get_users() as $user) {
32 | if (empty($user) || empty($user->username)) {
33 | continue;
34 | }
35 | if ($DB->record_exists('user', array('username' => $user->username, 'deleted' => 0))) {
36 | continue;
37 | }
38 |
39 | $locationindex = array_rand($CITIES);
40 |
41 | mtrace('Creating user ' . $user->username);
42 | $u = create_user_record($user->username, $user->password);
43 | $u->firstname = $user->firstname;
44 | $u->lastname = $user->lastname;
45 | $u->email = $user->email;
46 | $u->city = !empty($user->city) ? $user->city : $CITIES[$locationindex];
47 | $u->country = !empty($user->country) ? $user->country : $COUNTRIES[$locationindex];
48 | $u->lang = 'en';
49 | $u->description = '';
50 | $u->url = 'http://moodle.org';
51 | $u->idnumber = '';
52 | $u->institution = 'Moodle HQ';
53 | $u->department = $DEPARTMENTS[array_rand($DEPARTMENTS)];
54 | $u->phone1 = '';
55 | $u->phone2 = '';
56 | $u->address = '';
57 |
58 | // Adds an avatar to the user. Will slow down the process.
59 | if (MDK_AVATAR && !empty($user->pic)) {
60 | $url = new moodle_url($user->pic);
61 |
62 | // Temporary file name
63 | if (empty($CFG->tempdir)) {
64 | $tempdir = $CFG->dataroot . "/temp";
65 | } else {
66 | $tempdir = $CFG->tempdir;
67 | }
68 | $picture = $tempdir . '/' . 'mdk_script_users.jpg';
69 |
70 | download_file_content($url->out(false), null, null, false, 5, 2, false, $picture);
71 |
72 | // Ensures retro compatibility
73 | if (class_exists('context_user')) {
74 | $context = context_user::instance($u->id);
75 | } else {
76 | $context = get_context_instance(CONTEXT_USER, $u->id, MUST_EXIST);
77 | }
78 |
79 | $u->picture = process_new_icon($context, 'user', 'icon', 0, $picture);
80 | }
81 |
82 | $DB->update_record('user', $u);
83 | }
84 |
85 | /**
86 | * Users generator.
87 | */
88 | class mdk_users_generator {
89 |
90 | protected $users;
91 |
92 | public function __construct() {
93 | $this->users = $this->generate_users();
94 | }
95 |
96 | public function generate_users() {
97 | $data = "s1,test,Eric,Cartman,s1@example.com
98 | s2,test,Stan,Marsh,s2@example.com
99 | s3,test,Kyle,Broflovski,s3@example.com
100 | s4,test,Kenny,McCormick,s4@example.com
101 | s5,test,Butters,Stotch,s5@example.com
102 | s6,test,Clyde,Donovan,s6@example.com
103 | s7,test,Jimmy,Valmer,s7@example.com
104 | s8,test,Timmy,Burch,s8@example.com
105 | s9,test,Wendy,Testaburger,s9@example.com
106 | s10,test,Bebe,Stevens,s10@example.com
107 | t1,test,Herbert,Garrison,t1@example.com
108 | t2,test,Sheila,Brovslovski,t2@example.com
109 | t3,test,Liane,Cartman,t3@example.com
110 | m1,test,Officer,Barbady,m1@example.com
111 | m2,test,Principal,Victoria,m2@example.com
112 | m3,test,Randy,Marsh,m3@example.com";
113 |
114 | $id = 3;
115 | $urlparams = array(
116 | 'size' => 160,
117 | 'force' => 'y',
118 | 'default' => 'wavatar'
119 | );
120 | $users = array_map(function($user) use (&$id, $urlparams) {
121 | $data = (object) array_combine(['username', 'password', 'firstname', 'lastname', 'email'], explode(',', trim($user)));
122 | $data->pic = new moodle_url('http://www.gravatar.com/avatar/' . md5($id++ . ':' . $data->username), $urlparams);
123 | return $data;
124 | }, explode("\n", $data));
125 | return $users;
126 | }
127 |
128 | public function get_users() {
129 | return $this->users;
130 | }
131 |
132 | }
133 |
134 | /**
135 | * Users generator from randomuser.me.
136 | */
137 | class mdk_randomapi_users_generator extends mdk_users_generator {
138 |
139 | public function generate_users() {
140 | $curl = new curl([
141 | 'CURLOPT_CONNECTTIMEOUT' => 2,
142 | 'CURLOPT_TIMEOUT' => 5
143 | ]);
144 | $json = $curl->get('https://randomuser.me/api/?inc=name,picture,location,nat&results=16');
145 | $data = json_decode($json);
146 | if (!$data) {
147 | return parent::generate_users();
148 | }
149 |
150 | $usernames = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 't1', 't2', 't3', 'm1', 'm2', 'm3'];
151 | $users = array_map(function($user) use (&$usernames) {
152 | $username = array_shift($usernames);
153 | return (object) [
154 | 'username' => $username,
155 | 'password' => 'test',
156 | 'firstname' => ucfirst($user->name->first),
157 | 'lastname' => ucfirst($user->name->last),
158 | 'email' => $username . '@example.com',
159 | 'city' => ucfirst($user->location->city),
160 | 'country' => $user->nat,
161 | 'pic' => $user->picture->large
162 | ];
163 | }, $data->results);
164 |
165 | return $users;
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/mdk/scripts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | import shutil
27 | import stat
28 | import logging
29 | from pkg_resources import resource_filename
30 | from .tools import process
31 | from .config import Conf
32 | from .exceptions import ScriptNotFound, ConflictInScriptName, UnsupportedScript
33 |
34 | C = Conf()
35 |
36 |
37 | class Scripts(object):
38 |
39 | _supported = ['php', 'sh']
40 | _dirs = None
41 | _list = None
42 |
43 | @classmethod
44 | def dirs(cls):
45 | """Return the directories containing scripts, in priority order"""
46 |
47 | if not cls._dirs:
48 | dirs = ['~/.moodle-sdk']
49 | if C.get('dirs.moodle') != None:
50 | dirs.insert(0, C.get('dirs.moodle'))
51 |
52 | dirs.append('/etc/moodle-sdk')
53 |
54 | # Directory within the package.
55 | # This can point anywhere when the package is installed, or to the folder containing the module when it is not.
56 | packageDir = resource_filename('mdk', 'scripts')
57 | dirs.append(os.path.split(packageDir)[0])
58 |
59 | # Legacy: directory part of the root git repository, only if we can be sure that the parent directory is still MDK.
60 | if os.path.isfile(os.path.join(os.path.dirname(__file__), '..', 'mdk.py')):
61 | dirs.append(os.path.join(os.path.dirname(__file__), '..'))
62 |
63 | i = 0
64 | for d in dirs:
65 | dirs[i] = os.path.expanduser(os.path.join(d, 'scripts'))
66 | i += 1
67 |
68 | cls._dirs = dirs
69 |
70 | return cls._dirs
71 |
72 | @classmethod
73 | def list(cls):
74 | """Return a dict where keys are the name of the scripts
75 | and the value is the directory in which the script is stored"""
76 |
77 | if not cls._list:
78 | scripts = {}
79 |
80 | # Walk through the directories, in reverse to get the higher
81 | # priority last.
82 | dirs = cls.dirs()
83 | dirs.reverse()
84 | for d in dirs:
85 |
86 | if not os.path.isdir(d):
87 | continue
88 |
89 | # For each file found in the directory.
90 | l = os.listdir(d)
91 | for f in l:
92 |
93 | # Check if supported format.
94 | supported = False
95 | for ext in cls._supported:
96 | if f.endswith('.' + ext):
97 | supported = True
98 | break
99 |
100 | if supported:
101 | scripts[f] = d
102 |
103 | cls._list = scripts
104 |
105 | return cls._list
106 |
107 | @classmethod
108 | def find(cls, script):
109 | """Return the path to a script"""
110 |
111 | lst = cls.list()
112 | cli = None
113 | if script in lst.keys():
114 | cli = os.path.join(lst[script], script)
115 | else:
116 | found = 0
117 | for ext in cls._supported:
118 | candidate = script + '.' + ext
119 | if candidate in lst.keys():
120 | scriptFile = candidate
121 | found += 1
122 |
123 | if found > 1:
124 | raise ConflictInScriptName('The script name conflicts with other ones')
125 | elif found == 1:
126 | cli = os.path.join(lst[scriptFile], scriptFile)
127 |
128 | if not cli:
129 | raise ScriptNotFound('Script not found')
130 |
131 | return cli
132 |
133 | @classmethod
134 | def get_script_destination(cls, cli, path):
135 | """Get the final path where the script will be copied"""
136 |
137 | ext = os.path.splitext(cli)[1]
138 |
139 | i = 0
140 | while True:
141 | candidate = os.path.join(path, 'mdkscriptrun{}{}'.format(i if i > 0 else '', ext))
142 | if not os.path.isfile(candidate):
143 | break
144 | i += 1
145 |
146 | return candidate
147 |
148 | @classmethod
149 | def run(cls, script, path, arguments=None, cmdkwargs={}):
150 | """Executes a script at in a certain directory"""
151 |
152 | # Converts arguments to a string.
153 | arguments = '' if arguments == None else arguments
154 | if type(arguments) == list:
155 | arguments = ' '.join(arguments)
156 | arguments = ' ' + arguments
157 |
158 | cli = cls.find(script)
159 | dest = cls.get_script_destination(cli, path)
160 |
161 | if cli.endswith('.php'):
162 | logging.debug('Copying %s to %s' % (cli, dest))
163 | shutil.copyfile(cli, dest)
164 |
165 | cmd = '%s %s %s' % (C.get('php'), dest, arguments)
166 |
167 | result = process(cmd, cwd=path, **cmdkwargs)
168 | os.remove(dest)
169 | elif cli.endswith('.sh'):
170 | logging.debug('Copying %s to %s' % (cli, dest))
171 | shutil.copyfile(cli, dest)
172 | os.chmod(dest, stat.S_IRUSR | stat.S_IXUSR)
173 |
174 | cmd = '%s %s' % (dest, arguments)
175 | result = process(cmd, cwd=path, **cmdkwargs)
176 | os.remove(dest)
177 | else:
178 | raise UnsupportedScript('Script not supported')
179 |
180 | return result[0]
181 |
--------------------------------------------------------------------------------
/mdk/commands/alias.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import argparse
26 | from ..command import Command
27 |
28 |
29 | class AliasCommand(Command):
30 |
31 | _arguments = [
32 | (
33 | ['action'],
34 | {
35 | 'metavar': 'action',
36 | 'help': 'the action to perform',
37 | 'sub-commands':
38 | {
39 | 'list': (
40 | {
41 | 'help': 'list the aliases'
42 | },
43 | []
44 | ),
45 | 'show': (
46 | {
47 | 'help': 'display an alias'
48 | },
49 | [
50 | (
51 | ['alias'],
52 | {
53 | 'type': str,
54 | 'metavar': 'alias',
55 | 'default': None,
56 | 'help': 'alias to display'
57 | }
58 | )
59 | ]
60 | ),
61 | 'add': (
62 | {
63 | 'help': 'adds an alias'
64 | },
65 | [
66 | (
67 | ['alias'],
68 | {
69 | 'type': str,
70 | 'metavar': 'alias',
71 | 'default': None,
72 | 'help': 'alias name'
73 | }
74 | ),
75 | (
76 | ['definition'],
77 | {
78 | 'type': str,
79 | 'metavar': 'command',
80 | 'default': None,
81 | 'nargs': argparse.REMAINDER,
82 | 'help': 'alias definition'
83 | }
84 | )
85 | ]
86 | ),
87 | 'remove': (
88 | {
89 | 'help': 'remove an alias'
90 | },
91 | [
92 | (
93 | ['alias'],
94 | {
95 | 'type': str,
96 | 'metavar': 'alias',
97 | 'default': None,
98 | 'help': 'alias to remove'
99 | }
100 | )
101 | ]
102 | ),
103 | 'set': (
104 | {
105 | 'help': 'update/add an alias'
106 | },
107 | [
108 | (
109 | ['alias'],
110 | {
111 | 'type': str,
112 | 'metavar': 'alias',
113 | 'default': None,
114 | 'help': 'alias name'
115 | }
116 | ),
117 | (
118 | ['definition'],
119 | {
120 | 'type': str,
121 | 'metavar': 'command',
122 | 'default': None,
123 | 'nargs': argparse.REMAINDER,
124 | 'help': 'alias definition'
125 | }
126 | )
127 | ]
128 | )
129 | }
130 | }
131 | )
132 | ]
133 | _description = 'Manage your aliases'
134 |
135 | def run(self, args):
136 | if args.action == 'list':
137 | aliases = self.C.get('aliases')
138 | for alias, command in aliases.items():
139 | print '{0:<20}: {1}'.format(alias, command)
140 |
141 | elif args.action == 'show':
142 | alias = self.C.get('aliases.%s' % args.alias)
143 | if alias != None:
144 | print alias
145 |
146 | elif args.action == 'add':
147 | self.C.add('aliases.%s' % args.alias, ' '.join(args.definition))
148 |
149 | elif args.action == 'set':
150 | self.C.set('aliases.%s' % args.alias, ' '.join(args.definition))
151 |
152 | elif args.action == 'remove':
153 | self.C.remove('aliases.%s' % args.alias)
154 |
--------------------------------------------------------------------------------
/mdk/commands/phpunit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | import gzip
28 | import urllib
29 | from ..command import Command
30 | from ..tools import process, question
31 | from ..phpunit import PHPUnit
32 |
33 |
34 | class PhpunitCommand(Command):
35 |
36 | _arguments = [
37 | (
38 | ['-f', '--force'],
39 | {
40 | 'action': 'store_true',
41 | 'help': 'force the initialisation'
42 | }
43 | ),
44 | (
45 | ['-r', '--run'],
46 | {
47 | 'action': 'store_true',
48 | 'help': 'also run the tests'
49 | }
50 | ),
51 | (
52 | ['-t', '--testcase'],
53 | {
54 | 'default': None,
55 | 'help': 'testcase class to run (From Moodle 2.6)',
56 | 'metavar': 'testcase'
57 | }
58 | ),
59 | (
60 | ['-s', '--testsuite'],
61 | {
62 | 'default': None,
63 | 'help': 'testsuite to run',
64 | 'metavar': 'testsuite'
65 | }
66 | ),
67 | (
68 | ['-u', '--unittest'],
69 | {
70 | 'default': None,
71 | 'help': 'test file to run',
72 | 'metavar': 'path'
73 | }
74 | ),
75 | (
76 | ['-k', '--skip-init'],
77 | {
78 | 'action': 'store_true',
79 | 'dest': 'skipinit',
80 | 'help': 'allows tests to start quicker when the instance is already initialised'
81 | }
82 | ),
83 | (
84 | ['-q', '--stop-on-failure'],
85 | {
86 | 'action': 'store_true',
87 | 'dest': 'stoponfailure',
88 | 'help': 'stop execution upon first failure or error'
89 | }
90 | ),
91 | (
92 | ['-c', '--coverage'],
93 | {
94 | 'action': 'store_true',
95 | 'help': 'creates the HTML code coverage report'
96 | }
97 | ),
98 | (
99 | ['--filter'],
100 | {
101 | 'default': None,
102 | 'help': 'filter to pass through to PHPUnit',
103 | 'metavar': 'filter'
104 | }
105 | ),
106 | (
107 | ['name'],
108 | {
109 | 'default': None,
110 | 'help': 'name of the instance',
111 | 'metavar': 'name',
112 | 'nargs': '?'
113 | }
114 | )
115 | ]
116 | _description = 'Initialize PHPUnit'
117 |
118 | def run(self, args):
119 |
120 | # Loading instance
121 | M = self.Wp.resolve(args.name)
122 | if not M:
123 | raise Exception('This is not a Moodle instance')
124 |
125 | # Check if installed
126 | if not M.get('installed'):
127 | raise Exception('This instance needs to be installed first')
128 |
129 | # Check if testcase option is available.
130 | if args.testcase and M.branch_compare('26', '<'):
131 | self.argumentError('The --testcase option only works with Moodle 2.6 or greater.')
132 |
133 | # Create the Unit test object.
134 | PU = PHPUnit(self.Wp, M)
135 |
136 | # Skip init.
137 | if not args.skipinit:
138 | self.init(M, PU, args)
139 |
140 | # Automatically add the suffix _testsuite.
141 | testsuite = args.testsuite
142 | if testsuite and not testsuite.endswith('_testsuite'):
143 | testsuite += '_testsuite'
144 |
145 | kwargs = {
146 | 'coverage': args.coverage,
147 | 'filter': args.filter,
148 | 'testcase': args.testcase,
149 | 'testsuite': testsuite,
150 | 'unittest': args.unittest,
151 | 'stopon': [] if not args.stoponfailure else ['failure']
152 | }
153 |
154 | if args.run:
155 | PU.run(**kwargs)
156 | if args.coverage:
157 | logging.info('Code coverage is available at: \n %s', (PU.getCoverageUrl()))
158 | else:
159 | logging.info('Start PHPUnit:\n %s', (' '.join(PU.getCommand(**kwargs))))
160 |
161 | def init(self, M, PU, args):
162 | """Initialises PHP Unit"""
163 |
164 | # Install Composer
165 | if PU.usesComposer():
166 | if not os.path.isfile(os.path.join(M.get('path'), 'composer.phar')):
167 | logging.info('Installing Composer')
168 | cliFile = 'phpunit_install_composer.php'
169 | cliPath = os.path.join(M.get('path'), 'phpunit_install_composer.php')
170 | (to, headers) = urllib.urlretrieve('http://getcomposer.org/installer', cliPath)
171 | if headers.dict.get('content-encoding') == 'gzip':
172 | f = gzip.open(cliPath, 'r')
173 | content = f.read()
174 | f.close()
175 | f = open(cliPath, 'w')
176 | f.write(content)
177 | f.close()
178 | M.cli('/' + cliFile, stdout=None, stderr=None)
179 | os.remove(cliPath)
180 | M.cli('composer.phar', args='install --dev', stdout=None, stderr=None)
181 |
182 | # If Oracle, ask the user for a Behat prefix, if not set.
183 | prefix = M.get('phpunit_prefix')
184 | if M.get('dbtype') == 'oci' and (args.force or not prefix or len(prefix) > 2):
185 | while not prefix or len(prefix) > 2:
186 | prefix = question('What prefix would you like to use? (Oracle, max 2 chars)')
187 | else:
188 | prefix = None
189 |
190 | PU.init(force=args.force, prefix=prefix)
191 |
192 |
193 |
--------------------------------------------------------------------------------
/mdk/commands/css.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | import time
28 | import watchdog.events
29 | import watchdog.observers
30 | from .. import css
31 | from ..command import Command
32 |
33 |
34 | class CssCommand(Command):
35 |
36 | _arguments = [
37 | (
38 | ['-c', '--compile'],
39 | {
40 | 'action': 'store_true',
41 | 'dest': 'compile',
42 | 'help': 'compile the theme less files'
43 | }
44 | ),
45 | (
46 | ['-s', '--sheets'],
47 | {
48 | 'action': 'store',
49 | 'dest': 'sheets',
50 | 'default': None,
51 | 'help': 'the sheets to work on without their extensions. When not specified, it is guessed from the less folder.',
52 | 'nargs': '+'
53 | }
54 | ),
55 | (
56 | ['-t', '--theme'],
57 | {
58 | 'action': 'store',
59 | 'dest': 'theme',
60 | 'default': None,
61 | 'help': 'the theme to work on. The default is \'bootstrapbase\' but is ignored if we are in a theme folder.',
62 | }
63 | ),
64 | (
65 | ['-d', '--debug'],
66 | {
67 | 'action': 'store_true',
68 | 'dest': 'debug',
69 | 'help': 'produce an unminified debugging version with source maps'
70 | }
71 | ),
72 | (
73 | ['-w', '--watch'],
74 | {
75 | 'action': 'store_true',
76 | 'dest': 'watch',
77 | 'help': 'watch the directory'
78 | }
79 | ),
80 | (
81 | ['names'],
82 | {
83 | 'default': None,
84 | 'help': 'name of the instances',
85 | 'metavar': 'names',
86 | 'nargs': '*'
87 | }
88 | )
89 | ]
90 | _description = 'Wrapper for CSS functions'
91 |
92 | def run(self, args):
93 |
94 | Mlist = self.Wp.resolveMultiple(args.names)
95 | if len(Mlist) < 1:
96 | raise Exception('No instances to work on. Exiting...')
97 |
98 | # Resolve the theme folder we are in.
99 | if not args.theme:
100 | mpath = os.path.join(Mlist[0].get('path'), 'theme')
101 | cwd = os.path.realpath(os.path.abspath(os.getcwd()))
102 | if cwd.startswith(mpath):
103 | candidate = cwd.replace(mpath, '').strip('/')
104 | while True:
105 | (head, tail) = os.path.split(candidate)
106 | if not head and tail:
107 | # Found the theme.
108 | args.theme = tail
109 | logging.info('You are in the theme \'%s\', using that.' % (args.theme))
110 | break
111 | elif not head and not tail:
112 | # Nothing, let's leave.
113 | break
114 | candidate = head
115 |
116 | # We have not found anything, falling back on the default.
117 | if not args.theme:
118 | args.theme = 'bootstrapbase'
119 |
120 | for M in Mlist:
121 | if args.compile:
122 | logging.info('Compiling theme \'%s\' on %s' % (args.theme, M.get('identifier')))
123 | processor = css.Css(M)
124 | processor.setDebug(args.debug)
125 | if args.debug:
126 | processor.setCompiler('lessc')
127 | elif M.branch_compare(29, '<'):
128 | # Grunt was only introduced for 2.9.
129 | processor.setCompiler('recess')
130 |
131 | processor.compile(theme=args.theme, sheets=args.sheets)
132 |
133 | # Setting up watchdog. This code should be improved when we will have more than a compile option.
134 | observer = None
135 | if args.compile and args.watch:
136 | observer = watchdog.observers.Observer()
137 |
138 | for M in Mlist:
139 | if args.watch and args.compile:
140 | processor = css.Css(M)
141 | processorArgs = {'theme': args.theme, 'sheets': args.sheets}
142 | handler = LessWatcher(M, processor, processorArgs)
143 | observer.schedule(handler, processor.getThemeLessPath(args.theme), recursive=True)
144 | logging.info('Watchdog set up on %s/%s, waiting for changes...' % (M.get('identifier'), args.theme))
145 |
146 | if observer and args.compile and args.watch:
147 | observer.start()
148 |
149 | try:
150 | while True:
151 | time.sleep(1)
152 | except KeyboardInterrupt:
153 | observer.stop()
154 | finally:
155 | observer.join()
156 |
157 |
158 | class LessWatcher(watchdog.events.FileSystemEventHandler):
159 |
160 | _processor = None
161 | _args = None
162 | _ext = '.less'
163 | _M = None
164 |
165 | def __init__(self, M, processor, args):
166 | super(self.__class__, self).__init__()
167 | self._M = M
168 | self._processor = processor
169 | self._args = args
170 |
171 | def on_modified(self, event):
172 | self.process(event)
173 |
174 | def on_moved(self, event):
175 | self.process(event)
176 |
177 | def process(self, event):
178 | if event.is_directory:
179 | return
180 | elif not event.src_path.endswith(self._ext):
181 | return
182 |
183 | filename = event.src_path.replace(self._processor.getThemeLessPath(self._args['theme']), '').strip('/')
184 | logging.info('[%s] Changes detected in %s!' % (self._M.get('identifier'), filename))
185 | self._processor.compile(**self._args)
186 |
--------------------------------------------------------------------------------
/mdk/css.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | from .tools import process
28 | from .config import Conf
29 |
30 | C = Conf()
31 |
32 |
33 | class Css(object):
34 | """Class wrapping CSS related functions"""
35 |
36 | _M = None
37 |
38 | _debug = False
39 | _compiler = 'grunt'
40 |
41 | def __init__(self, M):
42 | self._M = M
43 |
44 | def setCompiler(self, compiler):
45 | self._compiler = compiler
46 |
47 | def setDebug(self, debug):
48 | self._debug = debug
49 |
50 | def compile(self, theme='bootstrapbase', sheets=None):
51 | """Compile LESS sheets contained within a theme"""
52 |
53 | source = self.getThemeLessPath(theme)
54 | dest = self.getThemeCssPath(theme)
55 | if not os.path.isdir(source):
56 | raise Exception('Unknown theme %s, or less directory not found' % (theme))
57 |
58 | if not sheets:
59 | # Guess the sheets from the theme less folder.
60 | sheets = []
61 | for candidate in os.listdir(source):
62 | if os.path.isfile(os.path.join(source, candidate)) and candidate.endswith('.less'):
63 | sheets.append(os.path.splitext(candidate)[0])
64 | elif type(sheets) != list:
65 | sheets = [sheets]
66 |
67 | if len(sheets) < 1:
68 | logging.warning('Could not find any sheets')
69 | return False
70 |
71 | hadErrors = False
72 |
73 | if self._compiler == 'grunt':
74 | sheets = ['moodle']
75 |
76 | for name in sheets:
77 | sheet = name + '.less'
78 | destSheet = name + '.css'
79 |
80 | if not os.path.isfile(os.path.join(source, sheet)):
81 | logging.warning('Could not find file %s' % (sheet))
82 | hadErrors = True
83 | continue
84 |
85 | try:
86 | if self._compiler == 'grunt':
87 | compiler = Grunt(source, os.path.join(source, sheet), os.path.join(dest, destSheet))
88 | elif self._compiler == 'recess':
89 | compiler = Recess(source, os.path.join(source, sheet), os.path.join(dest, destSheet))
90 | elif self._compiler == 'lessc':
91 | compiler = Lessc(self.getThemeDir(), os.path.join(source, sheet), os.path.join(dest, destSheet))
92 |
93 | compiler.setDebug(self._debug)
94 |
95 | compiler.execute()
96 | except CssCompileFailed:
97 | logging.warning('Failed compilation of %s' % (sheet))
98 | hadErrors = True
99 | continue
100 | else:
101 | logging.info('Compiled %s to %s' % (sheet, destSheet))
102 |
103 | return not hadErrors
104 |
105 | def getThemeCssPath(self, theme):
106 | return os.path.join(self.getThemePath(theme), 'style')
107 |
108 | def getThemeLessPath(self, theme):
109 | return os.path.join(self.getThemePath(theme), 'less')
110 |
111 | def getThemeDir(self):
112 | return os.path.join(self._M.get('path'), 'theme')
113 |
114 | def getThemePath(self, theme):
115 | return os.path.join(self.getThemeDir(), theme)
116 |
117 |
118 | class Compiler(object):
119 | """LESS compiler abstract"""
120 |
121 | _compress = True
122 | _debug = False
123 | _cwd = None
124 | _source = None
125 | _dest = None
126 |
127 | def __init__(self, cwd, source, dest):
128 | self._cwd = cwd
129 | self._source = source
130 | self._dest = dest
131 |
132 | def execute(self):
133 | raise Exception('Compiler does not implement execute() method')
134 |
135 | def setCompress(self, compress):
136 | self._compress = compress
137 |
138 | def setDebug(self, debug):
139 | self._debug = debug
140 |
141 |
142 | class Grunt(Compiler):
143 | """Grunt compiler"""
144 |
145 | def execute(self):
146 | executable = C.get('grunt')
147 | if not executable:
148 | raise Exception('Could not find executable path')
149 |
150 | cmd = [executable, 'css']
151 |
152 | (code, out, err) = process(cmd, self._cwd)
153 | if code != 0 or len(out) == 0:
154 | raise CssCompileFailed('Error during compile')
155 |
156 |
157 | class Recess(Compiler):
158 | """Recess compiler"""
159 |
160 | def execute(self):
161 | executable = C.get('recess')
162 | if not executable:
163 | raise Exception('Could not find executable path')
164 |
165 | cmd = [executable, self._source, '--compile']
166 |
167 | if self._compress:
168 | cmd.append('--compress')
169 |
170 | (code, out, err) = process(cmd, self._cwd)
171 | if code != 0 or len(out) == 0:
172 | raise CssCompileFailed('Error during compile')
173 |
174 | # Saving to destination
175 | with open(self._dest, 'w') as f:
176 | f.write(out)
177 |
178 |
179 | class Lessc(Compiler):
180 | """Lessc compiler"""
181 |
182 | def execute(self):
183 | executable = C.get('lessc')
184 | if not executable:
185 | raise Exception('Could not find executable path')
186 |
187 | cmd = [executable]
188 |
189 | sourcePath = os.path.relpath(self._source, self._cwd)
190 | sourceDir = os.path.dirname(sourcePath)
191 |
192 | if self._debug:
193 | cmd.append('--source-map-rootpath=' + sourceDir)
194 | cmd.append('--source-map-map-inline')
195 | self.setCompress(False)
196 |
197 | if self._compress:
198 | cmd.append('--compress')
199 |
200 | # Append the source and destination.
201 | cmd.append(sourcePath)
202 | cmd.append(os.path.relpath(self._dest, self._cwd))
203 |
204 | (code, out, err) = process(cmd, self._cwd)
205 | if code != 0 or len(out) != 0:
206 | raise CssCompileFailed('Error during compile')
207 |
208 |
209 | class CssCompileFailed(Exception):
210 | pass
211 |
--------------------------------------------------------------------------------
/mdk/commands/pull.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import re
26 | import os
27 | import logging
28 | from datetime import datetime
29 | from .. import tools, jira, fetch
30 | from ..command import Command
31 | from ..tools import question
32 |
33 |
34 | class PullCommand(Command):
35 |
36 | _arguments = [
37 | (
38 | ['-i', '--integration'],
39 | {
40 | 'action': 'store_true',
41 | 'help': 'checkout the stable branch before proceeding to the pull. Short for --mode integration.'
42 | }
43 | ),
44 | (
45 | ['-n', '--no-merge'],
46 | {
47 | 'action': 'store_true',
48 | 'dest': 'nomerge',
49 | 'help': 'checkout the remote branch without merging. Short for --mode checkout.'
50 | }
51 | ),
52 | (
53 | ['--fetch-only'],
54 | {
55 | 'action': 'store_true',
56 | 'dest': 'fetchonly',
57 | 'help': 'only fetches the remote branch, you can then use FETCH_HEAD. Short for --mode fetch.'
58 | }
59 | ),
60 | (
61 | ['-t', '--testing'],
62 | {
63 | 'action': 'store_true',
64 | 'help': 'checkout a testing branch before proceeding to the pull. Short for --mode testing.'
65 | }
66 | ),
67 | (
68 | ['-m', '--mode'],
69 | {
70 | 'action': 'store',
71 | 'choices': ['checkout', 'fetch', 'integration', 'pull', 'testing'],
72 | 'default': 'pull',
73 | 'help': 'define the mode to use'
74 | }
75 | ),
76 | (
77 | ['-p', '--prompt'],
78 | {
79 | 'action': 'store_true',
80 | 'help': 'prompts the user to choose the patch to download.'
81 | }
82 | ),
83 | (
84 | ['issue'],
85 | {
86 | 'default': None,
87 | 'help': 'tracker issue to pull from (MDL-12345, 12345). If not specified, read from current branch.',
88 | 'metavar': 'issue',
89 | 'nargs': '?'
90 | }
91 | )
92 | ]
93 | _description = 'Pull a branch from a tracker issue'
94 |
95 | def run(self, args):
96 |
97 | M = self.Wp.resolve()
98 | if not M:
99 | raise Exception('This is not a Moodle instance')
100 |
101 | # Get the mode.
102 | mode = args.mode
103 | if args.fetchonly:
104 | mode = 'fetch'
105 | elif args.nomerge:
106 | mode = 'checkout'
107 | elif args.testing:
108 | mode = 'testing'
109 | elif args.integration:
110 | mode = 'integration'
111 |
112 | # Prompt?
113 | prompt = args.prompt
114 |
115 | # Tracker issue number.
116 | issuenb = args.issue
117 | if not issuenb:
118 | parsedbranch = tools.parseBranch(M.currentBranch())
119 | if not parsedbranch:
120 | raise Exception('Could not extract issue number from %s' % M.currentBranch())
121 | issuenb = parsedbranch['issue']
122 |
123 | issue = re.sub(r'(MDL|mdl)(-|_)?', '', issuenb)
124 | mdl = 'MDL-' + issue
125 |
126 | # Reading the information about the current instance.
127 | branch = M.get('branch')
128 |
129 | # Get information from Tracker
130 | logging.info('Retrieving information about %s from Moodle Tracker' % (mdl))
131 | fetcher = fetch.FetchTracker(M)
132 |
133 | try:
134 | if not prompt:
135 | fetcher.setFromTracker(mdl, branch)
136 | except (fetch.FetchTrackerRepoException, fetch.FetchTrackerBranchException) as e:
137 | prompt = True
138 |
139 | if prompt:
140 | patches = self.pickPatches(mdl)
141 | if not patches:
142 | raise Exception('Could not find any relevant information for a successful pull')
143 | fetcher.usePatches(patches)
144 |
145 | if mode == 'pull':
146 | fetcher.pull()
147 | elif mode == 'checkout':
148 | fetcher.checkout()
149 | elif mode == 'fetch':
150 | fetcher.fetch()
151 | elif mode == 'integration':
152 | fetcher.pull(into=M.get('stablebranch'))
153 | elif mode == 'testing':
154 | i = 0
155 | while True:
156 | i += 1
157 | suffix = 'test' if i <= 1 else 'test' + str(i)
158 | newBranch = M.generateBranchName(issue, suffix=suffix, version=branch)
159 | if not M.git().hasBranch(newBranch):
160 | break
161 | fetcher.pull(into=newBranch, track=M.get('stablebranch'))
162 |
163 | def pickPatches(self, mdl):
164 | """Prompts the user to pick a patch"""
165 |
166 | J = jira.Jira()
167 | patches = J.getAttachments(mdl)
168 | patches = {k: v for k, v in patches.items() if v.get('filename').endswith('.patch')}
169 | toApply = []
170 |
171 | if len(patches) < 1:
172 | return False
173 |
174 | mapping = {}
175 | i = 1
176 | for key in sorted(patches.keys()):
177 | patch = patches[key]
178 | mapping[i] = patch
179 | print '{0:<2}: {1:<60} {2}'.format(i, key[:60], datetime.strftime(patch.get('date'), '%Y-%m-%d %H:%M'))
180 | i += 1
181 |
182 | while True:
183 | try:
184 | ids = question('What patches would you like to apply?')
185 | if ids.lower() == 'ankit':
186 | logging.warning('Sorry, I am unable to punch a child at the moment...')
187 | continue
188 | elif ids:
189 | ids = re.split(r'\s*[, ]\s*', ids)
190 | toApply = [mapping[int(i)] for i in ids if int(i) in mapping.keys()]
191 | except ValueError:
192 | logging.warning('Error while parsing the list of patches, try a little harder.')
193 | continue
194 | break
195 |
196 | return toApply
197 |
198 |
--------------------------------------------------------------------------------
/mdk/commands/tracker.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | from datetime import datetime
26 | import textwrap
27 | import re
28 | from ..command import Command
29 | from ..jira import Jira
30 | from ..tools import parseBranch, getText
31 |
32 |
33 | class TrackerCommand(Command):
34 |
35 | _arguments = [
36 | (
37 | ['-t', '--testing'],
38 | {
39 | 'action': 'store_true',
40 | 'help': 'include testing instructions'
41 | }
42 | ),
43 | (
44 | ['issue'],
45 | {
46 | 'help': 'MDL issue number. Guessed from the current branch if not specified.',
47 | 'nargs': '?'
48 | }
49 | ),
50 | (
51 | ['--add-labels'],
52 | {
53 | 'action': 'store',
54 | 'dest': 'addlabels',
55 | 'help': 'add the specified labels to the issue',
56 | 'metavar': 'labels',
57 | 'nargs': '+',
58 | }
59 | ),
60 | (
61 | ['--remove-labels'],
62 | {
63 | 'action': 'store',
64 | 'dest': 'removelabels',
65 | 'help': 'remove the specified labels from the issue',
66 | 'metavar': 'labels',
67 | 'nargs': '+',
68 | }
69 | ),
70 | (
71 | ['--comment'],
72 | {
73 | 'action': 'store_true',
74 | 'help': 'add a comment to the issue',
75 | }
76 | )
77 | ]
78 | _description = 'Interact with Moodle tracker'
79 |
80 | Jira = None
81 | mdl = None
82 |
83 | def run(self, args):
84 |
85 | issue = None
86 | if not args.issue:
87 | M = self.Wp.resolve()
88 | if M:
89 | parsedbranch = parseBranch(M.currentBranch())
90 | if parsedbranch:
91 | issue = parsedbranch['issue']
92 | else:
93 | issue = args.issue
94 |
95 | if not issue or not re.match('(MDL|mdl)?(-|_)?[1-9]+', issue):
96 | raise Exception('Invalid or unknown issue number')
97 |
98 | self.Jira = Jira()
99 | self.mdl = 'MDL-' + re.sub(r'(MDL|mdl)(-|_)?', '', issue)
100 |
101 | if args.addlabels:
102 | if 'triaged' in args.addlabels:
103 | self.argumentError('The label \'triaged\' cannot be added using MDK')
104 | elif 'triaging_in_progress' in args.addlabels:
105 | self.argumentError('The label \'triaging_in_progress\' cannot be added using MDK')
106 | self.Jira.addLabels(self.mdl, args.addlabels)
107 |
108 | if args.removelabels:
109 | if 'triaged' in args.removelabels:
110 | self.argumentError('The label \'triaged\' cannot be removed using MDK')
111 | elif 'triaging_in_progress' in args.removelabels:
112 | self.argumentError('The label \'triaging_in_progress\' cannot be removed using MDK')
113 | self.Jira.removeLabels(self.mdl, args.removelabels)
114 |
115 | if args.comment:
116 | comment = getText()
117 | self.Jira.addComment(self.mdl, comment)
118 |
119 | self.info(args)
120 |
121 | def info(self, args):
122 | """Display classic information about an issue"""
123 | issue = self.Jira.getIssue(self.mdl)
124 |
125 | title = u'%s: %s' % (issue['key'], issue['fields']['summary'])
126 | created = datetime.strftime(Jira.parseDate(issue['fields'].get('created')), '%Y-%m-%d %H:%M')
127 | resolution = u'' if issue['fields']['resolution'] == None else u'(%s)' % (issue['fields']['resolution']['name'])
128 | resolutiondate = u''
129 | if issue['fields'].get('resolutiondate') != None:
130 | resolutiondate = datetime.strftime(Jira.parseDate(issue['fields'].get('resolutiondate')), '%Y-%m-%d %H:%M')
131 | print u'-' * 72
132 | for l in textwrap.wrap(title, 68, initial_indent=' ', subsequent_indent=' '):
133 | print l
134 | print u' {0} - {1} - {2}'.format(issue['fields']['issuetype']['name'], issue['fields']['priority']['name'], u'https://tracker.moodle.org/browse/' + issue['key'])
135 | status = u'{0} {1} {2}'.format(issue['fields']['status']['name'], resolution, resolutiondate).strip()
136 | print u' {0}'.format(status)
137 |
138 | print u'-' * 72
139 | components = u'{0}: {1}'.format('Components', ', '.join([c['name'] for c in issue['fields']['components']]))
140 | for l in textwrap.wrap(components, 68, initial_indent=' ', subsequent_indent=' '):
141 | print l
142 | if issue['fields']['labels']:
143 | labels = u'{0}: {1}'.format('Labels', ', '.join(issue['fields']['labels']))
144 | for l in textwrap.wrap(labels, 68, initial_indent=' ', subsequent_indent=' '):
145 | print l
146 |
147 | vw = u'[ V: %d - W: %d ]' % (issue['fields']['votes']['votes'], issue['fields']['watches']['watchCount'])
148 | print '{0:->70}--'.format(vw)
149 | print u'{0:<20}: {1} ({2}) on {3}'.format('Reporter', issue['fields']['reporter']['displayName'], issue['fields']['reporter']['name'], created)
150 |
151 | if issue['fields'].get('assignee') != None:
152 | print u'{0:<20}: {1} ({2})'.format('Assignee', issue['fields']['assignee']['displayName'], issue['fields']['assignee']['name'])
153 | if issue['named'].get('Peer reviewer'):
154 | print u'{0:<20}: {1} ({2})'.format('Peer reviewer', issue['named']['Peer reviewer']['displayName'], issue['named']['Peer reviewer']['name'])
155 | if issue['named'].get('Integrator'):
156 | print u'{0:<20}: {1} ({2})'.format('Integrator', issue['named']['Integrator']['displayName'], issue['named']['Integrator']['name'])
157 | if issue['named'].get('Tester'):
158 | print u'{0:<20}: {1} ({2})'.format('Tester', issue['named']['Tester']['displayName'], issue['named']['Tester']['name'])
159 |
160 | if args.testing and issue['named'].get('Testing Instructions'):
161 | print u'-' * 72
162 | print u'Testing instructions:'
163 | for l in issue['named'].get('Testing Instructions').split('\r\n'):
164 | print ' ' + l
165 |
166 | print u'-' * 72
167 |
--------------------------------------------------------------------------------
/mdk/commands/push.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from .. import tools, jira
27 | from ..command import Command
28 | from ..tools import getMDLFromCommitMessage
29 |
30 |
31 | class PushCommand(Command):
32 |
33 | _description = 'Push a branch to a remote'
34 |
35 | def __init__(self, *args, **kwargs):
36 | super(PushCommand, self).__init__(*args, **kwargs)
37 | self._arguments = [
38 | (
39 | ['-b', '--branch'],
40 | {
41 | 'metavar': 'branch',
42 | 'help': 'the branch to push. Default is current branch.'
43 | }
44 | ),
45 | (
46 | ['-f', '--force'],
47 | {
48 | 'action': 'store_true',
49 | 'help': 'force the push (does not apply on the stable branch)'
50 | }
51 | ),
52 | (
53 | ['-r', '--remote'],
54 | {
55 | 'help': 'remote to push to. Default is your remote.',
56 | 'default': self.C.get('myRemote'),
57 | 'metavar': 'remote'
58 | }
59 | ),
60 | (
61 | ['-p', '--patch'],
62 | {
63 | 'action': 'store_true',
64 | 'help': 'instead of pushing to a remote, this will upload a patch file to the tracker. Security issues use this by default. This option discards most other flags.'
65 | }
66 | ),
67 | (
68 | ['-t', '--update-tracker'],
69 | {
70 | 'const': True,
71 | 'dest': 'updatetracker',
72 | 'help': 'also add the diff information to the tracker issue. If gitref is passed, it is used as a starting point for the diff URL.',
73 | 'metavar': 'gitref',
74 | 'nargs': '?'
75 | }
76 | ),
77 | (
78 | ['-s', '--include-stable'],
79 | {
80 | 'action': 'store_true',
81 | 'dest': 'includestable',
82 | 'help': 'also push the stable branch (MOODLE_xx_STABLE, master)'
83 | }
84 | ),
85 | (
86 | ['-k', '--force-stable'],
87 | {
88 | 'action': 'store_true',
89 | 'dest': 'forcestable',
90 | 'help': 'force the push on the stable branch'
91 | }
92 | ),
93 | (
94 | ['name'],
95 | {
96 | 'default': None,
97 | 'help': 'name of the instance to work on',
98 | 'metavar': 'name',
99 | 'nargs': '?'
100 | }
101 | )
102 | ]
103 |
104 | def run(self, args):
105 |
106 | M = self.Wp.resolve(args.name)
107 | if not M:
108 | raise Exception('This is not a Moodle instance')
109 |
110 | # Setting remote
111 | remote = args.remote
112 |
113 | # Setting branch
114 | if args.branch == None:
115 | branch = M.currentBranch()
116 | if branch == 'HEAD':
117 | raise Exception('Cannot push HEAD branch')
118 | else:
119 | branch = args.branch
120 |
121 | # Extra test to see if the commit message is correct. This prevents easy typos in branch or commit messages.
122 | parsedbranch = tools.parseBranch(branch)
123 | if parsedbranch or branch != M.get('stablebranch'):
124 | message = M.git().messages(count=1)[0]
125 |
126 | mdl = getMDLFromCommitMessage(message)
127 |
128 | if parsedbranch:
129 | branchmdl = 'MDL-%s' % (parsedbranch['issue'])
130 | else:
131 | branchmdl = branch
132 |
133 | if not mdl or mdl != branchmdl:
134 | if not mdl:
135 | print 'The MDL number could not be found in the commit message.'
136 | print 'Commit: %s' % (message)
137 |
138 | elif mdl != branchmdl:
139 | print 'The MDL number in the last commit does not match the branch being pushed to.'
140 | print 'Branch: \'%s\' vs. commit: \'%s\'' % (branchmdl, mdl)
141 |
142 | answer = tools.question('Are you sure you want to continue?', default='n')
143 | if answer.lower()[0] != 'y':
144 | print 'Exiting...'
145 | return
146 |
147 | J = jira.Jira()
148 |
149 | # If the mode is not set to patch yet, and we can identify the MDL number.
150 | if not args.patch and parsedbranch:
151 | mdlIssue = 'MDL-%s' % (parsedbranch['issue'])
152 | try:
153 | args.patch = J.isSecurityIssue(mdlIssue)
154 | if args.patch:
155 | logging.info('%s appears to be a security issue, switching to patch mode...', mdlIssue)
156 | except jira.JiraIssueNotFoundException:
157 | # The issue was not found, do not perform
158 | logging.warn('Could not check if %s is a security issue', mdlIssue)
159 |
160 | if args.patch:
161 | if not M.pushPatch(branch):
162 | return
163 |
164 | else:
165 | # Pushing current branch
166 | logging.info('Pushing branch %s to remote %s...' % (branch, remote))
167 | result = M.git().push(remote, branch, force=args.force)
168 | if result[0] != 0:
169 | raise Exception(result[2])
170 |
171 | # Update the tracker
172 | if args.updatetracker != None:
173 | ref = None if args.updatetracker == True else args.updatetracker
174 | M.updateTrackerGitInfo(branch=branch, ref=ref)
175 |
176 | # Pushing stable branch
177 | if args.includestable:
178 | branch = M.get('stablebranch')
179 | logging.info('Pushing branch %s to remote %s...' % (branch, remote))
180 | result = M.git().push(remote, branch, force=args.forcestable)
181 | if result[0] != 0:
182 | raise Exception(result[2])
183 |
184 | logging.info('Done.')
185 |
--------------------------------------------------------------------------------
/mdk/commands/rebase.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | from ..command import Command
27 |
28 |
29 | class RebaseCommand(Command):
30 |
31 | _description = 'Rebase branches'
32 |
33 | def __init__(self, *args, **kwargs):
34 | super(RebaseCommand, self).__init__(*args, **kwargs)
35 | self._arguments = [
36 | (
37 | ['-i', '--issues'],
38 | {
39 | 'help': 'issues to be rebased',
40 | 'metavar': 'issues',
41 | 'nargs': '+',
42 | 'required': True
43 | }
44 | ),
45 | (
46 | ['-s', '--suffix'],
47 | {
48 | 'help': 'the suffix of the branch of those issues',
49 | 'metavar': 'suffix'
50 | }
51 | ),
52 | (
53 | ['-v', '--versions'],
54 | {
55 | 'choices': [str(x) for x in range(13, int(self.C.get('masterBranch')))] + ['master'],
56 | 'help': 'versions to rebase the issues on. Ignored if names is set.',
57 | 'metavar': 'version',
58 | 'nargs': '+'
59 | }
60 | ),
61 | (
62 | ['-p', '--push'],
63 | {
64 | 'action': 'store_true',
65 | 'help': 'push the branch after successful rebase'
66 | }
67 | ),
68 | (
69 | ['-t', '--update-tracker'],
70 | {
71 | 'action': 'store_true',
72 | 'dest': 'updatetracker',
73 | 'help': 'to use with --push, also add the diff information to the tracker issue'
74 | }
75 | ),
76 | (
77 | ['-r', '--remote'],
78 | {
79 | 'default': self.C.get('myRemote'),
80 | 'help': 'the remote to push the branch to. Default is %s.' % self.C.get('myRemote'),
81 | 'metavar': 'remote'
82 | }
83 | ),
84 | (
85 | ['-f', '--force-push'],
86 | {
87 | 'action': 'store_true',
88 | 'dest': 'forcepush',
89 | 'help': 'Force the push'
90 | }
91 | ),
92 | (
93 | ['names'],
94 | {
95 | 'default': None,
96 | 'help': 'name of the instances to rebase',
97 | 'metavar': 'names',
98 | 'nargs': '*'
99 | }
100 | )
101 | ]
102 |
103 | def run(self, args):
104 |
105 | names = args.names
106 | issues = args.issues
107 | versions = args.versions
108 |
109 | # If we don't have a version, we need an instance
110 | if not names and not versions:
111 | raise Exception('This is not a Moodle instance')
112 |
113 | # We don't have any names but some versions are set
114 | if not names:
115 | names = []
116 | for v in versions:
117 | names.append(self.Wp.generateInstanceName(v))
118 |
119 | # Getting instances
120 | Mlist = self.Wp.resolveMultiple(names)
121 |
122 | # Updating cache remotes
123 | logging.info('Updating cached repositories')
124 | self.Wp.updateCachedClones(verbose=False)
125 |
126 | # Loops over instances to rebase
127 | for M in Mlist:
128 | logging.info('Working on %s' % (M.get('identifier')))
129 | M.git().fetch(self.C.get('upstreamRemote'))
130 |
131 | # Test if currently in a detached branch
132 | if M.git().currentBranch() == 'HEAD':
133 | result = M.git().checkout(M.get('stablebranch'))
134 | # If we can't checkout the stable branch, that is probably because we are in an unmerged situation
135 | if not result:
136 | logging.warning('Error. The repository seem to be on a detached branch. Skipping.')
137 | continue
138 |
139 | # Stash
140 | stash = M.git().stash(untracked=True)
141 | if stash[0] != 0:
142 | logging.error('Error while trying to stash your changes. Skipping %s.' % M.get('identifier'))
143 | logging.debug(stash[2])
144 | continue
145 | elif not stash[1].startswith('No local changes'):
146 | logging.info('Stashed your local changes')
147 |
148 | # Looping over each issue to rebase
149 | for issue in issues:
150 | branch = M.generateBranchName(issue, suffix=args.suffix)
151 | if not M.git().hasBranch(branch):
152 | logging.warning('Could not find branch %s' % (branch))
153 | continue
154 |
155 | # Rebase
156 | logging.info('> Rebasing %s...' % (branch))
157 | base = '%s/%s' % (self.C.get('upstreamRemote'), M.get('stablebranch'))
158 | result = M.git().rebase(branch=branch, base=base)
159 | if result[0] != 0:
160 | logging.warning('Error while rebasing branch %s on top of %s' % (branch, base))
161 | if result[0] == 1 and result[2].strip() == '':
162 | logging.debug('There must be conflicts.')
163 | logging.info('Aborting... Please rebase manually.')
164 | M.git().rebase(abort=True)
165 | else:
166 | logging.debug(result[2])
167 | continue
168 |
169 | # Pushing branch
170 | if args.push:
171 | remote = args.remote
172 | logging.info('Pushing %s to %s' % (branch, remote))
173 | result = M.git().push(remote=remote, branch=branch, force=args.forcepush)
174 | if result[0] != 0:
175 | logging.warning('Error while pushing to remote')
176 | logging.debug(result[2])
177 | continue
178 |
179 | # Update the tracker
180 | if args.updatetracker:
181 | M.updateTrackerGitInfo(branch=branch)
182 |
183 | # Stash pop
184 | if not stash[1].startswith('No local changes'):
185 | pop = M.git().stash(command='pop')
186 | if pop[0] != 0:
187 | logging.error('An error occurred while unstashing your changes')
188 | logging.debug(pop[2])
189 | else:
190 | logging.info('Popped the stash')
191 |
192 | logging.info('')
193 |
194 | logging.info('Done.')
195 |
--------------------------------------------------------------------------------
/mdk/commands/init.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | import grp
27 | import re
28 | import pwd
29 | import subprocess
30 | import logging
31 |
32 | from ..command import Command
33 | from ..tools import question, get_current_user, mkdir
34 |
35 |
36 | class InitCommand(Command):
37 |
38 | _arguments = [
39 | (
40 | ['-f', '--force'],
41 | {
42 | 'action': 'store_true',
43 | 'help': 'Force the initialisation'
44 | }
45 | )
46 | ]
47 | _description = 'Initialise MDK'
48 |
49 | def resolve_directory(self, path, user):
50 | if path.startswith('~'):
51 | path = re.sub(r'^~', '~%s' % user, path)
52 | path = os.path.abspath(os.path.realpath(os.path.expanduser(path)))
53 | return path
54 |
55 | def run(self, args):
56 |
57 | # Check what user we want to initialise for.
58 | while True:
59 | username = question('What user are you initialising MDK for?', get_current_user())
60 | try:
61 | user = pwd.getpwnam(username)
62 | except:
63 | logging.warning('Error while getting information for user %s' % (username))
64 | continue
65 |
66 | try:
67 | usergroup = grp.getgrgid(user.pw_gid)
68 | except:
69 | logging.warning('Error while getting the group of user %s' % (username))
70 | continue
71 |
72 | break
73 |
74 | # Default directories.
75 | userdir = self.resolve_directory('~/.moodle-sdk', username)
76 | scriptdir = os.path.dirname(os.path.realpath(__file__))
77 |
78 | # Create the main MDK folder.
79 | if not os.path.isdir(userdir):
80 | logging.info('Creating directory %s.' % userdir)
81 | mkdir(userdir, 0755)
82 | os.chown(userdir, user.pw_uid, usergroup.gr_gid)
83 |
84 | # Checking if the config file exists.
85 | userconfigfile = os.path.join(userdir, 'config.json')
86 | if os.path.isfile(userconfigfile):
87 | logging.info('Config file %s already in place.' % userconfigfile)
88 | if not args.force:
89 | raise Exception('Aborting. Use --force to continue.')
90 |
91 | elif not os.path.isfile(userconfigfile):
92 | logging.info('Creating user config file in %s.' % userconfigfile)
93 | open(userconfigfile, 'w')
94 | os.chown(userconfigfile, user.pw_uid, usergroup.gr_gid)
95 |
96 | # Loading the configuration.
97 | from ..config import Conf as Config
98 | C = Config(userfile=userconfigfile)
99 |
100 | # Asks the user what needs to be asked.
101 | while True:
102 | www = question('What is the DocumentRoot of your virtual host?', C.get('dirs.www'))
103 | www = self.resolve_directory(www, username)
104 | try:
105 | if not os.path.isdir(www):
106 | mkdir(www, 0775)
107 | os.chown(www, user.pw_uid, usergroup.gr_gid)
108 | except:
109 | logging.error('Error while creating directory %s' % www)
110 | continue
111 |
112 | if not os.access(www, os.W_OK):
113 | logging.error('You need to have permission to write to that directory.\nPlease fix or use another directory.')
114 | continue
115 |
116 | C.set('dirs.www', www)
117 | break
118 |
119 | while True:
120 | storage = question('Where do you want to store your Moodle instances?', C.get('dirs.storage'))
121 | storage = self.resolve_directory(storage, username)
122 | try:
123 | if not os.path.isdir(storage):
124 | if storage != www:
125 | mkdir(storage, 0775)
126 | os.chown(storage, user.pw_uid, usergroup.gr_gid)
127 | else:
128 | logging.error('Error! dirs.www and dirs.storage must be different!')
129 | continue
130 | except:
131 | logging.error('Error while creating directory %s' % storage)
132 | continue
133 |
134 | if not os.access(storage, os.W_OK):
135 | logging.error('You need to have permission to write to that directory.\nPlease fix or use another directory.')
136 | continue
137 |
138 | C.set('dirs.storage', storage)
139 | break
140 |
141 | # The default configuration file should point to the right directory for dirs.mdk,
142 | # we will just ensure that it exists.
143 | mdkdir = C.get('dirs.mdk')
144 | mdkdir = self.resolve_directory(mdkdir, username)
145 | if not os.path.isdir(mdkdir):
146 | try:
147 | logging.info('Creating MDK directory %s' % mdkdir)
148 | mkdir(mdkdir, 0775)
149 | os.chown(mdkdir, user.pw_uid, usergroup.gr_gid)
150 | except:
151 | logging.error('Error while creating %s, please fix manually.' % mdkdir)
152 |
153 | # Git repository.
154 | github = question('What is your Github username? (Leave blank if not using Github)')
155 | if github != None:
156 | C.set('remotes.mine', C.get('remotes.mine').replace('YourGitHub', github))
157 | C.set('repositoryUrl', C.get('repositoryUrl').replace('YourGitHub', github))
158 | C.set('diffUrlTemplate', C.get('diffUrlTemplate').replace('YourGitHub', github))
159 | C.set('myRemote', 'github')
160 | C.set('upstreamRemote', 'origin')
161 | else:
162 | C.set('remotes.mine', question('What is your remote?', C.get('remotes.mine')))
163 | C.set('myRemote', question('What to call your remote?', C.get('myRemote')))
164 | C.set('upstreamRemote', question('What to call the upsream remote (official Moodle remote)?', C.get('upstreamRemote')))
165 |
166 | # Database settings.
167 | C.set('db.mysqli.user', question('What is your MySQL user?', C.get('db.mysqli.user')))
168 | C.set('db.mysqli.passwd', question('What is your MySQL password?', 'root', password=True))
169 | C.set('db.pgsql.user', question('What is your PostgreSQL user?', C.get('db.pgsql.user')))
170 | C.set('db.pgsql.passwd', question('What is your PostgreSQL password?', 'root', password=True))
171 |
172 | print ''
173 | print 'MDK has been initialised with minimal configuration.'
174 | print 'For more settings, edit your config file: %s.' % userconfigfile
175 | print 'Use %s as documentation.' % os.path.join(scriptdir, 'config-dist.json')
176 | print ''
177 | print 'Type the following command to create your first instance:'
178 | print ' mdk create'
179 | print '(This will take some time, but don\'t worry, that\'s because the cache is still empty)'
180 | print ''
181 | print '/!\ Please logout/login before to avoid permission issues: sudo su `whoami`'
182 |
--------------------------------------------------------------------------------
/mdk/tools.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2012 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import sys
26 | import os
27 | import signal
28 | import subprocess
29 | import shlex
30 | import re
31 | import threading
32 | import getpass
33 | import logging
34 | import hashlib
35 | import tempfile
36 | from .config import Conf
37 |
38 | C = Conf()
39 |
40 | def yesOrNo(q):
41 | while True:
42 | i = raw_input('%s (y/n) ' % (q)).strip().lower()
43 | if i == 'y':
44 | return True
45 | elif i == 'n':
46 | return False
47 |
48 |
49 | def question(q, default=None, options=None, password=False):
50 | """Asks the user a question, and return the answer"""
51 | text = q
52 | if default != None:
53 | text = text + ' [%s]' % str(default)
54 |
55 | if password:
56 | i = getpass.getpass('%s\n ' % text)
57 | else:
58 | i = raw_input('%s\n ' % text)
59 |
60 | if i.strip() == '':
61 | return default
62 | else:
63 | if options != None and i not in options:
64 | return question(q, default, options)
65 | return i
66 |
67 |
68 | def chmodRecursive(path, chmod):
69 | os.chmod(path, chmod)
70 | for (dirpath, dirnames, filenames) in os.walk(path):
71 | for d in dirnames:
72 | dir = os.path.join(dirpath, d)
73 | os.chmod(dir, chmod)
74 | for f in filenames:
75 | file = os.path.join(dirpath, f)
76 | os.chmod(file, chmod)
77 |
78 |
79 | def getMDLFromCommitMessage(message):
80 | """Return the MDL-12345 number from a commit message"""
81 | mdl = None
82 | match = re.match(r'MDL(-|_)([0-9]+)', message, re.I)
83 | if match:
84 | mdl = 'MDL-%s' % (match.group(2))
85 | return mdl
86 |
87 |
88 | def get_current_user():
89 | """Attempt to get the currently logged in user"""
90 | username = 'root'
91 | try:
92 | username = os.getlogin()
93 | except OSError:
94 | import getpass
95 | try:
96 | username = getpass.getuser()
97 | except:
98 | pass
99 | return username
100 |
101 |
102 | def launchEditor(filepath=None, suffix='.tmp'):
103 | """Launchs up an editor
104 |
105 | If filepath is passed, the content of the file is used to populate the editor.
106 |
107 | This returns the path to the saved file.
108 | """
109 | editor = resolveEditor()
110 | if not editor:
111 | raise Exception('Could not locate the editor')
112 | with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmpfile:
113 | with open(filepath, 'r') as f:
114 | tmpfile.write(f.read())
115 | tmpfile.flush()
116 | subprocess.call([editor, tmpfile.name])
117 | return tmpfile.name
118 |
119 |
120 | def getText(suffix='.md', initialText=None):
121 | """Gets text from the user using an Editor
122 |
123 | This is a shortcut to using launchEditor as it returns text rather
124 | than the file in which the text entered is stored.
125 |
126 | When the returned value is empty, the user is asked is they want to resume.
127 | """
128 | with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmpfile:
129 | if initialText:
130 | tmpfile.write(initialText)
131 | tmpfile.flush()
132 | while True:
133 | editorFile = launchEditor(suffix=suffix, filepath=tmpfile.name)
134 | text = None
135 | with open(editorFile, 'r') as f:
136 | text = f.read()
137 |
138 | if len(text) <= 0:
139 | if not yesOrNo('No content detected. Would you like to resume editing?'):
140 | return ''
141 | else:
142 | return text
143 |
144 | def md5file(filepath):
145 | """Return the md5 sum of a file
146 | This is terribly memory inefficient!"""
147 | return hashlib.md5(open(filepath).read()).hexdigest()
148 |
149 |
150 | def mkdir(path, perms=0755):
151 | """Creates a directory ignoring the OS umask"""
152 | oldumask = os.umask(0000)
153 | os.mkdir(path, perms)
154 | os.umask(oldumask)
155 |
156 |
157 | def parseBranch(branch):
158 | pattern = re.compile(C.get('wording.branchRegex'), flags=re.I)
159 | result = pattern.search(branch)
160 | if not result:
161 | return False
162 |
163 | parsed = {
164 | 'issue': result.group(pattern.groupindex['issue']),
165 | 'version': result.group(pattern.groupindex['version'])
166 | }
167 | try:
168 | parsed['suffix'] = result.group(pattern.groupindex['suffix'])
169 | except:
170 | parsed['suffix'] = None
171 | return parsed
172 |
173 |
174 | def process(cmd, cwd=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE):
175 | if type(cmd) != list:
176 | cmd = shlex.split(str(cmd))
177 | logging.debug(' '.join(cmd))
178 | try:
179 | proc = subprocess.Popen(cmd, cwd=cwd, stdout=stdout, stderr=stderr)
180 | (out, err) = proc.communicate()
181 | except KeyboardInterrupt as e:
182 | proc.kill()
183 | raise e
184 | return (proc.returncode, out, err)
185 |
186 |
187 | def resolveEditor():
188 | """Try to resolve the editor that the user would want to use.
189 | This does actually checks if it is executable"""
190 | editor = C.get('editor')
191 | if not editor:
192 | editor = os.environ.get('EDITOR')
193 | if not editor:
194 | editor = os.environ.get('VISUAL')
195 | if not editor and os.path.isfile('/usr/bin/editor'):
196 | editor = '/usr/bin/editor'
197 | return editor
198 |
199 |
200 | def downloadProcessHook(count, size, total):
201 | """Hook to report the downloading a file using urllib.urlretrieve"""
202 | if count <= 0:
203 | return
204 | downloaded = int((count * size) / (1024))
205 | total = int(total / (1024)) if total != 0 else '?'
206 | if downloaded > total:
207 | downloaded = total
208 | sys.stderr.write("\r %sKB / %sKB" % (downloaded, total))
209 | sys.stderr.flush()
210 |
211 |
212 | def stableBranch(version):
213 | if version == 'master':
214 | return 'master'
215 | return 'MOODLE_%d_STABLE' % int(version)
216 |
217 |
218 | class ProcessInThread(threading.Thread):
219 | """Executes a process in a separate thread"""
220 |
221 | cmd = None
222 | cwd = None
223 | stdout = None
224 | stderr = None
225 | _kill = False
226 | _pid = None
227 |
228 | def __init__(self, cmd, cwd=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE):
229 | threading.Thread.__init__(self)
230 | if type(cmd) != 'list':
231 | cmd = shlex.split(str(cmd))
232 | self.cmd = cmd
233 | self.cwd = cwd
234 | self.stdout = stdout
235 | self.stderr = stderr
236 |
237 | def kill(self):
238 | os.kill(self._pid, signal.SIGKILL)
239 |
240 | def run(self):
241 | logging.debug(' '.join(self.cmd))
242 | proc = subprocess.Popen(self.cmd, cwd=self.cwd, stdout=self.stdout, stderr=self.stderr)
243 | self._pid = proc.pid
244 | while True:
245 | if proc.poll():
246 | break
247 |
248 | # Reading the output seems to prevent the process to hang.
249 | if self.stdout == subprocess.PIPE:
250 | proc.stdout.read(1)
251 |
--------------------------------------------------------------------------------
/mdk/backup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2012 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | import json
27 | import time
28 | import logging
29 | from distutils.dir_util import copy_tree
30 |
31 | from .tools import chmodRecursive, mkdir
32 | from .db import DB
33 | from .config import Conf
34 | from .workplace import Workplace
35 | from .exceptions import *
36 |
37 | C = Conf()
38 | jason = 'info.json'
39 | sqlfile = 'dump.sql'
40 |
41 |
42 | class BackupManager(object):
43 |
44 | def __init__(self):
45 | self.path = os.path.expanduser(os.path.join(C.get('dirs.moodle'), 'backup'))
46 | if not os.path.exists(self.path):
47 | mkdir(self.path, 0777)
48 |
49 | def create(self, M):
50 | """Creates a new backup of M"""
51 |
52 | if M.isInstalled() and M.get('dbtype') not in ('mysqli', 'mariadb'):
53 | raise BackupDBEngineNotSupported('Cannot backup database engine %s' % M.get('dbtype'))
54 |
55 | name = M.get('identifier')
56 | if name == None:
57 | raise Exception('Cannot backup instance without identifier!')
58 |
59 | now = int(time.time())
60 | backup_identifier = self.createIdentifier(name)
61 | Wp = Workplace()
62 |
63 | # Copy whole directory, shutil will create topath
64 | topath = os.path.join(self.path, backup_identifier)
65 | path = Wp.getPath(name)
66 | logging.info('Copying instance directory')
67 | copy_tree(path, topath, preserve_symlinks=1)
68 |
69 | # Dump the whole database
70 | if M.isInstalled():
71 | logging.info('Dumping database')
72 | dumpto = os.path.join(topath, sqlfile)
73 | fd = open(dumpto, 'w')
74 | M.dbo().selectdb(M.get('dbname'))
75 | M.dbo().dump(fd)
76 | else:
77 | logging.info('Instance not installed. Do not dump database.')
78 |
79 | # Create a JSON file containing all known information
80 | logging.info('Saving instance information')
81 | jsonto = os.path.join(topath, jason)
82 | info = M.info()
83 | info['backup_origin'] = path
84 | info['backup_identifier'] = backup_identifier
85 | info['backup_time'] = now
86 | json.dump(info, open(jsonto, 'w'), sort_keys=True, indent=4)
87 |
88 | return True
89 |
90 | def createIdentifier(self, name):
91 | """Creates an identifier"""
92 | for i in range(1, 100):
93 | identifier = '{0}_{1:0>2}'.format(name, i)
94 | if not self.exists(identifier):
95 | break
96 | identifier = None
97 | if not identifier:
98 | raise Exception('Could not generate a backup identifier! How many backup did you do?!')
99 | return identifier
100 |
101 | def exists(self, name):
102 | """Checks whether a backup exists under this name or not"""
103 | d = os.path.join(self.path, name)
104 | f = os.path.join(d, jason)
105 | if not os.path.isdir(d):
106 | return False
107 | return os.path.isfile(f)
108 |
109 | def get(self, name):
110 | return Backup(self.getPath(name))
111 |
112 | def getPath(self, name):
113 | return os.path.join(self.path, name)
114 |
115 | def list(self):
116 | """Returns a list of backups with their information"""
117 | dirs = os.listdir(self.path)
118 | backups = {}
119 | for name in dirs:
120 | if name == '.' or name == '..': continue
121 | if not self.exists(name): continue
122 | try:
123 | backups[name] = Backup(self.getPath(name))
124 | except:
125 | # Must successfully retrieve information to be a valid backup
126 | continue
127 | return backups
128 |
129 |
130 | class Backup(object):
131 |
132 | def __init__(self, path):
133 | self.path = path
134 | self.jason = os.path.join(path, jason)
135 | self.sqlfile = os.path.join(path, sqlfile)
136 | if not os.path.isdir(path):
137 | raise Exception('Could not find backup in %s' % path)
138 | elif not os.path.isfile(self.jason):
139 | raise Exception('Backup information file unfound!')
140 | self.load()
141 |
142 | def get(self, name):
143 | """Returns a info on the backup"""
144 | try:
145 | return self.infos[name]
146 | except:
147 | return None
148 |
149 | def load(self):
150 | """Loads the backup information"""
151 | if not os.path.isfile(self.jason):
152 | raise Exception('Backup information file not found!')
153 | try:
154 | self.infos = json.load(open(self.jason, 'r'))
155 | except:
156 | raise Exception('Could not load information from JSON file')
157 |
158 | def restore(self, destination=None):
159 | """Restores the backup"""
160 |
161 | identifier = self.get('identifier')
162 | if not identifier:
163 | raise Exception('Identifier is invalid! Cannot proceed.')
164 |
165 | Wp = Workplace()
166 | if destination == None:
167 | destination = self.get('backup_origin')
168 | if not destination:
169 | raise Exception('Wrong path to perform the restore!')
170 |
171 | if os.path.isdir(destination):
172 | raise BackupDirectoryExistsException('Destination directory already exists!')
173 |
174 | # Restoring database
175 | if self.get('installed') and os.path.isfile(self.sqlfile):
176 | dbname = self.get('dbname')
177 | dbo = DB(self.get('dbtype'), C.get('db.%s' % self.get('dbtype')))
178 | if dbo.dbexists(dbname):
179 | raise BackupDBExistsException('Database already exists!')
180 |
181 | # Copy tree to destination
182 | try:
183 | logging.info('Restoring instance directory')
184 | copy_tree(self.path, destination, preserve_symlinks=1)
185 | M = Wp.get(identifier)
186 | chmodRecursive(Wp.getPath(identifier, 'data'), 0777)
187 | except Exception as e:
188 | raise Exception('Error while restoring directory\n%s\nto %s. Exception: %s' % (self.path, destination, e))
189 |
190 | # Restoring database
191 | if self.get('installed') and os.path.isfile(self.sqlfile):
192 | logging.info('Restoring database')
193 | content = ''
194 | f = open(self.sqlfile, 'r')
195 | for l in f:
196 | content += l
197 | queries = content.split(';\n')
198 | content = None
199 | logging.info("%d queries to execute" % (len(queries)))
200 |
201 | dbo.createdb(dbname)
202 | dbo.selectdb(dbname)
203 | done = 0
204 | for query in queries:
205 | if len(query.strip()) == 0: continue
206 | try:
207 | dbo.execute(query)
208 | except:
209 | logging.error('Query failed! You will have to fix this mually. %s', query)
210 | done += 1
211 | if done % 500 == 0:
212 | logging.debug("%d queries done" % done)
213 | logging.info('%d queries done' % done)
214 | dbo.close()
215 |
216 | # Restoring symbolic link
217 | linkDir = os.path.join(Wp.www, identifier)
218 | wwwDir = Wp.getPath(identifier, 'www')
219 | if os.path.islink(linkDir):
220 | os.remove(linkDir)
221 | if os.path.isfile(linkDir) or os.path.isdir(linkDir): # No elif!
222 | logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (wwwDir, linkDir))
223 | else:
224 | os.symlink(wwwDir, linkDir)
225 |
226 | return M
227 |
--------------------------------------------------------------------------------
/mdk/commands/create.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2013 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import re
26 | import logging
27 |
28 | from ..db import DB
29 | from ..command import Command
30 | from ..tools import yesOrNo
31 | from ..exceptions import CreateException, InstallException
32 |
33 |
34 | class CreateCommand(Command):
35 |
36 | _description = 'Creates new instances of Moodle'
37 |
38 | def __init__(self, *args, **kwargs):
39 | super(CreateCommand, self).__init__(*args, **kwargs)
40 | self._arguments = [
41 | (
42 | ['-i', '--install'],
43 | {
44 | 'action': 'store_true',
45 | 'dest': 'install',
46 | 'help': 'launch the installation script after creating the instance'
47 | }
48 | ),
49 | (
50 | ['-e', '--engine'],
51 | {
52 | 'action': 'store',
53 | 'choices': ['mariadb', 'mysqli', 'pgsql', 'sqlsrv'],
54 | 'default': self.C.get('defaultEngine'),
55 | 'help': 'database engine to install the instance on, use with --install',
56 | 'metavar': 'engine'
57 | }
58 | ),
59 | (
60 | ['-t', '--integration'],
61 | {
62 | 'action': 'store_true',
63 | 'help': 'create an instance from integration'
64 | }
65 | ),
66 | (
67 | ['-r', '--run'],
68 | {
69 | 'action': 'store',
70 | 'help': 'scripts to run after installation',
71 | 'metavar': 'run',
72 | 'nargs': '*'
73 | }
74 | ),
75 | (
76 | ['-n', '--identifier'],
77 | {
78 | 'action': 'store',
79 | 'default': None,
80 | 'help': 'use this identifier instead of generating one. The flag --suffix will be used. ' +
81 | 'Do not use when creating multiple versions at once',
82 | 'metavar': 'name',
83 | }
84 | ),
85 | (
86 | ['-s', '--suffix'],
87 | {
88 | 'action': 'store',
89 | 'default': [None],
90 | 'help': 'suffixes for the instance name',
91 | 'metavar': 'suffix',
92 | 'nargs': '*'
93 | }
94 | ),
95 | (
96 | ['-v', '--version'],
97 | {
98 | 'choices': [str(x) for x in range(13, int(self.C.get('masterBranch')))] + ['master'],
99 | 'default': ['master'],
100 | 'help': 'version of Moodle',
101 | 'metavar': 'version',
102 | 'nargs': '*'
103 | }
104 | ),
105 | ]
106 |
107 | def run(self, args):
108 |
109 | engine = args.engine
110 | versions = args.version
111 | suffixes = args.suffix
112 | install = args.install
113 |
114 | # Throw an error when --engine is used without --install. The code is currently commented out
115 | # because as --engine has a default value, it will always be set, and so it becomes impossible
116 | # to create an instance without installing it. I cannot think about a clean fix yet. Removing
117 | # the default value will cause --help not to output the default as it should... Let's put more
118 | # thoughts into this and perhaps use argument groups.
119 | # if engine and not install:
120 | # self.argumentError('--engine can only be used with --install.')
121 |
122 | for version in versions:
123 | for suffix in suffixes:
124 | arguments = {
125 | 'version': version,
126 | 'suffix': suffix,
127 | 'engine': engine,
128 | 'integration': args.integration,
129 | 'identifier': args.identifier,
130 | 'install': install,
131 | 'run': args.run
132 | }
133 | self.do(arguments)
134 | logging.info('')
135 |
136 | logging.info('Process complete!')
137 |
138 | def do(self, args):
139 | """Proceeds to the creation of an instance"""
140 |
141 | # TODO Remove these ugly lines, but I'm lazy to rewrite the variables in this method...
142 | class Bunch:
143 | __init__ = lambda self, **kw: setattr(self, '__dict__', kw)
144 | args = Bunch(**args)
145 |
146 | engine = args.engine
147 | version = args.version
148 | name = self.Wp.generateInstanceName(version, integration=args.integration, suffix=args.suffix, identifier=args.identifier)
149 |
150 | # Wording version
151 | versionNice = version
152 | if version == 'master':
153 | versionNice = self.C.get('wording.master')
154 |
155 | # Generating names
156 | if args.integration:
157 | fullname = self.C.get('wording.integration') + ' ' + versionNice + ' ' + self.C.get('wording.%s' % engine)
158 | else:
159 | fullname = self.C.get('wording.stable') + ' ' + versionNice + ' ' + self.C.get('wording.%s' % engine)
160 |
161 | # Append the suffix
162 | if args.suffix:
163 | fullname += ' ' + args.suffix.replace('-', ' ').replace('_', ' ').title()
164 |
165 | # Create the instance
166 | logging.info('Creating instance %s...' % name)
167 | kwargs = {
168 | 'name': name,
169 | 'version': version,
170 | 'integration': args.integration
171 | }
172 | try:
173 | M = self.Wp.create(**kwargs)
174 | except CreateException as e:
175 | logging.error('Error creating %s:\n %s' % (name, e))
176 | return False
177 | except Exception as e:
178 | logging.exception('Error creating %s:\n %s' % (name, e))
179 | return False
180 |
181 | # Run the install script
182 | if args.install:
183 |
184 | # Checking database
185 | dbname = re.sub(r'[^a-zA-Z0-9]', '', name).lower()
186 | prefixDbname = self.C.get('db.namePrefix')
187 | if prefixDbname:
188 | dbname = prefixDbname + dbname
189 | dbname = dbname[:28]
190 | db = DB(engine, self.C.get('db.%s' % engine))
191 | dropDb = False
192 | if db.dbexists(dbname):
193 | logging.info('Database already exists (%s)' % dbname)
194 | dropDb = yesOrNo('Do you want to remove it?')
195 |
196 | # Install
197 | kwargs = {
198 | 'engine': engine,
199 | 'dbname': dbname,
200 | 'dropDb': dropDb,
201 | 'fullname': fullname,
202 | 'dataDir': self.Wp.getPath(name, 'data'),
203 | 'wwwroot': self.Wp.getUrl(name)
204 | }
205 | try:
206 | M.install(**kwargs)
207 | except InstallException as e:
208 | logging.warning('Error while installing %s:\n %s' % (name, e))
209 | return False
210 | except Exception as e:
211 | logging.exception('Error while installing %s:\n %s' % (name, e))
212 | return False
213 |
214 | # Running scripts
215 | if M.isInstalled() and type(args.run) == list:
216 | for script in args.run:
217 | logging.info('Running script \'%s\'' % (script))
218 | try:
219 | M.runScript(script)
220 | except Exception as e:
221 | logging.warning('Error while running the script \'%s\':\ %s' % (script, e))
222 |
--------------------------------------------------------------------------------
/mdk/fetch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import os
26 | import jira
27 | import logging
28 |
29 |
30 | class Fetch(object):
31 | """Holds the logic and processing to fetch a remote and following actions into an instance"""
32 |
33 | _M = None
34 | _ref = None
35 | _repo = None
36 |
37 | _canCreateBranch = True
38 | _hasstashed = False
39 |
40 | def __init__(self, M, repo=None, ref=None):
41 | self._M = M
42 | self._repo = repo
43 | self._ref = ref
44 |
45 | def checkout(self):
46 | """Fetch and checkout the fetched branch"""
47 | self.fetch()
48 | logging.info('Checking out branch as FETCH_HEAD')
49 | if not self.M.git().checkout('FETCH_HEAD'):
50 | raise FetchException('Could not checkout FETCH_HEAD')
51 |
52 | def fetch(self):
53 | """Perform the fetch"""
54 | if not self.repo:
55 | raise FetchException('The repository to fetch from is unknown')
56 | elif not self.ref:
57 | raise FetchException('The ref to fetch is unknown')
58 |
59 | git = self.M.git()
60 | logging.info('Fetching %s from %s' % (self.ref, self.repo))
61 | result = git.fetch(remote=self.repo, ref=self.ref)
62 | if not result:
63 | raise FetchException('Error while fetching %s from %s' % (self.ref, self.repo))
64 |
65 | def _merge(self):
66 | """Protected method to merge FETCH_HEAD into the current branch"""
67 | logging.info('Merging into current branch')
68 | if not self.M.git().merge('FETCH_HEAD'):
69 | raise FetchException('Merge failed, resolve the conflicts and commit')
70 |
71 | def pull(self, into=None, track=None):
72 | """Fetch and merge the fetched branch into a branch passed as param"""
73 | self._stash()
74 |
75 | try:
76 | self.fetch()
77 | git = self.M.git()
78 |
79 | if into:
80 | logging.info('Switching to branch %s' % (into))
81 |
82 | if not git.hasBranch(into):
83 | if self.canCreateBranch:
84 | if not git.createBranch(into, track=track):
85 | raise FetchException('Could not create the branch %s' % (into))
86 | else:
87 | raise FetchException('Branch %s does not exist and create branch is forbidden' % (into))
88 |
89 | if not git.checkout(into):
90 | raise FetchException('Could not checkout branch %s' % (into))
91 |
92 | self._merge()
93 |
94 | except FetchException as e:
95 | if self._hasstashed:
96 | logging.warning('An error occurred. Some files may have been left in your stash.')
97 | raise e
98 |
99 | self._unstash()
100 |
101 | def setRef(self, ref):
102 | """Set the reference to fetch"""
103 | self._ref = ref
104 |
105 | def setRepo(self, repo):
106 | """Set the repository to fetch from"""
107 | self._repo = repo
108 |
109 | def _stash(self):
110 | """Protected method to stash"""
111 | stash = self.M.git().stash(untracked=True)
112 | if stash[0] != 0:
113 | raise FetchException('Error while trying to stash your changes')
114 | elif not stash[1].startswith('No local changes'):
115 | logging.info('Stashed your local changes')
116 | self._hasstashed = True
117 |
118 | def _unstash(self):
119 | """Protected method to unstash"""
120 | if self._hasstashed:
121 | pop = self.M.git().stash(command='pop')
122 | if pop[0] != 0:
123 | logging.error('An error occurred while unstashing your changes')
124 | else:
125 | logging.info('Popped the stash')
126 | self._hasstashed = False
127 |
128 | @property
129 | def canCreateBranch(self):
130 | return self._canCreateBranch
131 |
132 | @property
133 | def into(self):
134 | return self._into
135 |
136 | @property
137 | def M(self):
138 | return self._M
139 |
140 | @property
141 | def ref(self):
142 | return self._ref
143 |
144 | @property
145 | def repo(self):
146 | return self._repo
147 |
148 |
149 | class FetchTracker(Fetch):
150 | """Pretty dodgy implementation of Fetch to work with the tracker.
151 |
152 | If a list of patches is set, we override the git methods to fetch from a remote
153 | to use the patches instead. I am not super convinced by this design, but at
154 | least the logic to fetch/pull/merge is more or less self contained.
155 | """
156 |
157 | _J = None
158 | _cache = None
159 | _patches = None
160 |
161 | def __init__(self, *args, **kwargs):
162 | super(FetchTracker, self).__init__(*args, **kwargs)
163 | self._J = jira.Jira()
164 | self._cache = {}
165 |
166 | def checkout(self):
167 | if not self.patches:
168 | return super(FetchTracker, self).checkout()
169 |
170 | self.fetch()
171 |
172 | def fetch(self):
173 | if not self.patches:
174 | return super(FetchTracker, self).fetch()
175 |
176 | for patch in self.patches:
177 | j = 0
178 | dest = None
179 | while True:
180 | downloadedTo = patch.get('filename') + (('.' + str(j)) if j > 0 else '')
181 | dest = os.path.join(self.M.get('path'), downloadedTo)
182 | j += 1
183 | if not os.path.isfile(dest):
184 | patch['downloadedTo'] = downloadedTo
185 | break
186 |
187 | logging.info('Downloading patch as %s' % (patch.get('downloadedTo')))
188 | if not dest or not self.J.download(patch.get('url'), dest):
189 | raise FetchTrackerException('Failed to download the patch to %s' % (dest))
190 |
191 |
192 | def _merge(self):
193 | if not self.patches:
194 | return super(FetchTracker, self)._merge()
195 |
196 | patchList = [patch.get('downloadedTo') for patch in self.patches]
197 | git = self.M.git()
198 | if not git.apply(patchList):
199 | raise FetchTrackerException('Could not apply the patch(es), please apply manually')
200 | else:
201 | for f in patchList:
202 | os.remove(f)
203 | logging.info('Patches applied successfully')
204 |
205 | def getPullInfo(self, mdl):
206 | """Return the pull information
207 |
208 | This implements its own local cache because we could potentially
209 | call it multiple times during the same request. This is bad though.
210 | """
211 | if not self._cache.has_key(mdl):
212 | issueInfo = self.J.getPullInfo(mdl)
213 | self._cache[mdl] = issueInfo
214 | return self._cache[mdl]
215 |
216 | def setFromTracker(self, mdl, branch):
217 | """Sets the repo and ref according to the tracker information"""
218 | issueInfo = self.getPullInfo(mdl)
219 |
220 | repo = issueInfo.get('repo', None)
221 | if not repo:
222 | raise FetchTrackerRepoException('Missing information about the repository to pull from on %s' % (mdl))
223 |
224 | ref = issueInfo.get('branches').get(str(branch), None)
225 | if not ref:
226 | raise FetchTrackerBranchException('Could not find branch info on %s' % (str(branch), mdl))
227 |
228 | self.setRepo(repo)
229 | self.setRef(ref.get('branch'))
230 |
231 | def usePatches(self, patches):
232 | """List of patches (returned by jira.Jira.getAttachments) to work with instead of the standard repo and ref"""
233 | self._patches = patches
234 |
235 | @property
236 | def J(self):
237 | return self._J
238 |
239 | @property
240 | def patches(self):
241 | return self._patches
242 |
243 |
244 | class FetchException(Exception):
245 | pass
246 |
247 |
248 | class FetchTrackerException(FetchException):
249 | pass
250 |
251 |
252 | class FetchTrackerBranchException(FetchTrackerException):
253 | pass
254 |
255 |
256 | class FetchTrackerRepoException(FetchTrackerException):
257 | pass
258 |
--------------------------------------------------------------------------------
/mdk/commands/js.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Moodle Development Kit
6 |
7 | Copyright (c) 2014 Frédéric Massart - FMCorz.net
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | This program is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU General Public License for more details.
18 |
19 | You should have received a copy of the GNU General Public License
20 | along with this program. If not, see .
21 |
22 | http://github.com/FMCorz/mdk
23 | """
24 |
25 | import logging
26 | import os
27 | import time
28 | import datetime
29 | import watchdog.events
30 | import watchdog.observers
31 | from ..command import Command
32 | from .. import js, plugins
33 |
34 |
35 | class JsCommand(Command):
36 |
37 | _arguments = [
38 | (
39 | ['mode'],
40 | {
41 | 'metavar': 'mode',
42 | 'help': 'the type of action to perform',
43 | 'sub-commands':
44 | {
45 | 'shift': (
46 | {
47 | 'help': 'keen to use shifter?'
48 | },
49 | [
50 | (
51 | ['-p', '--plugin'],
52 | {
53 | 'action': 'store',
54 | 'dest': 'plugin',
55 | 'default': None,
56 | 'help': 'the name of the plugin or subsystem to target. If not passed, we do our best to guess from the current path.'
57 | }
58 | ),
59 | (
60 | ['-m', '--module'],
61 | {
62 | 'action': 'store',
63 | 'dest': 'module',
64 | 'default': None,
65 | 'help': 'the name of the module in the plugin or subsystem. If omitted all the modules will be shifted, except we are in a module.'
66 | }
67 | ),
68 | (
69 | ['-w', '--watch'],
70 | {
71 | 'action': 'store_true',
72 | 'dest': 'watch',
73 | 'help': 'watch for changes to re-shift'
74 | }
75 | ),
76 | (
77 | ['names'],
78 | {
79 | 'default': None,
80 | 'help': 'name of the instances',
81 | 'metavar': 'names',
82 | 'nargs': '*'
83 | }
84 | )
85 | ]
86 | ),
87 | 'doc': (
88 | {
89 | 'help': 'keen to generate documentation?'
90 | },
91 | [
92 | (
93 | ['names'],
94 | {
95 | 'default': None,
96 | 'help': 'name of the instances',
97 | 'metavar': 'names',
98 | 'nargs': '*'
99 | }
100 | )
101 | ]
102 | )
103 | }
104 | }
105 | )
106 | ]
107 | _description = 'Wrapper for JS functions'
108 |
109 | def run(self, args):
110 | if args.mode == 'shift':
111 | self.shift(args)
112 | elif args.mode == 'doc':
113 | self.document(args)
114 |
115 |
116 | def shift(self, args):
117 | """The shift mode"""
118 |
119 | Mlist = self.Wp.resolveMultiple(args.names)
120 | if len(Mlist) < 1:
121 | raise Exception('No instances to work on. Exiting...')
122 |
123 | cwd = os.path.realpath(os.path.abspath(os.getcwd()))
124 | mpath = Mlist[0].get('path')
125 | relpath = cwd.replace(mpath, '').strip('/')
126 |
127 | # TODO Put that logic somewhere else because it is going to be re-used, I'm sure.
128 | if not args.plugin:
129 | (subsystemOrPlugin, pluginName) = plugins.PluginManager.getSubsystemOrPluginFromPath(cwd, Mlist[0])
130 | if subsystemOrPlugin:
131 | args.plugin = subsystemOrPlugin + ('_' + pluginName) if pluginName else ''
132 | logging.info("I guessed the plugin/subsystem to work on as '%s'" % (args.plugin))
133 | else:
134 | self.argumentError('The argument --plugin is required, I could not guess it.')
135 |
136 | if not args.module:
137 | candidate = relpath
138 | module = None
139 | while '/yui/src' in candidate:
140 | (head, tail) = os.path.split(candidate)
141 | if head.endswith('/yui/src'):
142 | module = tail
143 | break
144 | candidate = head
145 |
146 | if module:
147 | args.module = module
148 | logging.info("I guessed the JS module to work on as '%s'" % (args.module))
149 |
150 | for M in Mlist:
151 | if len(Mlist) > 1:
152 | logging.info('Let\'s shift everything you wanted on \'%s\'' % (M.get('identifier')))
153 |
154 | processor = js.Js(M)
155 | processor.shift(subsystemOrPlugin=args.plugin, module=args.module)
156 |
157 | if args.watch:
158 | observer = watchdog.observers.Observer()
159 |
160 | for M in Mlist:
161 | processor = js.Js(M)
162 | processorArgs = {'subsystemOrPlugin': args.plugin, 'module': args.module}
163 | handler = JsShiftWatcher(M, processor, processorArgs)
164 | observer.schedule(handler, processor.getYUISrcPath(**processorArgs), recursive=True)
165 | logging.info('Watchdog set up on %s, waiting for changes...' % (M.get('identifier')))
166 |
167 | observer.start()
168 |
169 | try:
170 | while True:
171 | time.sleep(1)
172 | except KeyboardInterrupt:
173 | observer.stop()
174 | finally:
175 | observer.join()
176 |
177 | def document(self, args):
178 | """The docmentation mode"""
179 |
180 | Mlist = self.Wp.resolveMultiple(args.names)
181 | if len(Mlist) < 1:
182 | raise Exception('No instances to work on. Exiting...')
183 |
184 | for M in Mlist:
185 | logging.info('Documenting everything you wanted on \'%s\'. This may take a while...', M.get('identifier'))
186 | outdir = self.Wp.getExtraDir(M.get('identifier'), 'jsdoc')
187 | outurl = self.Wp.getUrl(M.get('identifier'), extra='jsdoc')
188 | processor = js.Js(M)
189 | processor.document(outdir)
190 | logging.info('Documentation available at:\n %s\n %s', outdir, outurl)
191 |
192 |
193 | class JsShiftWatcher(watchdog.events.FileSystemEventHandler):
194 |
195 | _processor = None
196 | _args = None
197 | _ext = ['.js', '.json']
198 | _M = None
199 |
200 | def __init__(self, M, processor, args):
201 | super(self.__class__, self).__init__()
202 | self._M = M
203 | self._processor = processor
204 | self._args = args
205 |
206 | def on_modified(self, event):
207 | if event.is_directory:
208 | return
209 | elif not os.path.splitext(event.src_path)[1] in self._ext:
210 | return
211 | self.process(event)
212 |
213 | def on_moved(self, event):
214 | if not os.path.splitext(event.dest_path)[1] in self._ext:
215 | return
216 | self.process(event)
217 |
218 | def process(self, event):
219 | logging.info('[%s] (%s) Changes detected!' % (self._M.get('identifier'), datetime.datetime.now().strftime('%H:%M:%S')))
220 |
221 | try:
222 | self._processor.shift(**self._args)
223 | except js.ShifterCompileFailed:
224 | logging.error(' /!\ Error: Compile failed!')
225 |
--------------------------------------------------------------------------------