├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── extra ├── apache.conf ├── bash_completion ├── fish_completion.fish ├── goto_instance ├── goto_instance.bash_completion ├── nginx-docker.conf └── welcome.php ├── mdk.py ├── mdk ├── __init__.py ├── __main__.py ├── backup.py ├── ci.py ├── command.py ├── commands │ ├── __init__.py │ ├── alias.py │ ├── backport.py │ ├── backup.py │ ├── behat.py │ ├── config.py │ ├── create.py │ ├── cron.py │ ├── css.py │ ├── doctor.py │ ├── fix.py │ ├── info.py │ ├── init.py │ ├── install.py │ ├── js.py │ ├── php.py │ ├── phpunit.py │ ├── plugin.py │ ├── precheck.py │ ├── pull.py │ ├── purge.py │ ├── push.py │ ├── rebase.py │ ├── remove.py │ ├── run.py │ ├── tracker.py │ ├── uninstall.py │ ├── update.py │ └── upgrade.py ├── config-dist.json ├── config.py ├── container.py ├── css.py ├── db.py ├── exceptions.py ├── fetch.py ├── git.py ├── jira.py ├── js.py ├── moodle.py ├── phpunit.py ├── plugins.py ├── scripts.py ├── scripts │ ├── README.rst │ ├── dev.php │ ├── enrol.php │ ├── external_functions.php │ ├── jsconfig.php │ ├── less.sh │ ├── makecourse.sh │ ├── mincron.php │ ├── mindev.php │ ├── setup.sh │ ├── setupsecurity.sh │ ├── tokens.php │ ├── undev.php │ ├── users.php │ ├── version.php │ └── webservices.php ├── tools.py ├── version.py └── workplace.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config.json 3 | *.pyc 4 | .idea 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "eeyore.yapf" 4 | }, 5 | "files.associations": { 6 | "**/*.json": "jsonc" 7 | } 8 | } -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include mdk/config-dist.json 4 | include mdk/scripts/* 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /extra/nginx-docker.conf: -------------------------------------------------------------------------------- 1 | # Does this even work? 2 | 3 | server { 4 | listen 80; 5 | listen [::]:80; 6 | server_name ~^(?.+)\.mdk\.local$; 7 | 8 | location / { 9 | proxy_set_header Host $http_host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 13 | 14 | resolver 127.0.0.11 valid=30s; 15 | proxy_pass http://$subdomain/$uri; 16 | } 17 | } 18 | 19 | server { 20 | listen 80; 21 | listen [::]:80; 22 | server_name mdk.local; 23 | 24 | location ~ ^/([^/]+)/(.*)$ 25 | proxy_set_header Host $http_host; 26 | proxy_set_header X-Real-IP $remote_addr; 27 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 28 | proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 29 | 30 | resolver 127.0.0.11 valid=30s; 31 | proxy_pass http://$1/$2; 32 | } 33 | } -------------------------------------------------------------------------------- /extra/welcome.php: -------------------------------------------------------------------------------- 1 | . 19 | * 20 | * http://github.com/FMCorz/mdk 21 | */ 22 | 23 | echo "

Moodle Development Kit

"; 24 | echo "
    "; 25 | 26 | $path = getcwd(); 27 | $dirs = scandir($path); 28 | foreach ($dirs as $dir) { 29 | if ($dir == '.' || $dir == '..' || !is_dir($path . '/' . $dir)) { 30 | continue; 31 | } 32 | print "
  • $dir
  • "; 33 | } 34 | 35 | echo "
