├── .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 |
--------------------------------------------------------------------------------