Improved CSS command to watch directories
authorFrederic Massart <fred@moodle.com>
Tue, 18 Feb 2014 06:29:34 +0000 (14:29 +0800)
committerFrederic Massart <fred@moodle.com>
Tue, 18 Feb 2014 06:34:58 +0000 (14:34 +0800)
README.md
extra/bash_completion
lib/commands/css.py
lib/css.py
requirements.txt [new file with mode: 0644]

index 63706aa..1056bf0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -52,12 +52,18 @@ Manual installation
     cd /opt
     sudo git clone git://github.com/FMCorz/mdk.git moodle-sdk
 
-### 2. Make executable and accessible
+### 2. Install the dependencies
+
+You will need the tool [pip](http://www.pip-installer.org/en/latest/installing.html) to install the packages required by Python.
+
+    sudo pip install -r /opt/moodle-sdk/requirements.txt
+
+### 3. Make executable and accessible
 
     sudo chmod +x /opt/moodle-sdk/mdk.py
     sudo ln -s /opt/moodle-sdk/mdk.py /usr/local/bin/mdk
 
-### 3. Set up the basics
+### 4. Set up the basics
 
 Assuming that you are using Apache, which is set up to serve the files from /var/www, leave the default values as they are in `mdk init`, except for your remote and the database passwords.
 
@@ -65,7 +71,7 @@ Assuming that you are using Apache, which is set up to serve the files from /var
     sudo ln -s ~/www /var/www/m
     sudo mdk init
 
-### 4. Done
+### 5. Done
 
 Try the following command to create a typical Stable Master instance (this will take some time because the cache is still empty):
 
index 4a2cd6a..c0af8d6 100644 (file)
@@ -108,7 +108,7 @@ function _mdk() {
                 fi
                 ;;
             css)
-                OPTS="--compile"
+                OPTS="--compile --sheets --theme --watch"
                 ;;
             fix)
                 if [[ "$PREV" == "-n" || "$PREV" == "--name" ]]; then