"; 36 | -------------------------------------------------------------------------------- /mdk.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 | -------------------------------------------------------------------------------- /mdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMCorz/mdk/6b3b91acf2da58897d8922f4e7fb65fe753f2e1c/mdk/__init__.py -------------------------------------------------------------------------------- /mdk/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | 25 | def main(): 26 | 27 | import sys 28 | import argparse 29 | import os 30 | import re 31 | import logging 32 | import base64 33 | from .command import CommandRunner 34 | from .commands import getCommand, commandsList 35 | from .config import Conf 36 | from .tools import process 37 | from .version import __version__ 38 | 39 | C = Conf() 40 | 41 | try: 42 | debuglevel = getattr(logging, C.get('debug').upper()) 43 | except AttributeError: 44 | debuglevel = logging.INFO 45 | 46 | # Set logging levels. 47 | logging.basicConfig(format='%(message)s', level=debuglevel) 48 | logging.getLogger('requests').setLevel(logging.WARNING) # Reset logging level of 'requests' module. 49 | logging.getLogger('keyring.backend').setLevel(logging.WARNING) 50 | 51 | availaliases = [str(x) for x in list(C.get('aliases').keys())] 52 | choices = sorted(commandsList + availaliases) 53 | 54 | parser = argparse.ArgumentParser(description='Moodle Development Kit', add_help=False) 55 | parser.add_argument('-h', '--help', action='store_true', help='show this help message and exit') 56 | parser.add_argument('-l', '--list', action='store_true', help='list the available commands') 57 | parser.add_argument('-v', '--version', action='store_true', help='display the current version') 58 | parser.add_argument('--debug', action='store_true', help="sets the debugging level to 'debug'") 59 | parser.add_argument( 60 | *['--%s' % base64.b64decode(f).decode() for f in ('aWNhbnRyZWFj', 'aWNhbnRyZWFk')], 61 | dest='asdf', 62 | action='store_true', 63 | help=argparse.SUPPRESS 64 | ) 65 | parser.add_argument('command', metavar='command', nargs='?', help='command to call', choices=choices) 66 | parser.add_argument('args', metavar='arguments', nargs=argparse.REMAINDER, help='arguments of the command') 67 | parsedargs = parser.parse_args() 68 | cmd = parsedargs.command 69 | args = parsedargs.args 70 | 71 | # Enable debugging verbosity. 72 | if parsedargs.debug: 73 | logging.getLogger().setLevel(logging.DEBUG) 74 | 75 | # What do we do? 76 | if parsedargs.help: 77 | parser.print_help() 78 | sys.exit(0) 79 | elif parsedargs.version: 80 | print('MDK version %s' % __version__) 81 | sys.exit(0) 82 | elif parsedargs.list: 83 | for c in sorted(commandsList): 84 | print('{0:<15} {1}'.format(c, getCommand(c)._description)) 85 | sys.exit(0) 86 | elif parsedargs.asdf: 87 | print(base64.b64decode('U29ycnkgRGF2ZSwgTURLIGNhbm5vdCBoZWxwIHlvdSB3aXRoIHRoYXQuLi4=').decode()) 88 | sys.exit(0) 89 | elif not cmd: 90 | parser.print_help() 91 | sys.exit(0) 92 | 93 | # Looking up for an alias 94 | alias = C.get('aliases.%s' % cmd) 95 | if alias != None: 96 | if alias.startswith('!'): 97 | cmd = alias[1:] 98 | i = 0 99 | # Replace $1, $2, ... with passed arguments 100 | for arg in args: 101 | i += 1 102 | cmd = cmd.replace('$%d' % i, arg) 103 | # Remove unknown $[0-9] 104 | cmd = re.sub(r'\$[0-9]', '', cmd) 105 | result = process(cmd, stdout=None, stderr=None) 106 | sys.exit(result[0]) 107 | else: 108 | cmd = alias.split(' ')[0] 109 | args = alias.split(' ')[1:] + args 110 | 111 | cls = getCommand(cmd) 112 | Cmd = cls(C) 113 | Runner = CommandRunner(Cmd) 114 | try: 115 | Runner.run(args, prog='%s %s' % (os.path.basename(sys.argv[0]), cmd)) 116 | except Exception as e: 117 | import traceback 118 | info = sys.exc_info() 119 | logging.error('%s: %s', e.__class__.__name__, e) 120 | logging.debug(''.join(traceback.format_tb(info[2]))) 121 | sys.exit(1) 122 | 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /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, 0o777) 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'), 0o777) 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/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(list(infos.items()) + list(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 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import argparse 25 | import sys 26 | 27 | 28 | class Command(object): 29 | """Represents a command""" 30 | 31 | _arguments = [ 32 | ( 33 | ['foo'], 34 | { 35 | 'help': 'I\'m an argument' 36 | }, 37 | ), 38 | ( 39 | ['-b', '--bar'], 40 | { 41 | 'action': 'store_true', 42 | 'help': 'I\'m a flag' 43 | }, 44 | ), 45 | ] 46 | _description = 'Undocumented command' 47 | 48 | __C = None 49 | __Wp = None 50 | 51 | def __init__(self, config): 52 | self.__C = config 53 | 54 | def argumentError(self, message): 55 | raise CommandArgumentError(message) 56 | 57 | @property 58 | def arguments(self): 59 | return self._arguments 60 | 61 | @property 62 | def C(self): 63 | return self.__C 64 | 65 | @property 66 | def description(self): 67 | return self._description 68 | 69 | def run(self, args): 70 | return True 71 | 72 | @property 73 | def Wp(self): 74 | if not self.__Wp: 75 | from .workplace import Workplace 76 | self.__Wp = Workplace() 77 | return self.__Wp 78 | 79 | 80 | class CommandArgumentError(Exception): 81 | """Exception when a command sends an argument error""" 82 | pass 83 | 84 | 85 | class CommandArgumentFormatter(argparse.HelpFormatter): 86 | """Custom argument formatter""" 87 | 88 | def _get_help_string(self, action): 89 | help = action.help 90 | if '%(default)' not in action.help: 91 | forbiddentypes = ['_StoreTrueAction', '_StoreFalseAction'] 92 | if action.__class__.__name__ not in forbiddentypes and action.default is not argparse.SUPPRESS: 93 | defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] 94 | if action.option_strings or action.nargs in defaulting_nargs: 95 | help += ' (default: %(default)s)' 96 | return help 97 | 98 | 99 | class CommandArgumentParser(argparse.ArgumentParser): 100 | """Custom argument parser""" 101 | 102 | def error(self, message): 103 | self.print_help(sys.stderr) 104 | self.exit(2, '\n%s: error: %s\n' % (self.prog, message)) 105 | 106 | 107 | class CommandRunner(object): 108 | """Executes a command""" 109 | 110 | def __init__(self, command): 111 | self._command = command 112 | 113 | @property 114 | def command(self): 115 | return self._command 116 | 117 | def run(self, sysargs=sys.argv, prog=None): 118 | parser = CommandArgumentParser(description=self.command.description, prog=prog, formatter_class=CommandArgumentFormatter) 119 | for argument in self.command.arguments: 120 | args = argument[0] 121 | kwargs = argument[1] 122 | if 'sub-commands' in kwargs: 123 | subs = kwargs['sub-commands'] 124 | del kwargs['sub-commands'] 125 | subparsers = parser.add_subparsers(**kwargs) 126 | for name, sub in list(subs.items()): 127 | subparser = subparsers.add_parser(name, **sub[0]) 128 | defaults = {args[0]: name} 129 | subparser.set_defaults(**defaults) 130 | for subargument in sub[1]: 131 | sargs = subargument[0] 132 | skwargs = subargument[1] 133 | if 'silent' in skwargs: 134 | del skwargs['silent'] 135 | skwargs['help'] = argparse.SUPPRESS 136 | subparser.add_argument(*sargs, **skwargs) 137 | else: 138 | if 'silent' in kwargs: 139 | del kwargs['silent'] 140 | kwargs['help'] = argparse.SUPPRESS 141 | parser.add_argument(*args, **kwargs) 142 | 143 | if hasattr(self.command, 'parse_args'): 144 | args = self.command.parse_args(parser, sysargs) 145 | else: 146 | args = parser.parse_args(sysargs) 147 | 148 | try: 149 | self.command.run(args) 150 | except CommandArgumentError as e: 151 | parser.error(str(e)) 152 | 153 | 154 | if __name__ == "__main__": 155 | CommandRunner(Command()).run() 156 | -------------------------------------------------------------------------------- /mdk/commands/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | 25 | def getCommand(cmd): 26 | """Lazy loading of a command class. Millseconds saved, hurray!""" 27 | cls = cmd.capitalize() + 'Command' 28 | return getattr(getattr(getattr(__import__('mdk.%s.%s' % ('commands', cmd)), 'commands'), cmd), cls) 29 | 30 | 31 | commandsList = [ 32 | 'alias', 33 | 'backport', 34 | 'backup', 35 | 'behat', 36 | 'config', 37 | 'create', 38 | 'cron', 39 | 'css', 40 | 'doctor', 41 | 'fix', 42 | 'info', 43 | 'init', 44 | 'install', 45 | 'js', 46 | 'php', 47 | 'phpunit', 48 | 'plugin', 49 | 'precheck', 50 | 'pull', 51 | 'purge', 52 | 'push', 53 | 'rebase', 54 | 'remove', 55 | 'run', 56 | 'tracker', 57 | 'uninstall', 58 | 'update', 59 | 'upgrade', 60 | ] 61 | -------------------------------------------------------------------------------- /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 list(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/backup.py: -------------------------------------------------------------------------------- 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 | import sys 23 | import time 24 | import logging 25 | from distutils.errors import DistutilsFileError 26 | from .. import backup 27 | from ..command import Command 28 | from ..exceptions import * 29 | 30 | 31 | class BackupCommand(Command): 32 | 33 | _arguments = [ 34 | ( 35 | ['-i', '--info'], 36 | { 37 | 'dest': 'info', 38 | 'help': 'lists all the information about a backup', 39 | 'metavar': 'backup' 40 | }, 41 | ), 42 | ( 43 | ['-l', '--list'], 44 | { 45 | 'action': 'store_true', 46 | 'dest': 'list', 47 | 'help': 'list the backups' 48 | }, 49 | ), 50 | ( 51 | ['-r', '--restore'], 52 | { 53 | 'dest': 'restore', 54 | 'help': 'restore a backup', 55 | 'metavar': 'backup' 56 | }, 57 | ), 58 | ( 59 | ['name'], 60 | { 61 | 'default': None, 62 | 'help': 'name of the instance', 63 | 'nargs': '?' 64 | }, 65 | ), 66 | ] 67 | 68 | _description = 'Backup a Moodle instance' 69 | 70 | def run(self, args): 71 | print('This command has been removed as it was poorly supported and unmaintained.') 72 | print('If you were using it please raise an issue to let us know.') 73 | print('https://github.com/FMCorz/mdk/issues') 74 | sys.exit(1) 75 | return 76 | 77 | name = args.name 78 | BackupManager = backup.BackupManager() 79 | 80 | # List the backups 81 | if args.list: 82 | backups = BackupManager.list() 83 | for key in sorted(backups.keys()): 84 | B = backups[key] 85 | backuptime = time.ctime(B.get('backup_time')) 86 | print('{0:<25}: {1:<30} {2}'.format(key, B.get('release'), backuptime)) 87 | 88 | # Displays backup information 89 | elif args.info: 90 | name = args.info 91 | 92 | # Resolve the backup 93 | if not name or not BackupManager.exists(name): 94 | raise Exception('This is not a valid backup') 95 | 96 | # Restore process 97 | B = BackupManager.get(name) 98 | infos = B.infos 99 | print('Displaying information about %s' % name) 100 | for key in sorted(infos.keys()): 101 | print('{0:<20}: {1}'.format(key, infos[key])) 102 | 103 | # Restore 104 | elif args.restore: 105 | name = args.restore 106 | 107 | # Resolve the backup 108 | if not name or not BackupManager.exists(name): 109 | raise Exception('This is not a valid backup') 110 | 111 | # Restore process 112 | B = BackupManager.get(name) 113 | 114 | try: 115 | M = B.restore() 116 | except BackupDirectoryExistsException: 117 | raise Exception( 118 | 'Cannot restore an instance on an existing directory. Please remove %s first.' % B.get('identifier') + 119 | 'Run: moodle remove %s' % B.get('identifier') 120 | ) 121 | except BackupDBExistsException: 122 | raise Exception( 123 | 'The database %s already exists. Please remove it first.' % B.get('dbname') + 124 | 'This command could help: moodle remove %s' % B.get('identifier') 125 | ) 126 | 127 | # Loads M object and display information 128 | logging.info('') 129 | logging.info('Restored instance information') 130 | logging.info('') 131 | infos = M.info() 132 | for key in sorted(infos.keys()): 133 | print('{0:<20}: {1}'.format(key, infos[key])) 134 | logging.info('') 135 | 136 | logging.info('Done.') 137 | 138 | # Backup the instance 139 | else: 140 | M = self.Wp.resolve(name) 141 | if not M: 142 | raise Exception('This is not a Moodle instance') 143 | 144 | try: 145 | BackupManager.create(M) 146 | except BackupDBEngineNotSupported: 147 | raise Exception('Does not support backup for the DB engine %s yet, sorry!' % M.get('dbtype')) 148 | 149 | except DistutilsFileError: 150 | raise Exception( 151 | 'Error while copying files. Check the permissions on the data directory.' + 152 | 'Or run: sudo chmod -R 0777 %s' % M.get('dataroot') 153 | ) 154 | 155 | logging.info('Done.') 156 | -------------------------------------------------------------------------------- /mdk/commands/cron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2024 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | from ..command import Command 26 | 27 | 28 | class CronCommand(Command): 29 | 30 | _arguments = [ 31 | ( 32 | ['-k', '--keep-alive'], 33 | { 34 | 'default': False, 35 | 'help': 'keep alive the cron task', 36 | 'dest': 'keepalive', 37 | 'action': 'store_true' 38 | }, 39 | ), 40 | ( 41 | ['-t', '--task'], 42 | { 43 | 'default': False, 44 | 'help': 'the name of scheduled task to run, as component:taskname or component\\taskname', 45 | 'dest': 'task', 46 | }, 47 | ), 48 | ( 49 | ['name'], 50 | { 51 | 'default': None, 52 | 'help': 'name of the instance', 53 | 'metavar': 'name', 54 | 'nargs': '?' 55 | }, 56 | ), 57 | ] 58 | _description = 'Run cron' 59 | 60 | def run(self, args): 61 | 62 | M = self.Wp.resolve(args.name) 63 | if not M: 64 | raise Exception('No instance to work on. Exiting...') 65 | 66 | if args.task: 67 | taskname = args.task 68 | if ':' in args.task: 69 | parts = args.task.split(':') 70 | taskname = parts[0] + '\\task\\' + parts[1] 71 | elif '\\' in args.task and not '\\task\\' in args.task: 72 | parts = args.task.split('\\', 1) 73 | taskname = parts[0] + '\\task\\' + parts[1] 74 | 75 | logging.info('Executing task %s on %s' % (taskname, M.get('identifier'))) 76 | r, _, _ = M.cli('admin/cli/scheduled_task.php', args=['--execute=%s' % taskname], stdout=None, stderr=None) 77 | 78 | if r > 0 and not taskname.endswith('_task'): 79 | taskname += '_task' 80 | logging.info('Retrying with %s' % (taskname)) 81 | M.cli('admin/cli/scheduled_task.php', args=['--execute=%s' % taskname], stdout=None, stderr=None) 82 | 83 | return 84 | 85 | logging.info('Running cron on %s' % (M.get('identifier'))) 86 | 87 | cliargs = [] 88 | haskeepalive = M.branch_compare(401, '>') 89 | if not args.keepalive and haskeepalive: 90 | cliargs.append('--keep-alive=0') 91 | elif args.keepalive: 92 | if not haskeepalive: 93 | logging.warn('Option --keep-alive is not available for on older versions than 4.1') 94 | # Other versions keep-live by default, so no need for additional argument. 95 | 96 | M.cli('admin/cli/cron.php', args=cliargs, stdout=None, stderr=None) 97 | -------------------------------------------------------------------------------- /mdk/commands/css.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2014 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | import os 26 | import sys 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 | print('This command has been removed as it was obsolete and unmaintained.') 94 | print('If you were using it please raise an issue to let us know.') 95 | print('https://github.com/FMCorz/mdk/issues') 96 | sys.exit(1) 97 | 98 | Mlist = self.Wp.resolveMultiple(args.names) 99 | if len(Mlist) < 1: 100 | raise Exception('No instances to work on. Exiting...') 101 | 102 | # Resolve the theme folder we are in. 103 | if not args.theme: 104 | mpath = os.path.join(Mlist[0].get('path'), 'theme') 105 | cwd = os.path.realpath(os.path.abspath(os.getcwd())) 106 | if cwd.startswith(mpath): 107 | candidate = cwd.replace(mpath, '').strip('/') 108 | while True: 109 | (head, tail) = os.path.split(candidate) 110 | if not head and tail: 111 | # Found the theme. 112 | args.theme = tail 113 | logging.info('You are in the theme \'%s\', using that.' % (args.theme)) 114 | break 115 | elif not head and not tail: 116 | # Nothing, let's leave. 117 | break 118 | candidate = head 119 | 120 | # We have not found anything, falling back on the default. 121 | if not args.theme: 122 | args.theme = 'bootstrapbase' 123 | 124 | for M in Mlist: 125 | if args.compile: 126 | logging.info('Compiling theme \'%s\' on %s' % (args.theme, M.get('identifier'))) 127 | processor = css.Css(M) 128 | processor.setDebug(args.debug) 129 | if args.debug: 130 | processor.setCompiler('lessc') 131 | elif M.branch_compare(29, '<'): 132 | # Grunt was only introduced for 2.9. 133 | processor.setCompiler('recess') 134 | 135 | processor.compile(theme=args.theme, sheets=args.sheets) 136 | 137 | # Setting up watchdog. This code should be improved when we will have more than a compile option. 138 | observer = None 139 | if args.compile and args.watch: 140 | observer = watchdog.observers.Observer() 141 | 142 | for M in Mlist: 143 | if args.watch and args.compile: 144 | processor = css.Css(M) 145 | processorArgs = {'theme': args.theme, 'sheets': args.sheets} 146 | handler = LessWatcher(M, processor, processorArgs) 147 | observer.schedule(handler, processor.getThemeLessPath(args.theme), recursive=True) 148 | logging.info('Watchdog set up on %s/%s, waiting for changes...' % (M.get('identifier'), args.theme)) 149 | 150 | if observer and args.compile and args.watch: 151 | observer.start() 152 | 153 | try: 154 | while True: 155 | time.sleep(1) 156 | except KeyboardInterrupt: 157 | observer.stop() 158 | finally: 159 | observer.join() 160 | 161 | 162 | class LessWatcher(watchdog.events.FileSystemEventHandler): 163 | 164 | _processor = None 165 | _args = None 166 | _ext = '.less' 167 | _M = None 168 | 169 | def __init__(self, M, processor, args): 170 | super(self.__class__, self).__init__() 171 | self._M = M 172 | self._processor = processor 173 | self._args = args 174 | 175 | def on_modified(self, event): 176 | self.process(event) 177 | 178 | def on_moved(self, event): 179 | self.process(event) 180 | 181 | def process(self, event): 182 | if event.is_directory: 183 | return 184 | elif not event.src_path.endswith(self._ext): 185 | return 186 | 187 | filename = event.src_path.replace(self._processor.getThemeLessPath(self._args['theme']), '').strip('/') 188 | logging.info('[%s] Changes detected in %s!' % (self._M.get('identifier'), filename)) 189 | self._processor.compile(**self._args) 190 | -------------------------------------------------------------------------------- /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 | from ..tools import yesOrNo 29 | 30 | 31 | class FixCommand(Command): 32 | 33 | _arguments = [ 34 | ( 35 | ['issue'], 36 | { 37 | 'help': 'tracker issue or issue number' 38 | } 39 | ), 40 | ( 41 | ['suffix'], 42 | { 43 | 'default': '', 44 | 'help': 'suffix of the branch', 45 | 'nargs': '?' 46 | } 47 | ), 48 | ( 49 | ['--autofix'], 50 | { 51 | 'action': 'store_true', 52 | 'default': '', 53 | 'help': 'auto fix the bug related to the issue number' 54 | } 55 | ), 56 | ( 57 | ['-n', '--name'], 58 | { 59 | 'default': None, 60 | 'help': 'name of the instance', 61 | 'metavar': 'name' 62 | } 63 | ) 64 | ] 65 | _description = 'Creates a branch associated to an MDL issue' 66 | 67 | def run(self, args): 68 | 69 | # Loading instance 70 | M = self.Wp.resolve(args.name) 71 | if not M: 72 | raise Exception('This is not a Moodle instance') 73 | 74 | stablebranch = M.get('stablebranch') 75 | masterbranch = '' 76 | if stablebranch in ['master', 'main']: 77 | # Generate a branch name for master to check later whether there's already an existing working branch. 78 | masterbranch = M.generateBranchName(args.issue, args.suffix, 'master') 79 | 80 | # Branch name 81 | branch = M.generateBranchName(args.issue, suffix=args.suffix) 82 | 83 | # Track 84 | track = '%s/%s' % (self.C.get('upstreamRemote'), stablebranch) 85 | 86 | # Git repo 87 | repo = M.git() 88 | 89 | hasBranch = repo.hasBranch(branch) 90 | 91 | # In this case, `stablebranch` would be 'main'. 92 | if masterbranch != '' and not hasBranch: 93 | # If the *-main branch does not yet exist, check there's an already equivalent *-master branch. 94 | if repo.hasBranch(masterbranch): 95 | prompt = (' It seems like you already have an existing working branch (%s).\n' 96 | ' Would you like to check this out instead?') 97 | if yesOrNo(prompt % masterbranch): 98 | # We'll check out the issue's *-master branch instead. 99 | branch = masterbranch 100 | hasBranch = True 101 | 102 | # Creating and checking out the new branch 103 | if not hasBranch: 104 | if not repo.createBranch(branch, track): 105 | raise Exception('Could not create branch %s' % branch) 106 | 107 | if not repo.checkout(branch): 108 | raise Exception('Error while checking out branch %s' % branch) 109 | 110 | logging.info('Branch %s checked out' % branch) 111 | 112 | # Auto-fixing the bug 113 | if args.autofix: 114 | logging.info('Auto fixing bug, please wait...') 115 | from time import sleep 116 | sleep(3) 117 | logging.info('That\'s a tricky one! Bear with me.') 118 | sleep(3) 119 | logging.info('Almost there!') 120 | sleep(3) 121 | logging.info('...') 122 | sleep(3) 123 | logging.info('You didn\'t think I was serious, did you?') 124 | sleep(3) 125 | logging.info('Now get to work!') 126 | -------------------------------------------------------------------------------- /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/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, 0o755) 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, 0o775) 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, 0o775) 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 | while True: 142 | git = question('What is the path of your Git installation?', C.get('git')) 143 | git = self.resolve_directory(git, username) 144 | 145 | if not os.access(git, os.X_OK): 146 | logging.error('Error while executing the Git command by path %s' % git + '.\nPlease fix or use another executable path.') 147 | continue 148 | 149 | gitversion = subprocess.run([git, '--version'], stdout=subprocess.PIPE) 150 | logging.info('Using ' + gitversion.stdout.decode('utf-8')) 151 | 152 | C.set('git', git) 153 | break 154 | 155 | # The default configuration file should point to the right directory for dirs.mdk, 156 | # we will just ensure that it exists. 157 | mdkdir = C.get('dirs.mdk') 158 | mdkdir = self.resolve_directory(mdkdir, username) 159 | if not os.path.isdir(mdkdir): 160 | try: 161 | logging.info('Creating MDK directory %s' % mdkdir) 162 | mkdir(mdkdir, 0o775) 163 | os.chown(mdkdir, user.pw_uid, usergroup.gr_gid) 164 | except: 165 | logging.error('Error while creating %s, please fix manually.' % mdkdir) 166 | 167 | # Git repository. 168 | github = question('What is your Github username? (Leave blank if not using Github)') 169 | if github != None: 170 | C.set('remotes.mine', C.get('remotes.mine').replace('YourGitHub', github)) 171 | C.set('repositoryUrl', C.get('repositoryUrl').replace('YourGitHub', github)) 172 | C.set('diffUrlTemplate', C.get('diffUrlTemplate').replace('YourGitHub', github)) 173 | C.set('myRemote', 'github') 174 | C.set('upstreamRemote', 'origin') 175 | else: 176 | C.set('remotes.mine', question('What is your remote?', C.get('remotes.mine'))) 177 | C.set('myRemote', question('What to call your remote?', C.get('myRemote'))) 178 | C.set('upstreamRemote', question('What to call the upsream remote (official Moodle remote)?', C.get('upstreamRemote'))) 179 | 180 | # Database settings. 181 | C.set('db.mysqli.user', question('What is your MySQL user?', C.get('db.mysqli.user'))) 182 | C.set('db.mysqli.passwd', question('What is your MySQL password?', 'root', password=True)) 183 | C.set('db.pgsql.user', question('What is your PostgreSQL user?', C.get('db.pgsql.user'))) 184 | C.set('db.pgsql.passwd', question('What is your PostgreSQL password?', 'root', password=True)) 185 | 186 | print('') 187 | print('MDK has been initialised with minimal configuration.') 188 | print('For more settings, edit your config file: %s.' % userconfigfile) 189 | print('Use %s as documentation.' % os.path.join(scriptdir, 'config-dist.json')) 190 | print('') 191 | print('Type the following command to create your first instance:') 192 | print(' mdk create') 193 | print('(This will take some time, but don\'t worry, that\'s because the cache is still empty)') 194 | print('') 195 | print('/! Please logout/login before to avoid permission issues: sudo su `whoami`') 196 | -------------------------------------------------------------------------------- /mdk/commands/install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import os 25 | import logging 26 | from ..command import Command 27 | from ..tools import mkdir 28 | from ..config import Conf 29 | 30 | C = Conf() 31 | 32 | 33 | class InstallCommand(Command): 34 | 35 | _description = 'Install a Moodle instance' 36 | 37 | def __init__(self, *args, **kwargs): 38 | super(InstallCommand, self).__init__(*args, **kwargs) 39 | 40 | profiles = [k for k, v in C.get('db').items() if type(v) is dict and 'engine' in v] 41 | self._arguments = [( 42 | ['-e', '--engine', '--dbprofile'], 43 | { 44 | 'action': 'store', 45 | 'choices': profiles, 46 | 'default': self.C.get('defaultEngine'), 47 | 'help': 'database profile to use', 48 | 'metavar': 'profile', 49 | 'dest': 'dbprofile' 50 | }, 51 | ), ( 52 | ['-f', '--fullname'], 53 | { 54 | 'action': 'store', 55 | 'help': 'full name of the instance', 56 | 'metavar': 'fullname' 57 | }, 58 | ), ( 59 | ['-r', '--run'], 60 | { 61 | 'action': 'store', 62 | 'help': 'scripts to run after installation', 63 | 'metavar': 'run', 64 | 'nargs': '*' 65 | }, 66 | ), ( 67 | ['name'], 68 | { 69 | 'default': None, 70 | 'help': 'name of the instance', 71 | 'metavar': 'name', 72 | 'nargs': '?' 73 | }, 74 | )] 75 | 76 | def run(self, args): 77 | 78 | name = args.name 79 | dbprofile = args.dbprofile 80 | fullname = args.fullname 81 | 82 | M = self.Wp.resolve(name) 83 | if not M: 84 | raise Exception('This is not a Moodle instance') 85 | 86 | name = M.get('identifier') 87 | dataDir = self.Wp.getPath(name, 'data') 88 | if not os.path.isdir(dataDir): 89 | mkdir(dataDir, 0o777) 90 | 91 | kwargs = {'dbprofile': dbprofile, 'fullname': fullname, 'dataDir': dataDir, 'wwwroot': self.Wp.getUrl(name)} 92 | M.install(**kwargs) 93 | 94 | # Running scripts 95 | if M.isInstalled() and type(args.run) == list: 96 | for script in args.run: 97 | logging.info('Running script \'%s\'' % (script)) 98 | try: 99 | M.runScript(script) 100 | except Exception as e: 101 | logging.warning('Error while running the script: %s' % e) 102 | -------------------------------------------------------------------------------- /mdk/commands/js.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2014 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | import os 26 | import sys 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 = [(['mode'], { 38 | 'metavar': 'mode', 39 | 'help': 'the type of action to perform', 40 | 'sub-commands': { 41 | 'shift': ( 42 | { 43 | 'help': 'keen to use shifter?' 44 | }, 45 | [ 46 | ( 47 | ['-p', '--plugin'], 48 | { 49 | 'action': 50 | 'store', 51 | 'dest': 52 | 'plugin', 53 | 'default': 54 | None, 55 | 'help': 56 | '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': 63 | 'store', 64 | 'dest': 65 | 'module', 66 | 'default': 67 | None, 68 | 'help': 69 | 'the name of the module in the plugin or subsystem. If omitted all the modules will be shifted, except we are in a module.' 70 | }, 71 | ), 72 | ( 73 | ['-w', '--watch'], 74 | { 75 | 'action': 'store_true', 76 | 'dest': 'watch', 77 | 'help': 'watch for changes to re-shift' 78 | }, 79 | ), 80 | ( 81 | ['names'], 82 | { 83 | 'default': None, 84 | 'help': 'name of the instances', 85 | 'metavar': 'names', 86 | 'nargs': '*' 87 | }, 88 | ), 89 | ], 90 | ), 91 | 'doc': ( 92 | { 93 | 'help': 'keen to generate documentation?' 94 | }, 95 | [ 96 | ( 97 | ['names'], 98 | { 99 | 'default': None, 100 | 'help': 'name of the instances', 101 | 'metavar': 'names', 102 | 'nargs': '*' 103 | }, 104 | ), 105 | ], 106 | ) 107 | } 108 | })] 109 | _description = 'Wrapper for JS functions' 110 | 111 | def run(self, args): 112 | print('This command has been removed as it was obsolete and unmaintained.') 113 | print('If you were using it please raise an issue to let us know.') 114 | print('https://github.com/FMCorz/mdk/issues') 115 | sys.exit(1) 116 | 117 | if args.mode == 'shift': 118 | self.shift(args) 119 | elif args.mode == 'doc': 120 | self.document(args) 121 | 122 | def shift(self, args): 123 | """The shift mode""" 124 | 125 | Mlist = self.Wp.resolveMultiple(args.names) 126 | if len(Mlist) < 1: 127 | raise Exception('No instances to work on. Exiting...') 128 | 129 | cwd = os.path.realpath(os.path.abspath(os.getcwd())) 130 | mpath = Mlist[0].get('path') 131 | relpath = cwd.replace(mpath, '').strip('/') 132 | 133 | # TODO Put that logic somewhere else because it is going to be re-used, I'm sure. 134 | if not args.plugin: 135 | (subsystemOrPlugin, pluginName) = plugins.PluginManager.getSubsystemOrPluginFromPath(cwd, Mlist[0]) 136 | if subsystemOrPlugin: 137 | args.plugin = subsystemOrPlugin + ('_' + pluginName) if pluginName else '' 138 | logging.info("I guessed the plugin/subsystem to work on as '%s'" % (args.plugin)) 139 | else: 140 | self.argumentError('The argument --plugin is required, I could not guess it.') 141 | 142 | if not args.module: 143 | candidate = relpath 144 | module = None 145 | while '/yui/src' in candidate: 146 | (head, tail) = os.path.split(candidate) 147 | if head.endswith('/yui/src'): 148 | module = tail 149 | break 150 | candidate = head 151 | 152 | if module: 153 | args.module = module 154 | logging.info("I guessed the JS module to work on as '%s'" % (args.module)) 155 | 156 | for M in Mlist: 157 | if len(Mlist) > 1: 158 | logging.info('Let\'s shift everything you wanted on \'%s\'' % (M.get('identifier'))) 159 | 160 | processor = js.Js(M) 161 | processor.shift(subsystemOrPlugin=args.plugin, module=args.module) 162 | 163 | if args.watch: 164 | observer = watchdog.observers.Observer() 165 | 166 | for M in Mlist: 167 | processor = js.Js(M) 168 | processorArgs = {'subsystemOrPlugin': args.plugin, 'module': args.module} 169 | handler = JsShiftWatcher(M, processor, processorArgs) 170 | observer.schedule(handler, processor.getYUISrcPath(**processorArgs), recursive=True) 171 | logging.info('Watchdog set up on %s, waiting for changes...' % (M.get('identifier'))) 172 | 173 | observer.start() 174 | 175 | try: 176 | while True: 177 | time.sleep(1) 178 | except KeyboardInterrupt: 179 | observer.stop() 180 | finally: 181 | observer.join() 182 | 183 | def document(self, args): 184 | """The docmentation mode""" 185 | 186 | Mlist = self.Wp.resolveMultiple(args.names) 187 | if len(Mlist) < 1: 188 | raise Exception('No instances to work on. Exiting...') 189 | 190 | for M in Mlist: 191 | logging.info('Documenting everything you wanted on \'%s\'. This may take a while...', M.get('identifier')) 192 | outdir = self.Wp.getExtraDir(M.get('identifier'), 'jsdoc') 193 | outurl = self.Wp.getUrl(M.get('identifier'), extra='jsdoc') 194 | processor = js.Js(M) 195 | processor.document(outdir) 196 | logging.info('Documentation available at:\n %s\n %s', outdir, outurl) 197 | 198 | 199 | class JsShiftWatcher(watchdog.events.FileSystemEventHandler): 200 | 201 | _processor = None 202 | _args = None 203 | _ext = ['.js', '.json'] 204 | _M = None 205 | 206 | def __init__(self, M, processor, args): 207 | super(self.__class__, self).__init__() 208 | self._M = M 209 | self._processor = processor 210 | self._args = args 211 | 212 | def on_modified(self, event): 213 | if event.is_directory: 214 | return 215 | elif not os.path.splitext(event.src_path)[1] in self._ext: 216 | return 217 | self.process(event) 218 | 219 | def on_moved(self, event): 220 | if not os.path.splitext(event.dest_path)[1] in self._ext: 221 | return 222 | self.process(event) 223 | 224 | def process(self, event): 225 | logging.info('[%s] (%s) Changes detected!' % (self._M.get('identifier'), datetime.datetime.now().strftime('%H:%M:%S'))) 226 | 227 | try: 228 | self._processor.shift(**self._args) 229 | except js.ShifterCompileFailed: 230 | logging.error(' /!\ Error: Compile failed!') 231 | -------------------------------------------------------------------------------- /mdk/commands/php.py: -------------------------------------------------------------------------------- 1 | """ 2 | Moodle Development Kit 3 | 4 | Copyright (c) 2025 Frédéric Massart 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 | import argparse 23 | from typing import List 24 | from ..command import Command 25 | 26 | 27 | class PhpCommand(Command): 28 | 29 | # We cannot define arguments, or they could conflict with the ones for PHP. 30 | _arguments = [] 31 | _description = 'Invokes PHP in a Moodle instance' 32 | 33 | def parse_args(self, parser: argparse.ArgumentParser, sysargs: List[str]): 34 | [args, unknown] = parser.parse_known_args(sysargs) 35 | args.cmdargs = unknown 36 | return args 37 | 38 | def run(self, args): 39 | 40 | M = self.Wp.resolve(None) 41 | if not M: 42 | raise Exception('No instance to work on. Exiting...') 43 | 44 | M.php(args.cmdargs, stdout=None, stderr=None) 45 | -------------------------------------------------------------------------------- /mdk/commands/phpunit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | import os 26 | import gzip 27 | import urllib.request, urllib.parse, urllib.error 28 | from ..command import Command 29 | from ..tools import question 30 | from ..phpunit import PHPUnit 31 | 32 | 33 | class PhpunitCommand(Command): 34 | 35 | _arguments = [ 36 | ( 37 | ['-f', '--force'], 38 | { 39 | 'action': 'store_true', 40 | 'help': 'force the initialisation' 41 | }, 42 | ), 43 | ( 44 | ['-r', '--run'], 45 | { 46 | 'action': 'store_true', 47 | 'help': 'also run the tests' 48 | }, 49 | ), 50 | ( 51 | ['-t', '--testcase'], 52 | { 53 | 'default': None, 54 | 'help': 'testcase class to run (From Moodle 2.6)', 55 | 'metavar': 'testcase' 56 | }, 57 | ), 58 | ( 59 | ['-s', '--testsuite'], 60 | { 61 | 'default': None, 62 | 'help': 'testsuite to run', 63 | 'metavar': 'testsuite' 64 | }, 65 | ), 66 | ( 67 | ['-u', '--unittest'], 68 | { 69 | 'default': None, 70 | 'help': 'test file to run', 71 | 'metavar': 'path' 72 | }, 73 | ), 74 | ( 75 | ['-k', '--skip-init'], 76 | { 77 | 'action': 'store_true', 78 | 'dest': 'skipinit', 79 | 'help': 'allows tests to start quicker when the instance is already initialised' 80 | }, 81 | ), 82 | ( 83 | ['-q', '--stop-on-failure'], 84 | { 85 | 'action': 'store_true', 86 | 'dest': 'stoponfailure', 87 | 'help': 'stop execution upon first failure or error' 88 | }, 89 | ), 90 | ( 91 | ['-c', '--coverage'], 92 | { 93 | 'action': 'store_true', 94 | 'help': 'creates the HTML code coverage report' 95 | }, 96 | ), 97 | ( 98 | ['--filter'], 99 | { 100 | 'default': None, 101 | 'help': 'filter to pass through to PHPUnit', 102 | 'metavar': 'filter' 103 | }, 104 | ), 105 | ( 106 | ['--repeat'], 107 | { 108 | 'default': None, 109 | 'help': 'run tests repeatedly for the given number of times', 110 | 'metavar': 'times', 111 | 'type': int 112 | }, 113 | ), 114 | ( 115 | ['name'], 116 | { 117 | 'default': None, 118 | 'help': 'name of the instance', 119 | 'metavar': 'name', 120 | 'nargs': '?' 121 | }, 122 | ), 123 | ] 124 | _description = 'Initialize PHPUnit' 125 | 126 | def run(self, args): 127 | 128 | # Loading instance 129 | M = self.Wp.resolve(args.name) 130 | if not M: 131 | raise Exception('This is not a Moodle instance') 132 | 133 | # Check if installed 134 | if not M.get('installed'): 135 | raise Exception('This instance needs to be installed first') 136 | 137 | # Check if testcase option is available. 138 | if args.testcase and M.branch_compare('26', '<'): 139 | self.argumentError('The --testcase option only works with Moodle 2.6 or greater.') 140 | 141 | # Create the Unit test object. 142 | PU = PHPUnit(self.Wp, M) 143 | 144 | # Skip init. 145 | if not args.skipinit: 146 | self.init(M, PU, args) 147 | 148 | # Automatically add the suffix _testsuite. 149 | testsuite = args.testsuite 150 | if testsuite and not testsuite.endswith('_testsuite'): 151 | testsuite += '_testsuite' 152 | 153 | # Check repeat arg. 154 | repeat = args.repeat 155 | if repeat: 156 | # Make sure repeat is greater than 1. 157 | if repeat <= 1: 158 | repeat = None 159 | 160 | kwargs = { 161 | 'coverage': args.coverage, 162 | 'filter': args.filter, 163 | 'testcase': args.testcase, 164 | 'testsuite': testsuite, 165 | 'unittest': args.unittest, 166 | 'stopon': [] if not args.stoponfailure else ['failure'], 167 | 'repeat': repeat 168 | } 169 | 170 | if args.run: 171 | PU.run(**kwargs) 172 | if args.coverage: 173 | logging.info('Code coverage is available at: \n %s', (PU.getCoverageUrl())) 174 | else: 175 | logging.info('Start PHPUnit:\n %s', (' '.join(PU.getCommand(**kwargs)))) 176 | 177 | def init(self, M, PU, args): 178 | """Initialises PHP Unit""" 179 | 180 | # Install Composer 181 | if PU.usesComposer(): 182 | M.installComposerAndDevDependenciesIfNeeded() 183 | 184 | # If Oracle, ask the user for a Behat prefix, if not set. 185 | prefix = M.get('phpunit_prefix') 186 | if M.get('dbtype') == 'oci' and (args.force or not prefix or len(prefix) > 2): 187 | while not prefix or len(prefix) > 2: 188 | prefix = question('What prefix would you like to use? (Oracle, max 2 chars)') 189 | else: 190 | prefix = None 191 | 192 | PU.init(force=args.force, prefix=prefix) 193 | -------------------------------------------------------------------------------- /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/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 list(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 list(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/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/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, yesOrNo 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, main)' 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 | if not yesOrNo('Are you sure you want to continue?'): 143 | print('Exiting...') 144 | return 145 | 146 | J = jira.Jira() 147 | 148 | # If the mode is not set to patch yet, and we can identify the MDL number. 149 | if not args.patch and parsedbranch: 150 | mdlIssue = 'MDL-%s' % (parsedbranch['issue']) 151 | try: 152 | args.patch = J.isSecurityIssue(mdlIssue) 153 | if args.patch: 154 | logging.info('%s appears to be a security issue, switching to patch mode...', mdlIssue) 155 | except jira.JiraIssueNotFoundException: 156 | # The issue was not found, do not perform 157 | logging.warn('Could not check if %s is a security issue', mdlIssue) 158 | 159 | if args.patch: 160 | if not M.pushPatch(branch): 161 | return 162 | 163 | else: 164 | # Pushing current branch 165 | logging.info('Pushing branch %s to remote %s...' % (branch, remote)) 166 | result = M.git().push(remote, branch, force=args.force) 167 | if result[0] != 0: 168 | raise Exception(result[2]) 169 | 170 | # Update the tracker 171 | if args.updatetracker != None: 172 | ref = None if args.updatetracker == True else args.updatetracker 173 | M.updateTrackerGitInfo(branch=branch, ref=ref) 174 | 175 | # Pushing stable branch 176 | if args.includestable: 177 | branch = M.get('stablebranch') 178 | logging.info('Pushing branch %s to remote %s...' % (branch, remote)) 179 | result = M.git().push(remote, branch, force=args.forcestable) 180 | if result[0] != 0: 181 | raise Exception(result[2]) 182 | 183 | logging.info('Done.') 184 | -------------------------------------------------------------------------------- /mdk/commands/remove.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 ..tools import yesOrNo 27 | from ..command import Command 28 | 29 | 30 | class RemoveCommand(Command): 31 | 32 | _arguments = [ 33 | ( 34 | ['name'], 35 | { 36 | 'help': 'name of the instance' 37 | } 38 | ), 39 | ( 40 | ['-y'], 41 | { 42 | 'action': 'store_true', 43 | 'dest': 'do', 44 | 'help': 'do not ask for confirmation' 45 | } 46 | ), 47 | ( 48 | ['-f'], 49 | { 50 | 'action': 'store_true', 51 | 'dest': 'force', 52 | 'help': 'force and do not ask for confirmation' 53 | } 54 | ) 55 | ] 56 | _description = 'Completely remove an instance' 57 | 58 | def run(self, args): 59 | 60 | try: 61 | M = self.Wp.get(args.name) 62 | except: 63 | raise Exception('This is not a Moodle instance') 64 | 65 | if not args.do and not args.force: 66 | if not yesOrNo('Are you sure?'): 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/commands/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | from ..command import Command 26 | from ..scripts import Scripts 27 | 28 | 29 | class RunCommand(Command): 30 | 31 | _arguments = [ 32 | ( 33 | ['-l', '--list'], 34 | { 35 | 'action': 'store_true', 36 | 'dest': 'list', 37 | 'help': 'list the available scripts' 38 | }, 39 | ), 40 | ( 41 | ['-a', '--all'], 42 | { 43 | 'action': 'store_true', 44 | 'dest': 'all', 45 | 'help': 'runs the script on each instance' 46 | }, 47 | ), 48 | ( 49 | ['-i', '--integration'], 50 | { 51 | 'action': 'store_true', 52 | 'dest': 'integration', 53 | 'help': 'runs the script on integration instances' 54 | }, 55 | ), 56 | ( 57 | ['-s', '--stable'], 58 | { 59 | 'action': 'store_true', 60 | 'dest': 'stable', 61 | 'help': 'runs the script on stable instances' 62 | }, 63 | ), 64 | ( 65 | ['-g', '--arguments'], 66 | { 67 | 'help': 68 | 'a list of arguments to pass to the script. Use --arguments="--list of --arguments" if ' 69 | 'you need to use dashes. Otherwise add -- after the argument list.', 70 | 'metavar': 'arguments', 71 | 'nargs': '+' 72 | }, 73 | ), 74 | ( 75 | ['script'], 76 | { 77 | 'nargs': '?', 78 | 'help': 'the name of the script to run' 79 | }, 80 | ), 81 | ( 82 | ['names'], 83 | { 84 | 'default': None, 85 | 'help': 'name of the instances', 86 | 'nargs': '*' 87 | }, 88 | ), 89 | ] 90 | _description = 'Run a script on a Moodle instance' 91 | 92 | def run(self, args): 93 | 94 | # Printing existing scripts 95 | if args.list: 96 | scripts = Scripts.list() 97 | for script in sorted(scripts.keys()): 98 | print('%s (%s)' % (script, scripts[script])) 99 | return 100 | 101 | # Trigger error when script is missing 102 | if not args.script: 103 | self.argumentError('missing script name') 104 | 105 | # Resolving instances 106 | names = args.names 107 | if args.all: 108 | names = self.Wp.list() 109 | elif args.integration or args.stable: 110 | names = self.Wp.list(integration=args.integration, stable=args.stable) 111 | 112 | # Doing stuff 113 | Mlist = self.Wp.resolveMultiple(names) 114 | if len(Mlist) < 1: 115 | raise Exception('No instances to work on. Exiting...') 116 | 117 | for M in Mlist: 118 | logging.info('Running \'%s\' on \'%s\'' % (args.script, M.get('identifier'))) 119 | try: 120 | M.runScript(args.script, stderr=None, stdout=None, arguments=args.arguments) 121 | except Exception as e: 122 | logging.warning('Error while running the script on %s' % M.get('identifier')) 123 | logging.debug(e) 124 | else: 125 | logging.info('') 126 | 127 | logging.info('Done.') 128 | -------------------------------------------------------------------------------- /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 | ['--open'], 38 | { 39 | 'action': 'store_true', 40 | 'help': 'Open issue in browser' 41 | } 42 | ), 43 | ( 44 | ['-t', '--testing'], 45 | { 46 | 'action': 'store_true', 47 | 'help': 'include testing instructions' 48 | } 49 | ), 50 | ( 51 | ['issue'], 52 | { 53 | 'help': 'MDL issue number. Guessed from the current branch if not specified.', 54 | 'nargs': '?' 55 | } 56 | ), 57 | ( 58 | ['--add-labels'], 59 | { 60 | 'action': 'store', 61 | 'dest': 'addlabels', 62 | 'help': 'add the specified labels to the issue', 63 | 'metavar': 'labels', 64 | 'nargs': '+', 65 | } 66 | ), 67 | ( 68 | ['--remove-labels'], 69 | { 70 | 'action': 'store', 71 | 'dest': 'removelabels', 72 | 'help': 'remove the specified labels from the issue', 73 | 'metavar': 'labels', 74 | 'nargs': '+', 75 | } 76 | ), 77 | ( 78 | ['--comment'], 79 | { 80 | 'action': 'store_true', 81 | 'help': 'add a comment to the issue', 82 | } 83 | ) 84 | ] 85 | _description = 'Interact with Moodle tracker' 86 | 87 | Jira = None 88 | mdl = None 89 | 90 | def run(self, args): 91 | 92 | issue = None 93 | if not args.issue: 94 | M = self.Wp.resolve() 95 | if M: 96 | parsedbranch = parseBranch(M.currentBranch()) 97 | if parsedbranch: 98 | issue = parsedbranch['issue'] 99 | else: 100 | issue = args.issue 101 | 102 | if not issue or not re.match('(MDL|mdl)?(-|_)?[1-9]+', issue): 103 | raise Exception('Invalid or unknown issue number') 104 | 105 | self.mdl = 'MDL-' + re.sub(r'(MDL|mdl)(-|_)?', '', issue) 106 | 107 | if args.open: 108 | Jira.openInBrowser(self.mdl) 109 | return 110 | 111 | self.Jira = Jira() 112 | 113 | if args.addlabels: 114 | if 'triaged' in args.addlabels: 115 | self.argumentError('The label \'triaged\' cannot be added using MDK') 116 | elif 'triaging_in_progress' in args.addlabels: 117 | self.argumentError('The label \'triaging_in_progress\' cannot be added using MDK') 118 | self.Jira.addLabels(self.mdl, args.addlabels) 119 | 120 | if args.removelabels: 121 | if 'triaged' in args.removelabels: 122 | self.argumentError('The label \'triaged\' cannot be removed using MDK') 123 | elif 'triaging_in_progress' in args.removelabels: 124 | self.argumentError('The label \'triaging_in_progress\' cannot be removed using MDK') 125 | self.Jira.removeLabels(self.mdl, args.removelabels) 126 | 127 | if args.comment: 128 | comment = getText() 129 | self.Jira.addComment(self.mdl, comment) 130 | 131 | self.info(args) 132 | 133 | def info(self, args): 134 | """Display classic information about an issue""" 135 | issue = self.Jira.getIssue(self.mdl) 136 | 137 | title = '%s: %s' % (issue['key'], issue['fields']['summary']) 138 | created = datetime.strftime(Jira.parseDate(issue['fields'].get('created')), '%Y-%m-%d %H:%M') 139 | resolution = '' if issue['fields']['resolution'] == None else '(%s)' % (issue['fields']['resolution']['name']) 140 | resolutiondate = '' 141 | if issue['fields'].get('resolutiondate') != None: 142 | resolutiondate = datetime.strftime(Jira.parseDate(issue['fields'].get('resolutiondate')), '%Y-%m-%d %H:%M') 143 | print('-' * 72) 144 | for l in textwrap.wrap(title, 68, initial_indent=' ', subsequent_indent=' '): 145 | print(l) 146 | print(' {0} - {1} - {2}'.format(issue['fields']['issuetype']['name'], issue['fields']['priority']['name'], 'https://tracker.moodle.org/browse/' + issue['key'])) 147 | status = '{0} {1} {2}'.format(issue['fields']['status']['name'], resolution, resolutiondate).strip() 148 | print(' {0}'.format(status)) 149 | 150 | print('-' * 72) 151 | components = '{0}: {1}'.format('Components', ', '.join([c['name'] for c in issue['fields']['components']])) 152 | for l in textwrap.wrap(components, 68, initial_indent=' ', subsequent_indent=' '): 153 | print(l) 154 | if issue['fields']['labels']: 155 | labels = '{0}: {1}'.format('Labels', ', '.join(issue['fields']['labels'])) 156 | for l in textwrap.wrap(labels, 68, initial_indent=' ', subsequent_indent=' '): 157 | print(l) 158 | 159 | vw = '[ V: %d - W: %d ]' % (issue['fields']['votes']['votes'], issue['fields']['watches']['watchCount']) 160 | print('{0:->70}--'.format(vw)) 161 | print('{0:<20}: {1} ({2}) on {3}'.format('Reporter', issue['fields']['reporter']['displayName'], issue['fields']['reporter']['name'], created)) 162 | 163 | if issue['fields'].get('assignee') != None: 164 | print('{0:<20}: {1} ({2})'.format('Assignee', issue['fields']['assignee']['displayName'], issue['fields']['assignee']['name'])) 165 | if issue['named'].get('Peer reviewer'): 166 | print('{0:<20}: {1} ({2})'.format('Peer reviewer', issue['named']['Peer reviewer']['displayName'], issue['named']['Peer reviewer']['name'])) 167 | if issue['named'].get('Integrator'): 168 | print('{0:<20}: {1} ({2})'.format('Integrator', issue['named']['Integrator']['displayName'], issue['named']['Integrator']['name'])) 169 | if issue['named'].get('Tester'): 170 | print('{0:<20}: {1} ({2})'.format('Tester', issue['named']['Tester']['displayName'], issue['named']['Tester']['name'])) 171 | 172 | if args.testing and issue['named'].get('Testing Instructions'): 173 | print('-' * 72) 174 | print('Testing instructions:') 175 | for l in issue['named'].get('Testing Instructions').split('\r\n'): 176 | print(' ' + l) 177 | 178 | print('-' * 72) 179 | -------------------------------------------------------------------------------- /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 ..tools import yesOrNo 27 | from ..command import Command 28 | 29 | 30 | class UninstallCommand(Command): 31 | 32 | _description = 'Uninstall a Moodle instance' 33 | _arguments = [ 34 | ( 35 | ['name'], 36 | { 37 | 'default': None, 38 | 'help': 'name of the instance', 39 | 'metavar': 'name', 40 | 'nargs': '?' 41 | } 42 | ), 43 | ( 44 | ['-y'], 45 | { 46 | 'action': 'store_true', 47 | 'dest': 'do', 48 | 'help': 'do not ask for confirmation' 49 | } 50 | ) 51 | ] 52 | 53 | def run(self, args): 54 | 55 | M = self.Wp.resolve(args.name) 56 | if not M: 57 | raise Exception('This is not a Moodle instance') 58 | elif not M.isInstalled(): 59 | logging.info('This instance is not installed') 60 | return 61 | 62 | if not args.do: 63 | if not yesOrNo('Are you sure?'): 64 | logging.info('Aborting...') 65 | return 66 | 67 | logging.info('Uninstalling %s...' % M.get('identifier')) 68 | M.uninstall() 69 | logging.info('Done.') 70 | -------------------------------------------------------------------------------- /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/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/container.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import os 3 | from pathlib import Path 4 | import shutil 5 | from typing import Dict, List, Optional 6 | from mdk.config import Conf 7 | 8 | from mdk.tools import get_absolute_path, mkdir, process 9 | 10 | C = Conf() 11 | 12 | 13 | def is_docker_container_running(name: str) -> bool: 14 | """Check if a Docker container is running.""" 15 | r, _, _ = process(['docker', 'top', name]) 16 | return r == 0 17 | 18 | 19 | class Container(abc.ABC): 20 | """Interface to abstract env and commands inside a container.""" 21 | 22 | @abc.abstractmethod 23 | def chmod(self, path: Path, mode: int) -> None: 24 | pass 25 | 26 | @abc.abstractmethod 27 | def exists(self, path: Path) -> bool: 28 | pass 29 | 30 | @abc.abstractmethod 31 | def exec(self, command: List[str], **kwargs): 32 | pass 33 | 34 | @abc.abstractmethod 35 | def isdir(self, path: Path) -> bool: 36 | pass 37 | 38 | @abc.abstractmethod 39 | def mkdir(self, path: Path, mode: int) -> None: 40 | pass 41 | 42 | @property 43 | @abc.abstractmethod 44 | def path(self) -> Path: 45 | pass 46 | 47 | @abc.abstractmethod 48 | def rmtree(self, path: Path) -> None: 49 | pass 50 | 51 | @property 52 | @abc.abstractmethod 53 | def dataroot(self) -> Path: 54 | pass 55 | 56 | @property 57 | @abc.abstractmethod 58 | def behat_dataroot(self) -> Path: 59 | pass 60 | 61 | @property 62 | @abc.abstractmethod 63 | def behat_faildumps(self) -> Optional[Path]: 64 | """Return the path to the fail dumps directory. I'm not convinced about this.""" 65 | return None 66 | 67 | @property 68 | @abc.abstractmethod 69 | def behat_wwwroot(self) -> str: 70 | pass 71 | 72 | @property 73 | @abc.abstractmethod 74 | def phpunit_dataroot(self) -> Path: 75 | pass 76 | 77 | 78 | class HostContainer(Container): 79 | """Pretends to be a container but is the host machine.""" 80 | _identifier: Optional[str] 81 | _path: Path 82 | _dataroot: Optional[Path] 83 | _binaries: Dict[str, str] 84 | 85 | def __init__( 86 | self, *, path: Path, identifier: Optional[str] = None, dataroot: Optional[Path] = None, binaries: Dict[str, str] = None 87 | ): 88 | self._identifier = identifier 89 | self._path = path 90 | self._dataroot = dataroot 91 | self._binaries = binaries or {} 92 | 93 | def chmod(self, path: Path, mode: int) -> None: 94 | os.chmod(get_absolute_path(path, self.path), mode) 95 | 96 | def exists(self, path: Path) -> bool: 97 | return (get_absolute_path(path, self.path)).exists() 98 | 99 | def exec(self, command: List[str], **kwargs): 100 | bin = command[0] 101 | if bin in self._binaries: 102 | command[0] = self._binaries[bin] 103 | return process(command, cwd=self.path, **kwargs) 104 | 105 | def isdir(self, path: Path) -> bool: 106 | return (get_absolute_path(path, self.path)).is_dir() 107 | 108 | def mkdir(self, path: Path, mode: int) -> None: 109 | mkdir(get_absolute_path(path, self.path), mode) 110 | 111 | @property 112 | def path(self) -> Path: 113 | return self._path 114 | 115 | def rmtree(self, path: Path) -> None: 116 | shutil.rmtree(get_absolute_path(path, self.path), True) 117 | 118 | @property 119 | def dataroot(self) -> Path: 120 | if not self._dataroot: 121 | raise ValueError('Unknown dataroot.') 122 | return self._dataroot 123 | 124 | @property 125 | def behat_dataroot(self) -> Path: 126 | if not self._dataroot: 127 | raise ValueError('Cannot resolve behat dataroot without dataroot.') 128 | return self._dataroot.with_name(self._dataroot.name + '_behat') 129 | 130 | @property 131 | def behat_faildumps(self) -> Optional[Path]: 132 | return None 133 | 134 | @property 135 | def behat_wwwroot(self) -> str: 136 | if not self._identifier: 137 | raise ValueError('Instance identifier unknown.') 138 | wwwroot = '%s://%s/' % (C.get('scheme'), C.get('behat.host')) 139 | if C.get('path'): 140 | wwwroot = wwwroot + C.get('path') + '/' 141 | return wwwroot + self._identifier 142 | 143 | @property 144 | def phpunit_dataroot(self) -> Path: 145 | if not self._dataroot: 146 | raise ValueError('Cannot resolve PHPUnit dataroot without dataroot.') 147 | return self._dataroot.with_name(self._dataroot.name + '_phpu') 148 | 149 | 150 | class DockerContainer(Container): 151 | """Docker container.""" 152 | _name: str 153 | _hostpath: Path 154 | 155 | def __init__(self, *, hostpath: Path, name: str): 156 | self._name = name 157 | self._hostpath = hostpath 158 | 159 | def chmod(self, path: Path, mode: int) -> None: 160 | path = get_absolute_path(path, self.path) 161 | self.exec(['chmod', f'{mode:o}', path.as_posix()]) 162 | 163 | def exists(self, path: Path) -> bool: 164 | r, _, _ = self.exec(['test', '-e', (get_absolute_path(path, self.path)).as_posix()]) 165 | return r == 0 166 | 167 | def exec(self, command: List[str], **kwargs): 168 | # We surely will want to customise the user, but for simplicity at the moment all is done by root. 169 | hostcommand = ['docker', 'exec', '-w', self.path.as_posix(), '-u', '0:0', '-it', self._name, *command] 170 | return process(hostcommand, cwd=self._hostpath, **kwargs) 171 | 172 | def isdir(self, path: Path) -> bool: 173 | r, _, _ = self.exec(['test', '-d', get_absolute_path(path, self.path).as_posix()]) 174 | return r == 0 175 | 176 | def mkdir(self, path: Path, mode: int) -> None: 177 | path = get_absolute_path(path, self.path) 178 | self.exec(['mkdir', '-p', path.as_posix()]) 179 | self.chmod(path, mode) 180 | 181 | @property 182 | def path(self) -> Path: 183 | return Path('/var/www/html') 184 | 185 | def rmtree(self, path: Path) -> None: 186 | path = get_absolute_path(path, self.path) 187 | self.exec(['rm', '-r', path.as_posix()]) 188 | 189 | @property 190 | def dataroot(self) -> Path: 191 | return Path('/var/www/moodledata') 192 | 193 | @property 194 | def behat_dataroot(self) -> Path: 195 | return Path('/var/www/behatdata') 196 | 197 | @property 198 | def behat_faildumps(self) -> Optional[Path]: 199 | return Path('/var/www/behatfaildumps') 200 | 201 | @property 202 | def behat_wwwroot(self) -> str: 203 | return f'http://{self._name}' 204 | 205 | @property 206 | def phpunit_dataroot(self) -> Path: 207 | return Path('/var/www/phpunitdata') 208 | -------------------------------------------------------------------------------- /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/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/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/phpunit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2014 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | import os 26 | 27 | from mdk.moodle import Moodle 28 | 29 | from .config import Conf 30 | from .tools import mkdir 31 | 32 | C = Conf() 33 | 34 | 35 | class PHPUnit(object): 36 | """Class wrapping PHPUnit functions""" 37 | 38 | _M = None 39 | _Wp = None 40 | 41 | def __init__(self, Wp, M): 42 | self._Wp = Wp 43 | self._M = M 44 | 45 | def getCommand(self, testcase=None, unittest=None, filter=None, coverage=None, testsuite=None, stopon=None, repeat=None): 46 | """Get the PHPUnit command""" 47 | cmd = [] 48 | if self.usesComposer(): 49 | cmd.append('vendor/bin/phpunit') 50 | else: 51 | cmd.append('phpunit') 52 | 53 | if coverage: 54 | cmd.append('--coverage-html') 55 | cmd.append(self.getCoverageDir()) 56 | 57 | if stopon: 58 | for on in stopon: 59 | cmd.append('--stop-on-%s' % on) 60 | 61 | if repeat: 62 | cmd.append('--repeat=%d' % repeat) 63 | 64 | if testcase: 65 | cmd.append(testcase) 66 | elif unittest: 67 | cmd.append(unittest) 68 | elif filter: 69 | cmd.append('--filter="%s"' % filter) 70 | elif testsuite: 71 | cmd.append('--testsuite') 72 | cmd.append(testsuite) 73 | 74 | return cmd 75 | 76 | def getCoverageDir(self): 77 | """Get the Coverage directory, and create it if required""" 78 | return self.Wp.getExtraDir(self.M.get('identifier'), 'coverage') 79 | 80 | def getCoverageUrl(self): 81 | """Return the code coverage URL""" 82 | return self.Wp.getUrl(self.M.get('identifier'), extra='coverage') 83 | 84 | def init(self, force=False, prefix=None): 85 | """Initialise the PHPUnit environment""" 86 | 87 | if self.M.branch_compare(23, '<'): 88 | raise Exception('PHPUnit is only available from Moodle 2.3') 89 | 90 | # Set PHPUnit data root 91 | phpunit_dataroot = self.M.container.phpunit_dataroot 92 | self.M.updateConfig('phpunit_dataroot', phpunit_dataroot.as_posix()) 93 | if not self.M.container.isdir(self.M.container.phpunit_dataroot): 94 | self.M.container.mkdir(self.M.container.phpunit_dataroot, 0o777) 95 | 96 | # Set PHPUnit prefix 97 | currentPrefix = self.M.get('phpunit_prefix') 98 | phpunit_prefix = prefix or 'phpu_' 99 | 100 | if not currentPrefix or force: 101 | self.M.updateConfig('phpunit_prefix', phpunit_prefix) 102 | elif currentPrefix != phpunit_prefix and self.M.get('dbtype') != 'oci': 103 | # Warn that a prefix is already set and we did not change it. 104 | # No warning for Oracle as we need to set it to something else. 105 | logging.warning('PHPUnit prefix not changed, already set to \'%s\', expected \'%s\'.' % (currentPrefix, phpunit_prefix)) 106 | 107 | result = (None, None, None) 108 | exception = None 109 | try: 110 | if force: 111 | result = self.M.cli('/admin/tool/phpunit/cli/util.php', args=['--drop'], stdout=None, stderr=None) 112 | result = self.M.cli('/admin/tool/phpunit/cli/init.php', stdout=None, stderr=None) 113 | except Exception as exc: 114 | exception = exc 115 | pass 116 | 117 | resultcode = result[0] if result[0] is not None else -1 118 | if exception != None or resultcode > 0: 119 | if resultcode == 129: 120 | raise Exception('PHPUnit is not installed on your system') 121 | elif resultcode > 0: 122 | raise Exception('Something wrong with PHPUnit configuration') 123 | else: 124 | raise exception 125 | 126 | if C.get('phpunit.buildcomponentconfigs'): 127 | try: 128 | result = self.M.cli('/admin/tool/phpunit/cli/util.php', args=['--buildcomponentconfigs'], stdout=None, stderr=None) 129 | except Exception as exception: 130 | pass 131 | 132 | if exception != None or result[0] > 0: 133 | raise Exception('Unable to build distributed phpunit.xml files for each component') 134 | else: 135 | logging.info('Distributed phpunit.xml files built.') 136 | 137 | logging.info('PHPUnit ready!') 138 | 139 | def run(self, **kwargs): 140 | """Execute the command""" 141 | cmd = self.getCommand(**kwargs) 142 | return self.M.exec(cmd, stdout=None, stderr=None) 143 | 144 | def usesComposer(self): 145 | """Return whether or not the instance uses composer, the latter is considered installed""" 146 | return os.path.isfile(os.path.join(self.M.get('path'), 'composer.json')) 147 | 148 | @property 149 | def M(self) -> Moodle: 150 | return self._M 151 | 152 | @property 153 | def Wp(self): 154 | return self._Wp 155 | -------------------------------------------------------------------------------- /mdk/scripts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2013 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | import logging 25 | import os 26 | import shutil 27 | import stat 28 | from contextlib import contextmanager 29 | from pathlib import Path 30 | 31 | from .config import Conf 32 | from .exceptions import ConflictInScriptName, ScriptNotFound, UnsupportedScript 33 | from .tools import process 34 | 35 | C = Conf() 36 | 37 | 38 | class Scripts(object): 39 | 40 | _supported = ['php', 'sh'] 41 | _dirs = None 42 | _list = None 43 | 44 | @classmethod 45 | def dirs(cls): 46 | """Return the directories containing scripts, in priority order""" 47 | 48 | if not cls._dirs: 49 | dirs = ['~/.moodle-sdk'] 50 | if C.get('dirs.moodle') != None: 51 | dirs.insert(0, C.get('dirs.moodle')) 52 | 53 | dirs.append('/etc/moodle-sdk') 54 | 55 | # Directory within the package. 56 | # This can point anywhere when the package is installed, or to the folder containing the module when it is not. 57 | packageDir = os.path.join(os.path.dirname(__file__), 'scripts') 58 | dirs.append(os.path.split(packageDir)[0]) 59 | 60 | # Legacy: directory part of the root git repository, only if we can be sure that the parent directory is still MDK. 61 | if os.path.isfile(os.path.join(os.path.dirname(__file__), '..', 'mdk.py')): 62 | dirs.append(os.path.join(os.path.dirname(__file__), '..')) 63 | 64 | i = 0 65 | for d in dirs: 66 | dirs[i] = os.path.expanduser(os.path.join(d, 'scripts')) 67 | i += 1 68 | 69 | cls._dirs = dirs 70 | 71 | return cls._dirs 72 | 73 | @classmethod 74 | def list(cls): 75 | """Return a dict where keys are the name of the scripts 76 | and the value is the directory in which the script is stored""" 77 | 78 | if not cls._list: 79 | scripts = {} 80 | 81 | # Walk through the directories, in reverse to get the higher 82 | # priority last. 83 | dirs = cls.dirs() 84 | dirs.reverse() 85 | for d in dirs: 86 | 87 | if not os.path.isdir(d): 88 | continue 89 | 90 | # For each file found in the directory. 91 | l = os.listdir(d) 92 | for f in l: 93 | 94 | # Check if supported format. 95 | supported = False 96 | for ext in cls._supported: 97 | if f.endswith('.' + ext): 98 | supported = True 99 | break 100 | 101 | if supported: 102 | scripts[f] = d 103 | 104 | cls._list = scripts 105 | 106 | return cls._list 107 | 108 | @classmethod 109 | def find(cls, script): 110 | """Return the path to a script""" 111 | 112 | lst = cls.list() 113 | cli = None 114 | if script in list(lst.keys()): 115 | cli = os.path.join(lst[script], script) 116 | else: 117 | found = 0 118 | for ext in cls._supported: 119 | candidate = script + '.' + ext 120 | if candidate in list(lst.keys()): 121 | scriptFile = candidate 122 | found += 1 123 | 124 | if found > 1: 125 | raise ConflictInScriptName('The script name conflicts with other ones') 126 | elif found == 1: 127 | cli = os.path.join(lst[scriptFile], scriptFile) 128 | 129 | if not cli: 130 | raise ScriptNotFound('Script not found') 131 | 132 | return cli 133 | 134 | @classmethod 135 | def get_script_destination(cls, cli, path): 136 | """Get the final path where the script will be copied""" 137 | 138 | ext = os.path.splitext(cli)[1] 139 | 140 | i = 0 141 | while True: 142 | candidate = os.path.join(path, 'mdkscriptrun{}{}'.format(i if i > 0 else '', ext)) 143 | if not os.path.isfile(candidate): 144 | break 145 | i += 1 146 | 147 | return candidate 148 | 149 | @classmethod 150 | @contextmanager 151 | def prepare_script_in_path(cls, script, path, container=None): 152 | """Temporarily copy the script to a certain directory""" 153 | cli = cls.find(script) 154 | dest = cls.get_script_destination(cli, path) 155 | logging.debug('Copying %s to %s' % (cli, dest)) 156 | shutil.copyfile(cli, dest) 157 | if dest.endswith('.sh'): 158 | if container: 159 | container.chmod(Path(dest), stat.S_IRUSR | stat.S_IXUSR) 160 | else: 161 | os.chmod(dest, stat.S_IRUSR | stat.S_IXUSR) 162 | yield dest 163 | os.remove(dest) 164 | 165 | @classmethod 166 | def run(cls, script, path, arguments=None, cmdkwargs={}): 167 | """Executes a script at in a certain directory""" 168 | 169 | # Converts arguments to a string. 170 | arguments = '' if arguments == None else arguments 171 | if type(arguments) == list: 172 | arguments = ' '.join(arguments) 173 | arguments = ' ' + arguments 174 | 175 | with cls.prepare_script_in_path(script, path) as dest: 176 | if dest.endswith('.php'): 177 | cmd = '%s %s %s' % (C.get('php'), dest, arguments) 178 | result = process(cmd, cwd=path, **cmdkwargs) 179 | elif dest.endswith('.sh'): 180 | cmd = '%s %s' % (dest, arguments) 181 | result = process(cmd, cwd=path, **cmdkwargs) 182 | else: 183 | raise UnsupportedScript('Script not supported') 184 | 185 | return result[0] 186 | -------------------------------------------------------------------------------- /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/dev.php: -------------------------------------------------------------------------------- 1 | set_field('tool_usertours_tours', 'enabled', 0); 71 | 72 | // Adds moodle_database declaration to help VSCode detect moodle_database. 73 | $varmoodledb = '/** @var moodle_database */ 74 | $DB = isset($DB) ? $DB : null; 75 | '; 76 | $conffile = dirname(__FILE__) . '/config.php'; 77 | if ($content = file_get_contents($conffile)) { 78 | if (strpos($content, "@var moodle_database") === false) { 79 | if ($f = fopen($conffile, 'a')) { 80 | fputs($f, $varmoodledb); 81 | fclose($f); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /mdk/scripts/jsconfig.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * The jsconfig Configuration Generator for MDK and Moodle. 19 | * 20 | * The jsconfig file is used by the JavaScript LSP, which is used by vscode and other IDEs. 21 | * 22 | * @copyright 2022 Andrew Lyons 23 | */ 24 | class jsconfig { 25 | 26 | protected $config; 27 | 28 | protected $ignoreddirs = [ 29 | 'CVS' => true, 30 | '_vti_cnf' => true, 31 | 'amd' => true, 32 | 'classes' => true, 33 | 'db' => true, 34 | 'fonts' => true, 35 | 'lang' => true, 36 | 'pix' => true, 37 | 'simpletest' => true, 38 | 'templates' => true, 39 | 'tests' => true, 40 | 'yui' => true, 41 | ]; 42 | 43 | public function build(): void { 44 | if ($this->buildWithGrunt()) { 45 | echo "Built using Grunt task.\n"; 46 | } else { 47 | $this->generateConfiguration(); 48 | } 49 | } 50 | 51 | protected function buildWithGrunt(): bool { 52 | if (!file_exists(__DIR__ . '/.grunt/tasks/jsconfig.js')) { 53 | return false; 54 | } 55 | 56 | $command = "npx grunt jsconfig"; 57 | $result = null; 58 | exec($command, $output, $result); 59 | if ($result === 0) { 60 | return true; 61 | } 62 | 63 | echo "Error encountered whilst building.\n"; 64 | echo "Command: '{$command}'\n"; 65 | echo "Return code: {$result}\n"; 66 | echo "Error details follow:\n"; 67 | echo "======\n"; 68 | echo implode("\n", $output); 69 | echo "\n======\n\n"; 70 | 71 | return false; 72 | } 73 | 74 | protected function generateConfiguration(): void { 75 | $this->config = (object) [ 76 | 'compilerOptions' => (object) [ 77 | 'baseUrl' => '.', 78 | 'paths' => [ 79 | 'core/*' => ['lib/amd/src/*'], 80 | ], 81 | 'target' => 'es2015', 82 | 'allowSyntheticDefaultImports' => false, 83 | ], 84 | 'exclude' => [ 85 | 'node_modules', 86 | ], 87 | 'include' => [ 88 | 'lib/amd/src/**/*', 89 | ], 90 | ]; 91 | 92 | $this->loadComponents(); 93 | $this->processSubsystems(); 94 | $this->processPluginTypes((array) $this->componentList->plugintypes); 95 | 96 | ksort($this->config->compilerOptions->paths); 97 | sort($this->config->include); 98 | 99 | $this->writeConfiguration('jsconfig.json'); 100 | } 101 | 102 | protected function loadComponents(): void { 103 | $componentSrc = file_get_contents(__DIR__ . "/lib/components.json"); 104 | $this->componentList = json_decode($componentSrc); 105 | } 106 | 107 | protected function processSubsystems(): void { 108 | foreach ((array) $this->componentList->subsystems as $type => $path) { 109 | if ($path === null) { 110 | continue; 111 | } 112 | 113 | if (!empty($this->ignoreddirs[$type])) { 114 | continue; 115 | } 116 | 117 | $fulldir = "{$path}/amd/src"; 118 | $this->config->include[] = "{$fulldir}/**/*"; 119 | $this->config->compilerOptions->paths["core_{$type}/*"] = ["{$fulldir}/*"]; 120 | } 121 | } 122 | 123 | protected function processPluginTypes(array $plugintypes): void { 124 | foreach ($plugintypes as $type => $path) { 125 | if ($path === null) { 126 | continue; 127 | } 128 | 129 | $items = new \DirectoryIterator(__DIR__ . "/{$path}"); 130 | foreach ($items as $item) { 131 | if ($item->isDot() or !$item->isDir()) { 132 | continue; 133 | } 134 | 135 | $pluginname = $item->getFilename(); 136 | 137 | if (!$this->is_valid_plugin_name($type, $pluginname)) { 138 | continue; 139 | } 140 | 141 | $fulldir = "{$path}/{$pluginname}/amd/src"; 142 | $this->config->include[] = "{$fulldir}/**/*"; 143 | $this->config->compilerOptions->paths["{$type}_{$pluginname}/*"] = ["{$fulldir}/*"]; 144 | 145 | if (file_exists("{$path}/{$pluginname}/db/subplugins.json")) { 146 | $subplugins = json_decode(file_get_contents("{$path}/{$pluginname}/db/subplugins.json")); 147 | $this->processPluginTypes((array) $subplugins->plugintypes); 148 | } 149 | } 150 | } 151 | } 152 | 153 | protected function writeConfiguration(string $filepath): void { 154 | echo "Writing jsconfig configuration for jsconfig to {$filepath}\n"; 155 | $configuration = json_encode($this->config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 156 | file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . $filepath, $configuration . "\n"); 157 | $this->ensureGitIgnore($filepath); 158 | } 159 | 160 | protected function is_valid_plugin_name(string $plugintype, string $pluginname): bool { 161 | if ($plugintype === 'auth' and $pluginname === 'db') { 162 | // Special exception for this wrong plugin name. 163 | return true; 164 | } else if (!empty($this->ignoreddirs[$pluginname])) { 165 | return false; 166 | } 167 | 168 | if ($plugintype === 'mod') { 169 | // Modules must not have the same name as core subsystems. 170 | if (isset($this->componentList->subsystems->{$pluginname})) { 171 | return false; 172 | } 173 | 174 | // Modules MUST NOT have any underscores, 175 | // component normalisation would break very badly otherwise! 176 | return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname); 177 | 178 | } else { 179 | return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname); 180 | } 181 | } 182 | 183 | protected function ensureGitIgnore(string $filepath): void { 184 | $gitignorepath = __DIR__ . '/.git/info/exclude'; 185 | 186 | echo "Checking {$gitignorepath} for {$filepath}..."; 187 | $lines = explode("\n", file_get_contents($gitignorepath)); 188 | foreach ($lines as $line) { 189 | if ($line === $filepath) { 190 | // The file is already present. 191 | echo " already present.\n"; 192 | return; 193 | } 194 | } 195 | 196 | echo " Not found - adding.\n"; 197 | 198 | // File not present in the local gitignore. 199 | // Add it. 200 | $lines[] = '# Ignore the jsconfig.json file used by vscode (MDK).'; 201 | $lines[] = $filepath; 202 | $lines[] = ''; 203 | 204 | $content = implode("\n", $lines); 205 | 206 | file_put_contents($gitignorepath, $content); 207 | } 208 | } 209 | 210 | (new jsconfig())->build(); 211 | -------------------------------------------------------------------------------- /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/mincron.php: -------------------------------------------------------------------------------- 1 | get_in_or_equal($componentstodisable); 29 | $records = $DB->get_fieldset_select('task_scheduled', 'classname', "component $insql", $inparams); 30 | 31 | $tasks = array_merge($tasks, array_values($records)); 32 | sort($tasks); 33 | foreach ($tasks as $task) { 34 | mtrace('Disabling task ' . $task); 35 | $task = \core\task\manager::get_scheduled_task($task); 36 | $task->set_disabled(true); 37 | \core\task\manager::configure_scheduled_task($task); 38 | } 39 | -------------------------------------------------------------------------------- /mdk/scripts/mindev.php: -------------------------------------------------------------------------------- 1 | set_field('tool_usertours_tours', 'enabled', 0); 59 | 60 | // 61 | // Now we make sure that the performance-heavy related settings are disabled. 62 | // 63 | 64 | // Disable theme designer mode. 65 | mdk_set_config('themedesignermode', 0); 66 | 67 | // Cache JavaScript. 68 | mdk_set_config('cachejs', 0); 69 | 70 | // Use string caching. 71 | mdk_set_config('langstringcache', 0); 72 | 73 | // Use YUI combo loading. 74 | mdk_set_config('yuicomboloading', 1); 75 | 76 | // Don't cache templates. 77 | mdk_set_config('cachetemplates', 0); 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | // `themesettings` has been changed to `themesettingsadvanced` since 4.4. 67 | $settingspage = $adminroot->locate('themesettingsadvanced', true); 68 | if (empty($settingspage)) { 69 | // Fall back to `themesettings` for Moodle 4.3 and below. 70 | $settingspage = $adminroot->locate('themesettings', true); 71 | } 72 | $settings = $settingspage->settings; 73 | 74 | // Allow themes to be changed from the URL. 75 | $default = $settings->allowthemechangeonurl->get_defaultsetting(); 76 | mdk_set_config('allowthemechangeonurl', $default); 77 | 78 | // Enable designer mode. 79 | $default = $settings->themedesignermode->get_defaultsetting(); 80 | mdk_set_config('themedesignermode', $default); 81 | 82 | 83 | // Language settings. 84 | $settingspage = $adminroot->locate('langsettings', true); 85 | $settings = $settingspage->settings; 86 | 87 | // Restore core_string_manager application caching. 88 | $default = $settings->langstringcache->get_defaultsetting(); 89 | mdk_set_config('langstringcache', $default); 90 | 91 | 92 | // Javascript settings. 93 | $settingspage = $adminroot->locate('ajax', true); 94 | $settings = $settingspage->settings; 95 | 96 | // Do not cache JavaScript. 97 | $default = $settings->cachejs->get_defaultsetting(); 98 | mdk_set_config('cachejs', $default); 99 | 100 | // Do not use YUI combo loading. 101 | $default = $settings->yuicomboloading->get_defaultsetting(); 102 | mdk_set_config('yuicomboloading', $default); 103 | 104 | // Restore modintro for conciencious devs. 105 | $resources = array('book', 'folder', 'imscp', 'page', 'resource', 'url'); 106 | foreach ($resources as $r) { 107 | $settingpage = $adminroot->locate('modsetting' . $r, true); 108 | $settings = $settingpage->settings; 109 | if (isset($settings->requiremodintro)) { 110 | $default = $settings->requiremodintro->get_defaultsetting(); 111 | mdk_set_config('requiremodintro', $default, $r); 112 | } 113 | } 114 | 115 | // Cache templates. 116 | mdk_set_config('cachetemplates', 1); 117 | 118 | // Re-enabling user tours. 119 | $DB->set_field('tool_usertours_tours', 'enabled', 1); 120 | -------------------------------------------------------------------------------- /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/version.php: -------------------------------------------------------------------------------- 1 | libdir.'/clilib.php'); 6 | 7 | if (property_exists($CFG, 'root')) { 8 | require($CFG->root.'/version.php'); 9 | } else { 10 | require("$CFG->dirroot/version.php"); 11 | } 12 | 13 | cli_separator(); 14 | cli_heading('Resetting all version numbers'); 15 | 16 | $manager = core_plugin_manager::instance(); 17 | 18 | // Purge caches to make sure we have the fresh information about versions. 19 | $manager::reset_caches(); 20 | $configcache = cache::make('core', 'config'); 21 | $configcache->purge(); 22 | 23 | $plugininfo = $manager->get_plugins(); 24 | foreach ($plugininfo as $type => $plugins) { 25 | foreach ($plugins as $name => $plugin) { 26 | if ($plugin->get_status() !== core_plugin_manager::PLUGIN_STATUS_DOWNGRADE) { 27 | continue; 28 | } 29 | 30 | $frankenstyle = sprintf("%s_%s", $type, $name); 31 | 32 | mtrace("Updating {$frankenstyle} from {$plugin->versiondb} to {$plugin->versiondisk}"); 33 | $DB->set_field('config_plugins', 'value', $plugin->versiondisk, array('name' => 'version', 'plugin' => $frankenstyle)); 34 | } 35 | } 36 | 37 | // Check that the main version hasn't changed. 38 | if ((float) $CFG->version !== $version) { 39 | set_config('version', $version); 40 | mtrace("Updated main version from {$CFG->version} to {$version}"); 41 | } 42 | 43 | // Purge relevant caches again. 44 | $manager::reset_caches(); 45 | $configcache->purge(); 46 | -------------------------------------------------------------------------------- /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 | if (class_exists(\core\cron::class)) { 16 | \core\cron::setup_user(); 17 | } else { 18 | cron_setup_user(); 19 | } 20 | 21 | // Enable the Web Services. 22 | set_config('enablewebservices', 1); 23 | 24 | // Enable mobile web services. 25 | set_config('enablemobilewebservice', 1); 26 | 27 | // Enable Web Services documentation. 28 | set_config('enablewsdocumentation', 1); 29 | 30 | // Enable each protocol. 31 | set_config('webserviceprotocols', 'amf,rest,soap,xmlrpc,restful'); 32 | 33 | // Enable mobile service. 34 | $webservicemanager = new webservice(); 35 | $mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE); 36 | $mobileservice->enabled = 1; 37 | $webservicemanager->update_external_service($mobileservice); 38 | 39 | // Enable capability to use REST protocol. 40 | assign_capability('webservice/rest:use', CAP_ALLOW, $CFG->defaultuserroleid, SYSCONTEXTID, true); 41 | 42 | // Rename Web Service user that was created with test username, whoops. 43 | $legacyuser = $DB->get_record('user', ['username' => 'testtete']); 44 | if ($legacyuser) { 45 | $DB->update_record('user', ['id' => $legacyuser->id, 'username' => 'mdkwsuser']); 46 | } 47 | 48 | // Create the Web Service user. 49 | $user = $DB->get_record('user', ['username' => 'mdkwsuser']); 50 | if (!$user) { 51 | $user = new stdClass(); 52 | $user->username = 'mdkwsuser'; 53 | $user->firstname = 'Web'; 54 | $user->lastname = 'Service'; 55 | $user->password = 'test'; 56 | 57 | $dg = new testing_data_generator(); 58 | $user = $dg->create_user($user); 59 | } 60 | 61 | // Rename role that was create with test shortname. 62 | if ($legacyroleid = $DB->get_field('role', 'id', ['shortname' => 'testtete'])) { 63 | $DB->update_record('role', ['id' => $legacyroleid, 'shortname' => 'mdkwsrole']); 64 | } 65 | 66 | // Create a role for Web Services with all permissions. 67 | if (!$roleid = $DB->get_field('role', 'id', ['shortname' => 'mdkwsrole'])) { 68 | $roleid = create_role('MDK Web Service', 'mdkwsrole', 'MDK: All permissions given by default.', ''); 69 | } 70 | 71 | // Allow context levels. 72 | $context = context_system::instance(); 73 | set_role_contextlevels($roleid, array($context->contextlevel)); 74 | 75 | // Assign all permissions. 76 | if (method_exists($context, 'get_capabilities')) { 77 | $capabilities = $context->get_capabilities(); 78 | } else{ 79 | $capabilities = fetch_context_capabilities($context); 80 | } 81 | foreach ($capabilities as $capability) { 82 | assign_capability($capability->name, CAP_ALLOW, $roleid, $context->id, true); 83 | } 84 | 85 | // Allow role switches. 86 | $allows = get_default_role_archetype_allows('assign', 'manager'); 87 | 88 | foreach ($allows as $allowid) { 89 | if ($DB->record_exists('role_allow_assign', ['roleid' => $roleid, 'allowassign' => $allowid])) { 90 | continue; 91 | } 92 | core_role_set_assign_allowed($roleid, $allowid); 93 | } 94 | 95 | // Mark dirty. 96 | role_assign($roleid, $user->id, $context->id); 97 | $context->mark_dirty(); 98 | 99 | // Create a new service with all functions for the user. 100 | $webservicemanager = new webservice(); 101 | if (!$service = $DB->get_record('external_services', array('shortname' => 'mdk_all'))) { 102 | $service = new stdClass(); 103 | $service->name = 'MDK: All functions'; 104 | $service->shortname = 'mdk_all'; 105 | $service->enabled = 1; 106 | $service->restrictedusers = 1; 107 | $service->downloadfiles = 1; 108 | $service->uploadfiles = 1; 109 | $service->id = $webservicemanager->add_external_service($service); 110 | } 111 | $functions = $webservicemanager->get_not_associated_external_functions($service->id); 112 | foreach ($functions as $function) { 113 | $webservicemanager->add_external_function_to_service($function->name, $service->id); 114 | } 115 | if (!$webservicemanager->get_ws_authorised_user($service->id, $user->id)) { 116 | $adduser = new stdClass(); 117 | $adduser->externalserviceid = $service->id; 118 | $adduser->userid = $user->id; 119 | $webservicemanager->add_ws_authorised_user($adduser); 120 | } 121 | 122 | // Generate a token for the user. 123 | if (!$token = $DB->get_field('external_tokens', 'token', array('userid' => $user->id, 'externalserviceid' => $service->id))) { 124 | $token = external_generate_token(EXTERNAL_TOKEN_PERMANENT, $service->id, $user->id, $context, 0, ''); 125 | } 126 | mtrace('User \'webservice\' token: ' . $token); 127 | -------------------------------------------------------------------------------- /mdk/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Moodle Development Kit 5 | 6 | Copyright (c) 2012 Frédéric Massart - FMCorz.net 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | http://github.com/FMCorz/mdk 22 | """ 23 | 24 | __version__ = "2.1.3" 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyring>=3.5 2 | jenkinsapi>=0.3.2 3 | mysqlclient>=1.4.5 4 | psycopg2>=2.4.5 5 | requests>2.3.0 6 | watchdog>=0.8.0 7 | pyodbc>=4.0.21 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max_line_length = 132 3 | 4 | [yapf] 5 | based_on_style = pep8 6 | coalesce_brackets = true 7 | column_limit = 132 8 | dedent_closing_brackets = true 9 | indent_dictionary_value = true 10 | -------------------------------------------------------------------------------- /setup.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 | from setuptools import setup, find_packages 27 | 28 | # Load version number. 29 | from mdk.version import __version__ 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 :: 3.6', 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 | --------------------------------------------------------------------------------