├── 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 ""; 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 | --------------------------------------------------------------------------------