index 41381f2..5cac58c 100644 (file)
@@ -23,6 +23,10 @@ http://github.com/FMCorz/mdk
 """
 
 import logging
+import os
+import time
+import watchdog.events
+import watchdog.observers
 from lib.command import Command
 from lib import css
 
@@ -39,6 +43,33 @@ class CssCommand(Command):
             }
         ),
         (
+            ['-s', '--sheets'],
+            {
+                'action': 'store',
+                'dest': 'sheets',
+                'default': None,
+                'help': 'the sheets to work on without their extensions. When not specified, it is guessed from the less folder.',
+                'nargs': '+'
+            }
+        ),
+        (
+            ['-t', '--theme'],
+            {
+                'action': 'store',
+                'dest': 'theme',
+                'default': None,
+                'help': 'the theme to work on. The default is \'bootstrapbase\' but is ignored if we are in a theme folder.',
+            }
+        ),
+        (
+            ['-w', '--watch'],
+            {
+                'action': 'store_true',
+                'dest': 'watch',
+                'help': 'watch the directory'
+            }
+        ),
+        (
             ['names'],
             {
                 'default': None,
@@ -56,7 +87,81 @@ class CssCommand(Command):
         if len(Mlist) < 1:
             raise Exception('No instances to work on. Exiting...')
 
+        # Resolve the theme folder we are in.
+        if not args.theme:
+            mpath = os.path.join(Mlist[0].get('path'), 'theme')
+            cwd = os.path.realpath(os.path.abspath(os.getcwd()))
+            if cwd.startswith(mpath):
+                candidate = cwd.replace(mpath, '').strip('/')
+                while True:
+                    (head, tail) = os.path.split(candidate)
+                    if not head and tail:
+                        # Found the theme.
+                        args.theme = tail
+                        logging.info('You are in the theme \'%s\', using that.' % (args.theme))
+                        break
+                    elif not head and not tail:
+                        # Nothing, let's leave.
+                        break
+                    candidate = head
+
+        # We have not found anything, falling back on the default.
+        if not args.theme:
+            args.theme = 'bootstrapbase'
+
         for M in Mlist:
             if args.compile:
+                logging.info('Compiling theme \'%s\' on %s' % (args.theme, M.get('identifier')))
+                processor = css.Css(M)
+                processor.compile(theme=args.theme, sheets=args.sheets)
+
+        # Setting up watchdog. This code should be improved when we will have more than a compile option.
+        observer = None
+        if args.compile and args.watch:
+            observer = watchdog.observers.Observer()
+
+        for M in Mlist:
+            if args.watch and args.compile:
                 processor = css.Css(M)
-                processor.compile()
+                processorArgs = {'theme': args.theme, 'sheets': args.sheets}
+                handler = LessWatcher(M, processor, processorArgs)
+                observer.schedule(handler, processor.getThemeLessPath(args.theme), recursive=True)
+                logging.info('Watchdog set up on %s/%s, waiting for changes...' % (M.get('identifier'), args.theme))
+
+        if observer and args.compile and args.watch:
+            observer.start()
+
+            try:
+                while True:
+                    time.sleep(1)
+            except KeyboardInterrupt:
+                observer.stop()
+            finally:
+                observer.join()
+
+
+class LessWatcher(watchdog.events.FileSystemEventHandler):
+
+    _processor = None
+    _args = None
+    _ext = '.less'
+    _M = None
+
+    def __init__(self, M, processor, args):
+        super(self.__class__, self).__init__()
+        self._M = M
+        self._processor = processor
+        self._args = args
+
+    def on_modified(self, event):
+        self.process(event)
+
+    def process(self, event):
+        if event.is_directory:
+            return
+        elif not event.src_path.endswith(self._ext):
+            return
+
+        filename = event.src_path.replace(self._processor.getThemeLessPath(self._args['theme']), '').strip('/')
+        logging.info('[%s] Changes detected in %s!' % (self._M.get('identifier'), filename))
+        self._processor.compile(**self._args)
index ad92a40..9bf57e7 100644 (file)
@@ -38,15 +38,27 @@ class Css(object):
     def __init__(self, M):
         self._M = M
 
-    def compile(self, theme='bootstrapbase', sheets=['moodle', 'editor']):
+    def compile(self, theme='bootstrapbase', sheets=None):
         """Compile LESS sheets contained within a theme"""
 
-        cwd = os.path.join(self._M.get('path'), 'theme', theme, 'less')
-        if not os.path.isdir(cwd):
+        source = self.getThemeLessPath(theme)
+        dest = self.getThemeCssPath(theme)
+        if not os.path.isdir(source):
             raise Exception('Unknown theme %s, or less directory not found' % (theme))
 
-        source = os.path.join(self._M.get('path'), 'theme', theme, 'less')
-        dest = os.path.join(self._M.get('path'), 'theme', theme, 'style')
+        if not sheets:
+            # Guess the sheets from the theme less folder.
+            sheets = []
+            for candidate in os.listdir(source):
+                if os.path.isfile(os.path.join(source, candidate)) and candidate.endswith('.less'):
+                    sheets.append(os.path.splitext(candidate)[0])
+        elif type(sheets) != list:
+            sheets = [sheets]
+
+        if len(sheets) < 1:
+            logging.warning('Could not find any sheets')
+            return False
+
         hadErrors = False
 
         for name in sheets:
@@ -59,17 +71,26 @@ class Css(object):
                 continue
 
             try:
-                compiler = Recess(cwd, os.path.join(source, sheet), os.path.join(dest, destSheet))
+                compiler = Recess(source, os.path.join(source, sheet), os.path.join(dest, destSheet))
                 compiler.execute()
             except CssCompileFailed:
                 logging.warning('Failed compilation of %s' % (sheet))
                 hadErrors = True
                 continue
             else:
-                logging.info('Compiled %s' % (sheet))
+                logging.info('Compiled %s to %s' % (sheet, destSheet))
 
         return not hadErrors
 
+    def getThemeCssPath(self, theme):
+        return os.path.join(self.getThemePath(theme), 'style')
+
+    def getThemeLessPath(self, theme):
+        return os.path.join(self.getThemePath(theme), 'less')
+
+    def getThemePath(self, theme):
+        return os.path.join(self._M.get('path'), 'theme', theme)
+
 
 class Compiler(object):
     """LESS compiler abstract"""
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..350bf3e
--- /dev/null
@@ -0,0 +1,2 @@
+keyring
+watchdog