diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..78cafe90 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,19 @@ +*.pyc +*.swp +.environment_version +.selenium_log +.coverage* +.noseids +.venv +coverage.xml +pep8.txt +pylint.txt +reports +tabula/local/local_settings.py +/static/ +docs/build/ +docs/source/sourcecode +build +dist +#Autogenerated Documentation +doc/source/api \ No newline at end of file diff --git a/dashboard/README.rst b/dashboard/README.rst new file mode 100644 index 00000000..150c1b4a --- /dev/null +++ b/dashboard/README.rst @@ -0,0 +1,7 @@ +Keero Tabula README +===================== +Tabula is a project that provides Web UI to Keero Project. + +SEE ALSO +-------- +* `Keero `__ diff --git a/dashboard/bin/less/lessc b/dashboard/bin/less/lessc new file mode 100755 index 00000000..30ae3520 --- /dev/null +++ b/dashboard/bin/less/lessc @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +var path = require('path'), + fs = require('fs'), + sys = require('util'), + os = require('os'); + +var less = require('../lib/less'); +var args = process.argv.slice(1); +var options = { + compress: false, + yuicompress: false, + optimization: 1, + silent: false, + paths: [], + color: true, + strictImports: false +}; + +args = args.filter(function (arg) { + var match; + + if (match = arg.match(/^-I(.+)$/)) { + options.paths.push(match[1]); + return false; + } + + if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i)) { arg = match[1] } + else { return arg } + + switch (arg) { + case 'v': + case 'version': + sys.puts("lessc " + less.version.join('.') + " (LESS Compiler) [JavaScript]"); + process.exit(0); + case 'verbose': + options.verbose = true; + break; + case 's': + case 'silent': + options.silent = true; + break; + case 'strict-imports': + options.strictImports = true; + break; + case 'h': + case 'help': + sys.puts("usage: lessc source [destination]"); + process.exit(0); + case 'x': + case 'compress': + options.compress = true; + break; + case 'yui-compress': + options.yuicompress = true; + break; + case 'no-color': + options.color = false; + break; + case 'include-path': + options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':') + .map(function(p) { + if (p) { + return path.resolve(process.cwd(), p); + } + }); + break; + case 'O0': options.optimization = 0; break; + case 'O1': options.optimization = 1; break; + case 'O2': options.optimization = 2; break; + } +}); + +var input = args[1]; +if (input && input != '-') { + input = path.resolve(process.cwd(), input); +} +var output = args[2]; +if (output) { + output = path.resolve(process.cwd(), output); +} + +var css, fd, tree; + +if (! input) { + sys.puts("lessc: no input files"); + process.exit(1); +} + +var parseLessFile = function (e, data) { + if (e) { + sys.puts("lessc: " + e.message); + process.exit(1); + } + + new(less.Parser)({ + paths: [path.dirname(input)].concat(options.paths), + optimization: options.optimization, + filename: input, + strictImports: options.strictImports + }).parse(data, function (err, tree) { + if (err) { + less.writeError(err, options); + process.exit(1); + } else { + try { + css = tree.toCSS({ + compress: options.compress, + yuicompress: options.yuicompress + }); + if (output) { + fd = fs.openSync(output, "w"); + fs.writeSync(fd, css, 0, "utf8"); + } else { + sys.print(css); + } + } catch (e) { + less.writeError(e, options); + process.exit(2); + } + } + }); +}; + +if (input != '-') { + fs.readFile(input, 'utf-8', parseLessFile); +} else { + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + var buffer = ''; + process.stdin.on('data', function(data) { + buffer += data; + }); + + process.stdin.on('end', function() { + parseLessFile(false, buffer); + }); +} diff --git a/dashboard/bin/lib/less/browser.js b/dashboard/bin/lib/less/browser.js new file mode 100644 index 00000000..cab913be --- /dev/null +++ b/dashboard/bin/lib/less/browser.js @@ -0,0 +1,380 @@ +// +// browser.js - client-side engine +// + +var isFileProtocol = (location.protocol === 'file:' || + location.protocol === 'chrome:' || + location.protocol === 'chrome-extension:' || + location.protocol === 'resource:'); + +less.env = less.env || (location.hostname == '127.0.0.1' || + location.hostname == '0.0.0.0' || + location.hostname == 'localhost' || + location.port.length > 0 || + isFileProtocol ? 'development' + : 'production'); + +// Load styles asynchronously (default: false) +// +// This is set to `false` by default, so that the body +// doesn't start loading before the stylesheets are parsed. +// Setting this to `true` can result in flickering. +// +less.async = false; + +// Interval between watch polls +less.poll = less.poll || (isFileProtocol ? 1000 : 1500); + +// +// Watch mode +// +less.watch = function () { return this.watchMode = true }; +less.unwatch = function () { return this.watchMode = false }; + +if (less.env === 'development') { + less.optimization = 0; + + if (/!watch/.test(location.hash)) { + less.watch(); + } + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (root) { + createCSS(root.toCSS(), sheet, env.lastModified); + } + }); + } + }, less.poll); +} else { + less.optimization = 3; +} + +var cache; + +try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; +} catch (_) { + cache = null; +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); +var typePattern = /^text\/(x-)?less$/; + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + + +less.refresh = function (reload) { + var startTime, endTime; + startTime = endTime = new(Date); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (env.local) { + log("loading " + sheet.href + " from cache."); + } else { + log("parsed " + sheet.href + " successfully."); + createCSS(root.toCSS(), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); + endTime = new(Date); + }, reload); + + loadStyles(); +}; +less.refreshStyles = loadStyles; + +less.refresh(less.env === 'development'); + +function loadStyles() { + var styles = document.getElementsByTagName('style'); + for (var i = 0; i < styles.length; i++) { + if (styles[i].type.match(typePattern)) { + new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) { + var css = tree.toCSS(); + var style = styles[i]; + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }); + } + } +} + +function loadStyleSheets(callback, reload) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); + } +} + +function loadStyleSheet(sheet, callback, reload, remaining) { + var url = window.location.href.replace(/[#?].*$/, ''); + var href = sheet.href.replace(/\?.*$/, ''); + var css = cache && cache.getItem(href); + var timestamp = cache && cache.getItem(href + ':timestamp'); + var styles = { css: css, timestamp: timestamp }; + + // Stylesheets in IE don't always return the full path + if (! /^(https?|file):/.test(href)) { + if (href.charAt(0) == "/") { + href = window.location.protocol + "//" + window.location.host + href; + } else { + href = url.slice(0, url.lastIndexOf('/') + 1) + href; + } + } + var filename = href.match(/([^\/]+)$/)[1]; + + xhr(sheet.href, sheet.type, function (data, lastModified) { + if (!reload && styles && lastModified && + (new(Date)(lastModified).valueOf() === + new(Date)(styles.timestamp).valueOf())) { + // Use local copy + createCSS(styles.css, sheet); + callback(null, null, data, sheet, { local: true, remaining: remaining }); + } else { + // Use remote copy (re-parse) + try { + new(less.Parser)({ + optimization: less.optimization, + paths: [href.replace(/[\w\.-]+$/, '')], + mime: sheet.type, + filename: filename + }).parse(data, function (e, root) { + if (e) { return error(e, href) } + try { + callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }); + removeNode(document.getElementById('less-error-message:' + extractId(href))); + } catch (e) { + error(e, href); + } + }); + } catch (e) { + error(e, href); + } + } + }, function (status, url) { + throw new(Error)("Couldn't load " + url + " (" + status + ")"); + }); +} + +function extractId(href) { + return href.replace(/^[a-z]+:\/\/?[^\/]+/, '' ) // Remove protocol & domain + .replace(/^\//, '' ) // Remove root / + .replace(/\?.*$/, '' ) // Remove query + .replace(/\.[^\.\/]+$/, '' ) // Remove file extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function createCSS(styles, sheet, lastModified) { + var css; + + // Strip the query-string + var href = sheet.href ? sheet.href.replace(/\?.*$/, '') : ''; + + // If there is no title set, use the filename, minus the extension + var id = 'less:' + (sheet.title || extractId(href)); + + // If the stylesheet doesn't exist, create a new node + if ((css = document.getElementById(id)) === null) { + css = document.createElement('style'); + css.type = 'text/css'; + css.media = sheet.media || 'screen'; + css.id = id; + document.getElementsByTagName('head')[0].appendChild(css); + } + + if (css.styleSheet) { // IE + try { + css.styleSheet.cssText = styles; + } catch (e) { + throw new(Error)("Couldn't reassign styleSheet.cssText."); + } + } else { + (function (node) { + if (css.childNodes.length > 0) { + if (css.firstChild.nodeValue !== node.nodeValue) { + css.replaceChild(node, css.firstChild); + } + } else { + css.appendChild(node); + } + })(document.createTextNode(styles)); + } + + // Don't update the local store if the file wasn't modified + if (lastModified && cache) { + log('saving ' + href + ' to cache.'); + cache.setItem(href, styles); + cache.setItem(href + ':timestamp', lastModified); + } +} + +function xhr(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? false : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + if (isFileProtocol) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new(XMLHttpRequest); + } else { + try { + return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX."); + return null; + } + } +} + +function removeNode(node) { + return node && node.parentNode.removeChild(node); +} + +function log(str) { + if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } +} + +function error(e, href) { + var id = 'less-error-message:' + extractId(href); + var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, error = []; + var filename = e.filename || href; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filename + " "; + + var errorline = function (e, i, classname) { + if (e.extract[i]) { + error.push(template.replace(/\{line\}/, parseInt(e.line) + (i - 1)) + .replace(/\{class\}/, classname) + .replace(/\{content\}/, e.extract[i])); + } + }; + + if (e.stack) { + content += '
    ' + e.stack.split('\n').slice(1).join('
    '); + } else if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + + ''; + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} + diff --git a/dashboard/bin/lib/less/colors.js b/dashboard/bin/lib/less/colors.js new file mode 100644 index 00000000..ed4c2838 --- /dev/null +++ b/dashboard/bin/lib/less/colors.js @@ -0,0 +1,152 @@ +(function (tree) { + tree.colors = { + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgrey':'#a9a9a9', + 'darkgreen':'#006400', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgrey':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370d8', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#d87093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + 'transparent':'rgba(0,0,0,0)', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' + }; +})(require('./tree')); diff --git a/dashboard/bin/lib/less/cssmin.js b/dashboard/bin/lib/less/cssmin.js new file mode 100644 index 00000000..427de71c --- /dev/null +++ b/dashboard/bin/lib/less/cssmin.js @@ -0,0 +1,355 @@ +/** + * cssmin.js + * Author: Stoyan Stefanov - http://phpied.com/ + * This is a JavaScript port of the CSS minification tool + * distributed with YUICompressor, itself a port + * of the cssmin utility by Isaac Schlueter - http://foohack.com/ + * Permission is hereby granted to use the JavaScript version under the same + * conditions as the YUICompressor (original YUICompressor note below). + */ + +/* +* YUI Compressor +* http://developer.yahoo.com/yui/compressor/ +* Author: Julien Lecomte - http://www.julienlecomte.net/ +* Copyright (c) 2011 Yahoo! Inc. All rights reserved. +* The copyrights embodied in the content of this file are licensed +* by Yahoo! Inc. under the BSD (revised) open source license. +*/ +var YAHOO = YAHOO || {}; +YAHOO.compressor = YAHOO.compressor || {}; + +/** + * Utility method to replace all data urls with tokens before we start + * compressing, to avoid performance issues running some of the subsequent + * regexes against large strings chunks. + * + * @private + * @method _extractDataUrls + * @param {String} css The input css + * @param {Array} The global array of tokens to preserve + * @returns String The processed css + */ +YAHOO.compressor._extractDataUrls = function (css, preservedTokens) { + + // Leave data urls alone to increase parse performance. + var maxIndex = css.length - 1, + appendIndex = 0, + startIndex, + endIndex, + terminator, + foundTerminator, + sb = [], + m, + preserver, + token, + pattern = /url\(\s*(["']?)data\:/g; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + + while ((m = pattern.exec(css)) !== null) { + + startIndex = m.index + 4; // "url(".length() + terminator = m[1]; // ', " or empty (not quoted) + + if (terminator.length === 0) { + terminator = ")"; + } + + foundTerminator = false; + + endIndex = pattern.lastIndex - 1; + + while(foundTerminator === false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex + 1); + + // endIndex == 0 doesn't really apply here + if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) { + foundTerminator = true; + if (")" != terminator) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.push(css.substring(appendIndex, m.index)); + + if (foundTerminator) { + token = css.substring(startIndex, endIndex); + token = token.replace(/\s+/g, ""); + preservedTokens.push(token); + + preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)"; + sb.push(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.push(css.substring(m.index, pattern.lastIndex)); + appendIndex = pattern.lastIndex; + } + } + + sb.push(css.substring(appendIndex)); + + return sb.join(""); +}; + +/** + * Utility method to compress hex color values of the form #AABBCC to #ABC. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @private + * @method _compressHexColors + * @param {String} css The input css + * @returns String The processed css + */ +YAHOO.compressor._compressHexColors = function(css) { + + // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) + var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi, + m, + index = 0, + isFilter, + sb = []; + + while ((m = pattern.exec(css)) !== null) { + + sb.push(css.substring(index, m.index)); + + isFilter = m[1]; + + if (isFilter) { + // Restore, maintain case, otherwise filter will break + sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7])); + } else { + if (m[2].toLowerCase() == m[3].toLowerCase() && + m[4].toLowerCase() == m[5].toLowerCase() && + m[6].toLowerCase() == m[7].toLowerCase()) { + + // Compress. + sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase()); + } else { + // Non compressible color, restore but lower case. + sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase()); + } + } + + index = pattern.lastIndex = pattern.lastIndex - m[8].length; + } + + sb.push(css.substring(index)); + + return sb.join(""); +}; + +YAHOO.compressor.cssmin = function (css, linebreakpos) { + + var startIndex = 0, + endIndex = 0, + i = 0, max = 0, + preservedTokens = [], + comments = [], + token = '', + totallen = css.length, + placeholder = ''; + + css = this._extractDataUrls(css, preservedTokens); + + // collect all comment blocks... + while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { + endIndex = css.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + token = css.slice(startIndex + 2, endIndex); + comments.push(token); + css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); + startIndex += 2; + } + + // preserve strings so their content doesn't get accidentally minified + css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { + var i, max, quote = match.substring(0, 1); + + match = match.slice(1, -1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.length; i < max; i = i + 1) { + match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); + } + } + + // minify alpha opacity in filter strings + match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + preservedTokens.push(match); + return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; + }); + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.length; i < max; i = i + 1) { + + token = comments[i]; + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (token.charAt(0) === "!") { + preservedTokens.push(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.charAt(token.length - 1) === "\\") { + preservedTokens.push("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.push(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length === 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) === '>') { + preservedTokens.push(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replace(/\s+/g, " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) { + return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + }); + css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); + css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); + + // retain space for special IE6 cases + css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replace(/\*\/ /g, '*/'); + + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); + css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replace(/\band\(/gi, "and ("); + + + // Remove the spaces after the things that should not have spaces after them. + css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); + + // remove unnecessary semicolons + css = css.replace(/;+\}/g, "}"); + + // Replace 0(px,em,%) with 0. + css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replace(/:0 0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0(;|\})/g, ":0$1"); + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0 0" + tail; + }); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () { + var i, rgbcolors = arguments[1].split(','); + for (i = 0; i < rgbcolors.length; i = i + 1) { + rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); + if (rgbcolors[i].length === 1) { + rgbcolors[i] = '0' + rgbcolors[i]; + } + } + return '#' + rgbcolors.join(''); + }); + + // Shorten colors from #AABBCC to #ABC. + css = this._compressHexColors(css); + + // border: none -> border:0 + css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0" + tail; + }); + + // shorter opacity IE filter + css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + // Remove empty rules. + css = css.replace(/[^\};\{\/]+\{\}/g, ""); + + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + startIndex = 0; + i = 0; + while (i < css.length) { + i = i + 1; + if (css[i - 1] === '}' && i - startIndex > linebreakpos) { + css = css.slice(0, i) + '\n' + css.slice(i); + startIndex = i; + } + } + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replace(/;;+/g, ";"); + + // restore preserved comments and strings + for (i = 0, max = preservedTokens.length; i < max; i = i + 1) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.replace(/^\s+|\s+$/g, ""); + + return css; + +}; + +exports.compressor = YAHOO.compressor; diff --git a/dashboard/bin/lib/less/functions.js b/dashboard/bin/lib/less/functions.js new file mode 100644 index 00000000..6eb34bac --- /dev/null +++ b/dashboard/bin/lib/less/functions.js @@ -0,0 +1,228 @@ +(function (tree) { + +tree.functions = { + rgb: function (r, g, b) { + return this.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return number(c) }), + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return this.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + h = (number(h) % 360) / 360; + s = number(s); l = number(l); a = number(a); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return this.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + + function hue(h) { + h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + else if (h * 2 < 1) return m2; + else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; + else return m1; + } + }, + hue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().h)); + }, + saturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + saturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return this.desaturate(color, new(tree.Dimension)(100)); + }, + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + '%': function (quoted /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + str = quoted.value; + + for (var i = 0; i < args.length; i++) { + str = str.replace(/%[sda]/i, function(token) { + var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); + return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; + }); + } + str = str.replace(/%%/g, '%'); + return new(tree.Quoted)('"' + str + '"', str); + }, + round: function (n) { + return this._math('round', n); + }, + ceil: function (n) { + return this._math('ceil', n); + }, + floor: function (n) { + return this._math('floor', n); + }, + _math: function (fn, n) { + if (n instanceof tree.Dimension) { + return new(tree.Dimension)(Math[fn](number(n)), n.unit); + } else if (typeof(n) === 'number') { + return Math[fn](n); + } else { + throw { type: "Argument", message: "argument must be a number" }; + } + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + }, + color: function (n) { + if (n instanceof tree.Quoted) { + return new(tree.Color)(n.value.slice(1)); + } else { + throw { type: "Argument", message: "argument must be a string" }; + } + }, + iscolor: function (n) { + return this._isa(n, tree.Color); + }, + isnumber: function (n) { + return this._isa(n, tree.Dimension); + }, + isstring: function (n) { + return this._isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return this._isa(n, tree.Keyword); + }, + isurl: function (n) { + return this._isa(n, tree.URL); + }, + ispixel: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'px' ? tree.True : tree.False; + }, + ispercentage: function (n) { + return (n instanceof tree.Dimension) && n.unit === '%' ? tree.True : tree.False; + }, + isem: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'em' ? tree.True : tree.False; + }, + _isa: function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + } +}; + +function hsla(hsla) { + return tree.functions.hsla(hsla.h, hsla.s, hsla.l, hsla.a); +} + +function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit == '%' ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } +} + +function clamp(val) { + return Math.min(1, Math.max(0, val)); +} + +})(require('./tree')); diff --git a/dashboard/bin/lib/less/index.js b/dashboard/bin/lib/less/index.js new file mode 100644 index 00000000..a11fa998 --- /dev/null +++ b/dashboard/bin/lib/less/index.js @@ -0,0 +1,148 @@ +var path = require('path'), + sys = require('util'), + fs = require('fs'); + +var less = { + version: [1, 3, 0], + Parser: require('./parser').Parser, + importer: require('./parser').importer, + tree: require('./tree'), + render: function (input, options, callback) { + options = options || {}; + + if (typeof(options) === 'function') { + callback = options, options = {}; + } + + var parser = new(less.Parser)(options), + ee; + + if (callback) { + parser.parse(input, function (e, root) { + callback(e, root && root.toCSS && root.toCSS(options)); + }); + } else { + ee = new(require('events').EventEmitter); + + process.nextTick(function () { + parser.parse(input, function (e, root) { + if (e) { ee.emit('error', e) } + else { ee.emit('success', root.toCSS(options)) } + }); + }); + return ee; + } + }, + writeError: function (ctx, options) { + options = options || {}; + + var message = ""; + var extract = ctx.extract; + var error = []; + var stylize = options.color ? less.stylize : function (str) { return str }; + + if (options.silent) { return } + + if (ctx.stack) { return sys.error(stylize(ctx.stack, 'red')) } + + if (!ctx.hasOwnProperty('index')) { + return sys.error(ctx.stack || ctx.message); + } + + if (typeof(extract[0]) === 'string') { + error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey')); + } + + if (extract[1]) { + error.push(ctx.line + ' ' + extract[1].slice(0, ctx.column) + + stylize(stylize(stylize(extract[1][ctx.column], 'bold') + + extract[1].slice(ctx.column + 1), 'red'), 'inverse')); + } + + if (typeof(extract[2]) === 'string') { + error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey')); + } + error = error.join('\n') + '\033[0m\n'; + + message += stylize(ctx.type + 'Error: ' + ctx.message, 'red'); + ctx.filename && (message += stylize(' in ', 'red') + ctx.filename + + stylize(':' + ctx.line + ':' + ctx.column, 'grey')); + + sys.error(message, error); + + if (ctx.callLine) { + sys.error(stylize('from ', 'red') + (ctx.filename || '')); + sys.error(stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract); + } + } +}; + +['color', 'directive', 'operation', 'dimension', + 'keyword', 'variable', 'ruleset', 'element', + 'selector', 'quoted', 'expression', 'rule', + 'call', 'url', 'alpha', 'import', + 'mixin', 'comment', 'anonymous', 'value', + 'javascript', 'assignment', 'condition', 'paren', + 'media' +].forEach(function (n) { + require('./tree/' + n); +}); + +less.Parser.importer = function (file, paths, callback, env) { + var pathname; + + // TODO: Undo this at some point, + // or use different approach. + paths.unshift('.'); + + for (var i = 0; i < paths.length; i++) { + try { + pathname = path.join(paths[i], file); + fs.statSync(pathname); + break; + } catch (e) { + pathname = null; + } + } + + if (pathname) { + fs.readFile(pathname, 'utf-8', function(e, data) { + if (e) return callback(e); + + new(less.Parser)({ + paths: [path.dirname(pathname)].concat(paths), + filename: pathname + }).parse(data, function (e, root) { + callback(e, root, data); + }); + }); + } else { + if (typeof(env.errback) === "function") { + env.errback(file, paths, callback); + } else { + callback({ type: 'File', message: "'" + file + "' wasn't found.\n" }); + } + } +} + +require('./functions'); +require('./colors'); + +for (var k in less) { exports[k] = less[k] } + +// Stylize a string +function stylize(str, style) { + var styles = { + 'bold' : [1, 22], + 'inverse' : [7, 27], + 'underline' : [4, 24], + 'yellow' : [33, 39], + 'green' : [32, 39], + 'red' : [31, 39], + 'grey' : [90, 39] + }; + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; +} +less.stylize = stylize; + diff --git a/dashboard/bin/lib/less/parser.js b/dashboard/bin/lib/less/parser.js new file mode 100644 index 00000000..d732e1b1 --- /dev/null +++ b/dashboard/bin/lib/less/parser.js @@ -0,0 +1,1334 @@ +var less, tree; + +if (typeof environment === "object" && ({}).toString.call(environment) === "[object Environment]") { + // Rhino + // Details on how to detect Rhino: https://github.com/ringo/ringojs/issues/88 + if (typeof(window) === 'undefined') { less = {} } + else { less = window.less = {} } + tree = less.tree = {}; + less.mode = 'rhino'; +} else if (typeof(window) === 'undefined') { + // Node.js + less = exports, + tree = require('./tree'); + less.mode = 'node'; +} else { + // Browser + if (typeof(window.less) === 'undefined') { window.less = {} } + less = window.less, + tree = window.less.tree = {}; + less.mode = 'browser'; +} +// +// less.js - parser +// +// A relatively straight-forward predictive parser. +// There is no tokenization/lexing stage, the input is parsed +// in one sweep. +// +// To make the parser fast enough to run in the browser, several +// optimization had to be made: +// +// - Matching and slicing on a huge input is often cause of slowdowns. +// The solution is to chunkify the input into smaller strings. +// The chunks are stored in the `chunks` var, +// `j` holds the current chunk index, and `current` holds +// the index of the current chunk in relation to `input`. +// This gives us an almost 4x speed-up. +// +// - In many cases, we don't need to match individual tokens; +// for example, if a value doesn't hold any variables, operations +// or dynamic references, the parser can effectively 'skip' it, +// treating it as a literal. +// An example would be '1px solid #000' - which evaluates to itself, +// we don't need to know what the individual components are. +// The drawback, of course is that you don't get the benefits of +// syntax-checking on the CSS. This gives us a 50% speed-up in the parser, +// and a smaller speed-up in the code-gen. +// +// +// Token matching is done with the `$` function, which either takes +// a terminal string or regexp, or a non-terminal function to call. +// It also takes care of moving all the indices forwards. +// +// +less.Parser = function Parser(env) { + var input, // LeSS input string + i, // current index in `input` + j, // current chunk + temp, // temporarily holds a chunk's state, for backtracking + memo, // temporarily holds `i`, when backtracking + furthest, // furthest index the parser has gone to + chunks, // chunkified input + current, // index of current chunk, in `input` + parser; + + var that = this; + + // This function is called after all files + // have been imported through `@import`. + var finish = function () {}; + + var imports = this.imports = { + paths: env && env.paths || [], // Search paths, when importing + queue: [], // Files which haven't been imported yet + files: {}, // Holds the imported parse trees + contents: {}, // Holds the imported file contents + mime: env && env.mime, // MIME type of .less files + error: null, // Error in parsing/evaluating an import + push: function (path, callback) { + var that = this; + this.queue.push(path); + + // + // Import a file asynchronously + // + less.Parser.importer(path, this.paths, function (e, root, contents) { + that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue + + var imported = path in that.files; + + that.files[path] = root; // Store the root + that.contents[path] = contents; + + if (e && !that.error) { that.error = e } + + callback(e, root, imported); + + if (that.queue.length === 0) { finish() } // Call `finish` if we're done importing + }, env); + } + }; + + function save() { temp = chunks[j], memo = i, current = i } + function restore() { chunks[j] = temp, i = memo, current = i } + + function sync() { + if (i > current) { + chunks[j] = chunks[j].slice(i - current); + current = i; + } + } + // + // Parse from a token, regexp or string, and move forward if match + // + function $(tok) { + var match, args, length, c, index, endIndex, k, mem; + + // + // Non-terminal + // + if (tok instanceof Function) { + return tok.call(parser.parsers); + // + // Terminal + // + // Either match a single character in the input, + // or match a regexp in the current chunk (chunk[j]). + // + } else if (typeof(tok) === 'string') { + match = input.charAt(i) === tok ? tok : null; + length = 1; + sync (); + } else { + sync (); + + if (match = tok.exec(chunks[j])) { + length = match[0].length; + } else { + return null; + } + } + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + if (match) { + mem = i += length; + endIndex = i + chunks[j].length - length; + + while (i < endIndex) { + c = input.charCodeAt(i); + if (! (c === 32 || c === 10 || c === 9)) { break } + i++; + } + chunks[j] = chunks[j].slice(length + (i - mem)); + current = i; + + if (chunks[j].length === 0 && j < chunks.length - 1) { j++ } + + if(typeof(match) === 'string') { + return match; + } else { + return match.length === 1 ? match[0] : match; + } + } + } + + function expect(arg, msg) { + var result = $(arg); + if (! result) { + error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" + : "unexpected token")); + } else { + return result; + } + } + + function error(msg, type) { + throw { index: i, type: type || 'Syntax', message: msg }; + } + + // Same as $(), but don't change the state of the parser, + // just return the match. + function peek(tok) { + if (typeof(tok) === 'string') { + return input.charAt(i) === tok; + } else { + if (tok.test(chunks[j])) { + return true; + } else { + return false; + } + } + } + + function basename(pathname) { + if (less.mode === 'node') { + return require('path').basename(pathname); + } else { + return pathname.match(/[^\/]+$/)[0]; + } + } + + function getInput(e, env) { + if (e.filename && env.filename && (e.filename !== env.filename)) { + return parser.imports.contents[basename(e.filename)]; + } else { + return input; + } + } + + function getLocation(index, input) { + for (var n = index, column = -1; + n >= 0 && input.charAt(n) !== '\n'; + n--) { column++ } + + return { line: typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null, + column: column }; + } + + function LessError(e, env) { + var input = getInput(e, env), + loc = getLocation(e.index, input), + line = loc.line, + col = loc.column, + lines = input.split('\n'); + + this.type = e.type || 'Syntax'; + this.message = e.message; + this.filename = e.filename || env.filename; + this.index = e.index; + this.line = typeof(line) === 'number' ? line + 1 : null; + this.callLine = e.call && (getLocation(e.call, input).line + 1); + this.callExtract = lines[getLocation(e.call, input).line]; + this.stack = e.stack; + this.column = col; + this.extract = [ + lines[line - 1], + lines[line], + lines[line + 1] + ]; + } + + this.env = env = env || {}; + + // The optimization level dictates the thoroughness of the parser, + // the lower the number, the less nodes it will create in the tree. + // This could matter for debugging, or if you want to access + // the individual nodes in the tree. + this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; + + this.env.filename = this.env.filename || null; + + // + // The Parser + // + return parser = { + + imports: imports, + // + // Parse an input string into an abstract syntax tree, + // call `callback` when done. + // + parse: function (str, callback) { + var root, start, end, zone, line, lines, buff = [], c, error = null; + + i = j = current = furthest = 0; + input = str.replace(/\r\n/g, '\n'); + + // Split the input into chunks. + chunks = (function (chunks) { + var j = 0, + skip = /[^"'`\{\}\/\(\)\\]+/g, + comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g, + string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`\\\r\n]|\\.)*)`/g, + level = 0, + match, + chunk = chunks[0], + inParam; + + for (var i = 0, c, cc; i < input.length; i++) { + skip.lastIndex = i; + if (match = skip.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + } + } + c = input.charAt(i); + comment.lastIndex = string.lastIndex = i; + + if (match = string.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + + if (!inParam && c === '/') { + cc = input.charAt(i + 1); + if (cc === '/' || cc === '*') { + if (match = comment.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + } + } + + switch (c) { + case '{': if (! inParam) { level ++; chunk.push(c); break } + case '}': if (! inParam) { level --; chunk.push(c); chunks[++j] = chunk = []; break } + case '(': if (! inParam) { inParam = true; chunk.push(c); break } + case ')': if ( inParam) { inParam = false; chunk.push(c); break } + default: chunk.push(c); + } + } + if (level > 0) { + error = new(LessError)({ + index: i, + type: 'Parse', + message: "missing closing `}`", + filename: env.filename + }, env); + } + + return chunks.map(function (c) { return c.join('') });; + })([[]]); + + if (error) { + return callback(error); + } + + // Start with the primary rule. + // The whole syntax tree is held under a Ruleset node, + // with the `root` property set to true, so no `{}` are + // output. The callback is called when the input is parsed. + try { + root = new(tree.Ruleset)([], $(this.parsers.primary)); + root.root = true; + } catch (e) { + return callback(new(LessError)(e, env)); + } + + root.toCSS = (function (evaluate) { + var line, lines, column; + + return function (options, variables) { + var frames = [], importError; + + options = options || {}; + // + // Allows setting variables with a hash, so: + // + // `{ color: new(tree.Color)('#f01') }` will become: + // + // new(tree.Rule)('@color', + // new(tree.Value)([ + // new(tree.Expression)([ + // new(tree.Color)('#f01') + // ]) + // ]) + // ) + // + if (typeof(variables) === 'object' && !Array.isArray(variables)) { + variables = Object.keys(variables).map(function (k) { + var value = variables[k]; + + if (! (value instanceof tree.Value)) { + if (! (value instanceof tree.Expression)) { + value = new(tree.Expression)([value]); + } + value = new(tree.Value)([value]); + } + return new(tree.Rule)('@' + k, value, false, 0); + }); + frames = [new(tree.Ruleset)(null, variables)]; + } + + try { + var css = evaluate.call(this, { frames: frames }) + .toCSS([], { compress: options.compress || false }); + } catch (e) { + throw new(LessError)(e, env); + } + + if ((importError = parser.imports.error)) { // Check if there was an error during importing + if (importError instanceof LessError) throw importError; + else throw new(LessError)(importError, env); + } + + if (options.yuicompress && less.mode === 'node') { + return require('./cssmin').compressor.cssmin(css); + } else if (options.compress) { + return css.replace(/(\s)+/g, "$1"); + } else { + return css; + } + }; + })(root.eval); + + // If `i` is smaller than the `input.length - 1`, + // it means the parser wasn't able to parse the whole + // string, so we've got a parsing error. + // + // We try to extract a \n delimited string, + // showing the line where the parse error occured. + // We split it up into two parts (the part which parsed, + // and the part which didn't), so we can color them differently. + if (i < input.length - 1) { + i = furthest; + lines = input.split('\n'); + line = (input.slice(0, i).match(/\n/g) || "").length + 1; + + for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } + + error = { + type: "Parse", + message: "Syntax Error on line " + line, + index: i, + filename: env.filename, + line: line, + column: column, + extract: [ + lines[line - 2], + lines[line - 1], + lines[line] + ] + }; + } + + if (this.imports.queue.length > 0) { + finish = function () { callback(error, root) }; + } else { + callback(error, root); + } + }, + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + parsers: { + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + primary: function () { + var node, root = []; + + while ((node = $(this.mixin.definition) || $(this.rule) || $(this.ruleset) || + $(this.mixin.call) || $(this.comment) || $(this.directive)) + || $(/^[\s\n]+/)) { + node && root.push(node); + } + return root; + }, + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + comment: function () { + var comment; + + if (input.charAt(i) !== '/') return; + + if (input.charAt(i + 1) === '/') { + return new(tree.Comment)($(/^\/\/.*/), true); + } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { + return new(tree.Comment)(comment); + } + }, + + // + // Entities are tokens which can be found inside an Expression + // + entities: { + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + quoted: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return; + + e && $('~'); + + if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) { + return new(tree.Quoted)(str[0], str[1] || str[2], e); + } + }, + + // + // A catch-all word, such as: + // + // black border-collapse + // + keyword: function () { + var k; + + if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) { + if (tree.colors.hasOwnProperty(k)) { + // detect named color + return new(tree.Color)(tree.colors[k].slice(1)); + } else { + return new(tree.Keyword)(k); + } + } + }, + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + call: function () { + var name, args, index = i; + + if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return; + + name = name[1].toLowerCase(); + + if (name === 'url') { return null } + else { i += name.length } + + if (name === 'alpha') { return $(this.alpha) } + + $('('); // Parse the '(' and consume whitespace. + + args = $(this.entities.arguments); + + if (! $(')')) return; + + if (name) { return new(tree.Call)(name, args, index, env.filename) } + }, + arguments: function () { + var args = [], arg; + + while (arg = $(this.entities.assignment) || $(this.expression)) { + args.push(arg); + if (! $(',')) { break } + } + return args; + }, + literal: function () { + return $(this.entities.dimension) || + $(this.entities.color) || + $(this.entities.quoted); + }, + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + + assignment: function () { + var key, value; + if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) { + return new(tree.Assignment)(key, value); + } + }, + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + url: function () { + var value; + + if (input.charAt(i) !== 'u' || !$(/^url\(/)) return; + value = $(this.entities.quoted) || $(this.entities.variable) || + $(this.entities.dataURI) || $(/^[-\w%@$\/.&=:;#+?~]+/) || ""; + + expect(')'); + + return new(tree.URL)((value.value || value.data || value instanceof tree.Variable) + ? value : new(tree.Anonymous)(value), imports.paths); + }, + + dataURI: function () { + var obj; + + if ($(/^data:/)) { + obj = {}; + obj.mime = $(/^[^\/]+\/[^,;)]+/) || ''; + obj.charset = $(/^;\s*charset=[^,;)]+/) || ''; + obj.base64 = $(/^;\s*base64/) || ''; + obj.data = $(/^,\s*[^)]+/); + + if (obj.data) { return obj } + } + }, + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + variable: function () { + var name, index = i; + + if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) { + return new(tree.Variable)(name, index, env.filename); + } + }, + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + color: function () { + var rgb; + + if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) { + return new(tree.Color)(rgb[1]); + } + }, + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + dimension: function () { + var value, c = input.charCodeAt(i); + if ((c > 57 || c < 45) || c === 47) return; + + if (value = $(/^(-?\d*\.?\d+)(px|%|em|rem|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn|dpi)?/)) { + return new(tree.Dimension)(value[1], value[2]); + } + }, + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + javascript: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '`') { return } + + e && $('~'); + + if (str = $(/^`([^`]*)`/)) { + return new(tree.JavaScript)(str[1], i, e); + } + } + }, + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + variable: function () { + var name; + + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] } + }, + + // + // A font size/line-height shorthand + // + // small/12px + // + // We need to peek first, or we'll match on keywords and dimensions + // + shorthand: function () { + var a, b; + + if (! peek(/^[@\w.%-]+\/[@\w.-]+/)) return; + + if ((a = $(this.entity)) && $('/') && (b = $(this.entity))) { + return new(tree.Shorthand)(a, b); + } + }, + + // + // Mixins + // + mixin: { + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + call: function () { + var elements = [], e, c, args = [], arg, index = i, s = input.charAt(i), name, value, important = false; + + if (s !== '.' && s !== '#') { return } + + while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) { + elements.push(new(tree.Element)(c, e, i)); + c = $('>'); + } + if ($('(')) { + while (arg = $(this.expression)) { + value = arg; + name = null; + + // Variable + if (arg.value.length == 1) { + var val = arg.value[0]; + if (val instanceof tree.Variable) { + if ($(':')) { + if (value = $(this.expression)) { + name = val.name; + } else { + throw new(Error)("Expected value"); + } + } + } + } + + args.push({ name: name, value: value }); + + if (! $(',')) { break } + } + if (! $(')')) throw new(Error)("Expected )"); + } + + if ($(this.important)) { + important = true; + } + + if (elements.length > 0 && ($(';') || peek('}'))) { + return new(tree.mixin.Call)(elements, args, index, env.filename, important); + } + }, + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + definition: function () { + var name, params = [], match, ruleset, param, value, cond, variadic = false; + if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || + peek(/^[^{]*(;|})/)) return; + + save(); + + if (match = $(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)) { + name = match[1]; + + do { + if (input.charAt(i) === '.' && $(/^\.{3}/)) { + variadic = true; + break; + } else if (param = $(this.entities.variable) || $(this.entities.literal) + || $(this.entities.keyword)) { + // Variable + if (param instanceof tree.Variable) { + if ($(':')) { + value = expect(this.expression, 'expected expression'); + params.push({ name: param.name, value: value }); + } else if ($(/^\.{3}/)) { + params.push({ name: param.name, variadic: true }); + variadic = true; + break; + } else { + params.push({ name: param.name }); + } + } else { + params.push({ value: param }); + } + } else { + break; + } + } while ($(',')) + + expect(')'); + + if ($(/^when/)) { // Guard + cond = expect(this.conditions, 'expected condition'); + } + + ruleset = $(this.block); + + if (ruleset) { + return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); + } else { + restore(); + } + } + } + }, + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + entity: function () { + return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) || + $(this.entities.call) || $(this.entities.keyword) || $(this.entities.javascript) || + $(this.comment); + }, + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was ommitted. + // + end: function () { + return $(';') || peek('}'); + }, + + // + // IE's alpha function + // + // alpha(opacity=88) + // + alpha: function () { + var value; + + if (! $(/^\(opacity=/i)) return; + if (value = $(/^\d+/) || $(this.entities.variable)) { + expect(')'); + return new(tree.Alpha)(value); + } + }, + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + element: function () { + var e, t, c, v; + + c = $(this.combinator); + e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/) || + $('*') || $(this.attribute) || $(/^\([^)@]+\)/); + + if (! e) { + $('(') && (v = $(this.entities.variable)) && $(')') && (e = new(tree.Paren)(v)); + } + + if (e) { return new(tree.Element)(c, e, i) } + + if (c.value && c.value.charAt(0) === '&') { + return new(tree.Element)(c, null, i); + } + }, + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + combinator: function () { + var match, c = input.charAt(i); + + if (c === '>' || c === '+' || c === '~') { + i++; + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(c); + } else if (c === '&') { + match = '&'; + i++; + if(input.charAt(i) === ' ') { + match = '& '; + } + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(match); + } else if (input.charAt(i - 1) === ' ') { + return new(tree.Combinator)(" "); + } else { + return new(tree.Combinator)(null); + } + }, + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + selector: function () { + var sel, e, elements = [], c, match; + + if ($('(')) { + sel = $(this.entity); + expect(')'); + return new(tree.Selector)([new(tree.Element)('', sel, i)]); + } + + while (e = $(this.element)) { + c = input.charAt(i); + elements.push(e) + if (c === '{' || c === '}' || c === ';' || c === ',') { break } + } + + if (elements.length > 0) { return new(tree.Selector)(elements) } + }, + tag: function () { + return $(/^[a-zA-Z][a-zA-Z-]*[0-9]?/) || $('*'); + }, + attribute: function () { + var attr = '', key, val, op; + + if (! $('[')) return; + + if (key = $(/^[a-zA-Z-]+/) || $(this.entities.quoted)) { + if ((op = $(/^[|~*$^]?=/)) && + (val = $(this.entities.quoted) || $(/^[\w-]+/))) { + attr = [key, op, val.toCSS ? val.toCSS() : val].join(''); + } else { attr = key } + } + + if (! $(']')) return; + + if (attr) { return "[" + attr + "]" } + }, + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + block: function () { + var content; + + if ($('{') && (content = $(this.primary)) && $('}')) { + return content; + } + }, + + // + // div, .class, body > p {...} + // + ruleset: function () { + var selectors = [], s, rules, match; + save(); + + while (s = $(this.selector)) { + selectors.push(s); + $(this.comment); + if (! $(',')) { break } + $(this.comment); + } + + if (selectors.length > 0 && (rules = $(this.block))) { + return new(tree.Ruleset)(selectors, rules, env.strictImports); + } else { + // Backtrack + furthest = i; + restore(); + } + }, + rule: function () { + var name, value, c = input.charAt(i), important, match; + save(); + + if (c === '.' || c === '#' || c === '&') { return } + + if (name = $(this.variable) || $(this.property)) { + if ((name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) { + i += match[0].length - 1; + value = new(tree.Anonymous)(match[1]); + } else if (name === "font") { + value = $(this.font); + } else { + value = $(this.value); + } + important = $(this.important); + + if (value && $(this.end)) { + return new(tree.Rule)(name, value, important, memo); + } else { + furthest = i; + restore(); + } + } + }, + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + "import": function () { + var path, features, index = i; + var dir = $(/^@import(?:-(once))?\s+/); + + if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) { + features = $(this.mediaFeatures); + if ($(';')) { + return new(tree.Import)(path, imports, features, (dir[1] === 'once'), index); + } + } + }, + + mediaFeature: function () { + var e, p, nodes = []; + + do { + if (e = $(this.entities.keyword)) { + nodes.push(e); + } else if ($('(')) { + p = $(this.property); + e = $(this.entity); + if ($(')')) { + if (p && e) { + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, true))); + } else if (e) { + nodes.push(new(tree.Paren)(e)); + } else { + return null; + } + } else { return null } + } + } while (e); + + if (nodes.length > 0) { + return new(tree.Expression)(nodes); + } + }, + + mediaFeatures: function () { + var e, features = []; + + do { + if (e = $(this.mediaFeature)) { + features.push(e); + if (! $(',')) { break } + } else if (e = $(this.entities.variable)) { + features.push(e); + if (! $(',')) { break } + } + } while (e); + + return features.length > 0 ? features : null; + }, + + media: function () { + var features, rules; + + if ($(/^@media/)) { + features = $(this.mediaFeatures); + + if (rules = $(this.block)) { + return new(tree.Media)(rules, features); + } + } + }, + + // + // A CSS Directive + // + // @charset "utf-8"; + // + directive: function () { + var name, value, rules, types, e, nodes; + + if (input.charAt(i) !== '@') return; + + if (value = $(this['import']) || $(this.media)) { + return value; + } else if (name = $(/^@page|@keyframes/) || $(/^@(?:-webkit-|-moz-|-o-|-ms-)[a-z0-9-]+/)) { + types = ($(/^[^{]+/) || '').trim(); + if (rules = $(this.block)) { + return new(tree.Directive)(name + " " + types, rules); + } + } else if (name = $(/^@[-a-z]+/)) { + if (name === '@font-face') { + if (rules = $(this.block)) { + return new(tree.Directive)(name, rules); + } + } else if ((value = $(this.entity)) && $(';')) { + return new(tree.Directive)(name, value); + } + } + }, + font: function () { + var value = [], expression = [], weight, shorthand, font, e; + + while (e = $(this.shorthand) || $(this.entity)) { + expression.push(e); + } + value.push(new(tree.Expression)(expression)); + + if ($(',')) { + while (e = $(this.expression)) { + value.push(e); + if (! $(',')) { break } + } + } + return new(tree.Value)(value); + }, + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + value: function () { + var e, expressions = [], important; + + while (e = $(this.expression)) { + expressions.push(e); + if (! $(',')) { break } + } + + if (expressions.length > 0) { + return new(tree.Value)(expressions); + } + }, + important: function () { + if (input.charAt(i) === '!') { + return $(/^! *important/); + } + }, + sub: function () { + var e; + + if ($('(') && (e = $(this.expression)) && $(')')) { + return e; + } + }, + multiplication: function () { + var m, a, op, operation; + if (m = $(this.operand)) { + while (!peek(/^\/\*/) && (op = ($('/') || $('*'))) && (a = $(this.operand))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + addition: function () { + var m, a, op, operation; + if (m = $(this.multiplication)) { + while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) && + (a = $(this.multiplication))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + conditions: function () { + var a, b, index = i, condition; + + if (a = $(this.condition)) { + while ($(',') && (b = $(this.condition))) { + condition = new(tree.Condition)('or', condition || a, b, index); + } + return condition || a; + } + }, + condition: function () { + var a, b, c, op, index = i, negate = false; + + if ($(/^not/)) { negate = true } + expect('('); + if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + if (op = $(/^(?:>=|=<|[<=>])/)) { + if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + c = new(tree.Condition)(op, a, b, index, negate); + } else { + error('expected expression'); + } + } else { + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); + } + expect(')'); + return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c; + } + }, + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + operand: function () { + var negate, p = input.charAt(i + 1); + + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') } + var o = $(this.sub) || $(this.entities.dimension) || + $(this.entities.color) || $(this.entities.variable) || + $(this.entities.call); + return negate ? new(tree.Operation)('*', [new(tree.Dimension)(-1), o]) + : o; + }, + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + expression: function () { + var e, delim, entities = [], d; + + while (e = $(this.addition) || $(this.entity)) { + entities.push(e); + } + if (entities.length > 0) { + return new(tree.Expression)(entities); + } + }, + property: function () { + var name; + + if (name = $(/^(\*?-?[-a-z_0-9]+)\s*:/)) { + return name[1]; + } + } + } + }; +}; + +if (less.mode === 'browser' || less.mode === 'rhino') { + // + // Used by `@import` directives + // + less.Parser.importer = function (path, paths, callback, env) { + if (!/^([a-z]+:)?\//.test(path) && paths.length > 0) { + path = paths[0] + path; + } + // We pass `true` as 3rd argument, to force the reload of the import. + // This is so we can get the syntax tree as opposed to just the CSS output, + // as we need this to evaluate the current stylesheet. + loadStyleSheet({ href: path, title: path, type: env.mime }, function (e) { + if (e && typeof(env.errback) === "function") { + env.errback.call(null, path, paths, callback, env); + } else { + callback.apply(null, arguments); + } + }, true); + }; +} + diff --git a/dashboard/bin/lib/less/rhino.js b/dashboard/bin/lib/less/rhino.js new file mode 100644 index 00000000..a2c5662f --- /dev/null +++ b/dashboard/bin/lib/less/rhino.js @@ -0,0 +1,62 @@ +var name; + +function loadStyleSheet(sheet, callback, reload, remaining) { + var sheetName = name.slice(0, name.lastIndexOf('/') + 1) + sheet.href; + var input = readFile(sheetName); + var parser = new less.Parser({ + paths: [sheet.href.replace(/[\w\.-]+$/, '')] + }); + parser.parse(input, function (e, root) { + if (e) { + print("Error: " + e); + quit(1); + } + callback(root, sheet, { local: false, lastModified: 0, remaining: remaining }); + }); + + // callback({}, sheet, { local: true, remaining: remaining }); +} + +function writeFile(filename, content) { + var fstream = new java.io.FileWriter(filename); + var out = new java.io.BufferedWriter(fstream); + out.write(content); + out.close(); +} + +// Command line integration via Rhino +(function (args) { + name = args[0]; + var output = args[1]; + + if (!name) { + print('No files present in the fileset; Check your pattern match in build.xml'); + quit(1); + } + path = name.split("/");path.pop();path=path.join("/") + + var input = readFile(name); + + if (!input) { + print('lesscss: couldn\'t open file ' + name); + quit(1); + } + + var result; + var parser = new less.Parser(); + parser.parse(input, function (e, root) { + if (e) { + quit(1); + } else { + result = root.toCSS(); + if (output) { + writeFile(output, result); + print("Written to " + output); + } else { + print(result); + } + quit(0); + } + }); + print("done"); +}(arguments)); diff --git a/dashboard/bin/lib/less/tree.js b/dashboard/bin/lib/less/tree.js new file mode 100644 index 00000000..24ecd712 --- /dev/null +++ b/dashboard/bin/lib/less/tree.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.find = function (obj, fun) { + for (var i = 0, r; i < obj.length; i++) { + if (r = fun.call(obj, obj[i])) { return r } + } + return null; +}; +tree.jsify = function (obj) { + if (Array.isArray(obj.value) && (obj.value.length > 1)) { + return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']'; + } else { + return obj.toCSS(false); + } +}; + +})(require('./tree')); diff --git a/dashboard/bin/lib/less/tree/alpha.js b/dashboard/bin/lib/less/tree/alpha.js new file mode 100644 index 00000000..139ae920 --- /dev/null +++ b/dashboard/bin/lib/less/tree/alpha.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Alpha = function (val) { + this.value = val; +}; +tree.Alpha.prototype = { + toCSS: function () { + return "alpha(opacity=" + + (this.value.toCSS ? this.value.toCSS() : this.value) + ")"; + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/anonymous.js b/dashboard/bin/lib/less/tree/anonymous.js new file mode 100644 index 00000000..460c9ec7 --- /dev/null +++ b/dashboard/bin/lib/less/tree/anonymous.js @@ -0,0 +1,13 @@ +(function (tree) { + +tree.Anonymous = function (string) { + this.value = string.value || string; +}; +tree.Anonymous.prototype = { + toCSS: function () { + return this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/assignment.js b/dashboard/bin/lib/less/tree/assignment.js new file mode 100644 index 00000000..70ce6e2f --- /dev/null +++ b/dashboard/bin/lib/less/tree/assignment.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Assignment = function (key, val) { + this.key = key; + this.value = val; +}; +tree.Assignment.prototype = { + toCSS: function () { + return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value); + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); \ No newline at end of file diff --git a/dashboard/bin/lib/less/tree/call.js b/dashboard/bin/lib/less/tree/call.js new file mode 100644 index 00000000..c1465dd4 --- /dev/null +++ b/dashboard/bin/lib/less/tree/call.js @@ -0,0 +1,48 @@ +(function (tree) { + +// +// A function call node. +// +tree.Call = function (name, args, index, filename) { + this.name = name; + this.args = args; + this.index = index; + this.filename = filename; +}; +tree.Call.prototype = { + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // or we simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + eval: function (env) { + var args = this.args.map(function (a) { return a.eval(env) }); + + if (this.name in tree.functions) { // 1. + try { + return tree.functions[this.name].apply(tree.functions, args); + } catch (e) { + throw { type: e.type || "Runtime", + message: "error evaluating function `" + this.name + "`" + + (e.message ? ': ' + e.message : ''), + index: this.index, filename: this.filename }; + } + } else { // 2. + return new(tree.Anonymous)(this.name + + "(" + args.map(function (a) { return a.toCSS() }).join(', ') + ")"); + } + }, + + toCSS: function (env) { + return this.eval(env).toCSS(); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/color.js b/dashboard/bin/lib/less/tree/color.js new file mode 100644 index 00000000..37ce1781 --- /dev/null +++ b/dashboard/bin/lib/less/tree/color.js @@ -0,0 +1,101 @@ +(function (tree) { +// +// RGB Colors - #ff0014, #eee +// +tree.Color = function (rgb, a) { + // + // The end goal here, is to parse the arguments + // into an integer triplet, such as `128, 255, 0` + // + // This facilitates operations and conversions. + // + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +tree.Color.prototype = { + eval: function () { return this }, + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + toCSS: function () { + if (this.alpha < 1.0) { + return "rgba(" + this.rgb.map(function (c) { + return Math.round(c); + }).concat(this.alpha).join(', ') + ")"; + } else { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } + }, + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + operate: function (op, other) { + var result = []; + + if (! (other instanceof tree.Color)) { + other = other.toColor(); + } + + for (var c = 0; c < 3; c++) { + result[c] = tree.operate(op, this.rgb[c], other.rgb[c]); + } + return new(tree.Color)(result, this.alpha + other.alpha); + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } +}; + + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/comment.js b/dashboard/bin/lib/less/tree/comment.js new file mode 100644 index 00000000..f4a33840 --- /dev/null +++ b/dashboard/bin/lib/less/tree/comment.js @@ -0,0 +1,14 @@ +(function (tree) { + +tree.Comment = function (value, silent) { + this.value = value; + this.silent = !!silent; +}; +tree.Comment.prototype = { + toCSS: function (env) { + return env.compress ? '' : this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/condition.js b/dashboard/bin/lib/less/tree/condition.js new file mode 100644 index 00000000..6b79dc96 --- /dev/null +++ b/dashboard/bin/lib/less/tree/condition.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Condition = function (op, l, r, i, negate) { + this.op = op.trim(); + this.lvalue = l; + this.rvalue = r; + this.index = i; + this.negate = negate; +}; +tree.Condition.prototype.eval = function (env) { + var a = this.lvalue.eval(env), + b = this.rvalue.eval(env); + + var i = this.index, result; + + var result = (function (op) { + switch (op) { + case 'and': + return a && b; + case 'or': + return a || b; + default: + if (a.compare) { + result = a.compare(b); + } else if (b.compare) { + result = b.compare(a); + } else { + throw { type: "Type", + message: "Unable to perform comparison", + index: i }; + } + switch (result) { + case -1: return op === '<' || op === '=<'; + case 0: return op === '=' || op === '>=' || op === '=<'; + case 1: return op === '>' || op === '>='; + } + } + })(this.op); + return this.negate ? !result : result; +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/dimension.js b/dashboard/bin/lib/less/tree/dimension.js new file mode 100644 index 00000000..9a6fce3d --- /dev/null +++ b/dashboard/bin/lib/less/tree/dimension.js @@ -0,0 +1,49 @@ +(function (tree) { + +// +// A number with a unit +// +tree.Dimension = function (value, unit) { + this.value = parseFloat(value); + this.unit = unit || null; +}; + +tree.Dimension.prototype = { + eval: function () { return this }, + toColor: function () { + return new(tree.Color)([this.value, this.value, this.value]); + }, + toCSS: function () { + var css = this.value + this.unit; + return css; + }, + + // In an operation between two Dimensions, + // we default to the first Dimension's unit, + // so `1px + 2em` will yield `3px`. + // In the future, we could implement some unit + // conversions such that `100cm + 10mm` would yield + // `101cm`. + operate: function (op, other) { + return new(tree.Dimension) + (tree.operate(op, this.value, other.value), + this.unit || other.unit); + }, + + // TODO: Perform unit conversion before comparing + compare: function (other) { + if (other instanceof tree.Dimension) { + if (other.value > this.value) { + return -1; + } else if (other.value < this.value) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/directive.js b/dashboard/bin/lib/less/tree/directive.js new file mode 100644 index 00000000..27538332 --- /dev/null +++ b/dashboard/bin/lib/less/tree/directive.js @@ -0,0 +1,35 @@ +(function (tree) { + +tree.Directive = function (name, value, features) { + this.name = name; + + if (Array.isArray(value)) { + this.ruleset = new(tree.Ruleset)([], value); + this.ruleset.allowImports = true; + } else { + this.value = value; + } +}; +tree.Directive.prototype = { + toCSS: function (ctx, env) { + if (this.ruleset) { + this.ruleset.root = true; + return this.name + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + } else { + return this.name + ' ' + this.value.toCSS() + ';\n'; + } + }, + eval: function (env) { + env.frames.unshift(this); + this.ruleset = this.ruleset && this.ruleset.eval(env); + env.frames.shift(); + return this; + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/element.js b/dashboard/bin/lib/less/tree/element.js new file mode 100644 index 00000000..14b08d2e --- /dev/null +++ b/dashboard/bin/lib/less/tree/element.js @@ -0,0 +1,52 @@ +(function (tree) { + +tree.Element = function (combinator, value, index) { + this.combinator = combinator instanceof tree.Combinator ? + combinator : new(tree.Combinator)(combinator); + + if (typeof(value) === 'string') { + this.value = value.trim(); + } else if (value) { + this.value = value; + } else { + this.value = ""; + } + this.index = index; +}; +tree.Element.prototype.eval = function (env) { + return new(tree.Element)(this.combinator, + this.value.eval ? this.value.eval(env) : this.value, + this.index); +}; +tree.Element.prototype.toCSS = function (env) { + var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); + if (value == '' && this.combinator.value.charAt(0) == '&') { + return ''; + } else { + return this.combinator.toCSS(env || {}) + value; + } +}; + +tree.Combinator = function (value) { + if (value === ' ') { + this.value = ' '; + } else if (value === '& ') { + this.value = '& '; + } else { + this.value = value ? value.trim() : ""; + } +}; +tree.Combinator.prototype.toCSS = function (env) { + return { + '' : '', + ' ' : ' ', + '&' : '', + '& ' : ' ', + ':' : ' :', + '+' : env.compress ? '+' : ' + ', + '~' : env.compress ? '~' : ' ~ ', + '>' : env.compress ? '>' : ' > ' + }[this.value]; +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/expression.js b/dashboard/bin/lib/less/tree/expression.js new file mode 100644 index 00000000..fbfa9c5b --- /dev/null +++ b/dashboard/bin/lib/less/tree/expression.js @@ -0,0 +1,23 @@ +(function (tree) { + +tree.Expression = function (value) { this.value = value }; +tree.Expression.prototype = { + eval: function (env) { + if (this.value.length > 1) { + return new(tree.Expression)(this.value.map(function (e) { + return e.eval(env); + })); + } else if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return this; + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS ? e.toCSS(env) : ''; + }).join(' '); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/import.js b/dashboard/bin/lib/less/tree/import.js new file mode 100644 index 00000000..7a977def --- /dev/null +++ b/dashboard/bin/lib/less/tree/import.js @@ -0,0 +1,83 @@ +(function (tree) { +// +// CSS @import node +// +// The general strategy here is that we don't want to wait +// for the parsing to be completed, before we start importing +// the file. That's because in the context of a browser, +// most of the time will be spent waiting for the server to respond. +// +// On creation, we push the import path to our import queue, though +// `import,push`, we also pass it a callback, which it'll call once +// the file has been fetched, and parsed. +// +tree.Import = function (path, imports, features, once, index) { + var that = this; + + this.once = once; + this.index = index; + this._path = path; + this.features = features && new(tree.Value)(features); + + // The '.less' extension is optional + if (path instanceof tree.Quoted) { + this.path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less'; + } else { + this.path = path.value.value || path.value; + } + + this.css = /css(\?.*)?$/.test(this.path); + + // Only pre-compile .less files + if (! this.css) { + imports.push(this.path, function (e, root, imported) { + if (e) { e.index = index } + if (imported && that.once) that.skip = imported; + that.root = root || new(tree.Ruleset)([], []); + }); + } +}; + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// +tree.Import.prototype = { + toCSS: function (env) { + var features = this.features ? ' ' + this.features.toCSS(env) : ''; + + if (this.css) { + return "@import " + this._path.toCSS() + features + ';\n'; + } else { + return ""; + } + }, + eval: function (env) { + var ruleset, features = this.features && this.features.eval(env); + + if (this.skip) return []; + + if (this.css) { + return this; + } else { + ruleset = new(tree.Ruleset)([], this.root.rules.slice(0)); + + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype + .splice + .apply(ruleset.rules, + [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/javascript.js b/dashboard/bin/lib/less/tree/javascript.js new file mode 100644 index 00000000..772a31dd --- /dev/null +++ b/dashboard/bin/lib/less/tree/javascript.js @@ -0,0 +1,51 @@ +(function (tree) { + +tree.JavaScript = function (string, index, escaped) { + this.escaped = escaped; + this.expression = string; + this.index = index; +}; +tree.JavaScript.prototype = { + eval: function (env) { + var result, + that = this, + context = {}; + + var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { + return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); + }); + + try { + expression = new(Function)('return (' + expression + ')'); + } catch (e) { + throw { message: "JavaScript evaluation error: `" + expression + "`" , + index: this.index }; + } + + for (var k in env.frames[0].variables()) { + context[k.slice(1)] = { + value: env.frames[0].variables()[k].value, + toJS: function () { + return this.value.eval(env).toCSS(); + } + }; + } + + try { + result = expression.call(context); + } catch (e) { + throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" , + index: this.index }; + } + if (typeof(result) === 'string') { + return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); + } else if (Array.isArray(result)) { + return new(tree.Anonymous)(result.join(', ')); + } else { + return new(tree.Anonymous)(result); + } + } +}; + +})(require('../tree')); + diff --git a/dashboard/bin/lib/less/tree/keyword.js b/dashboard/bin/lib/less/tree/keyword.js new file mode 100644 index 00000000..701b79e5 --- /dev/null +++ b/dashboard/bin/lib/less/tree/keyword.js @@ -0,0 +1,19 @@ +(function (tree) { + +tree.Keyword = function (value) { this.value = value }; +tree.Keyword.prototype = { + eval: function () { return this }, + toCSS: function () { return this.value }, + compare: function (other) { + if (other instanceof tree.Keyword) { + return other.value === this.value ? 0 : 1; + } else { + return -1; + } + } +}; + +tree.True = new(tree.Keyword)('true'); +tree.False = new(tree.Keyword)('false'); + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/media.js b/dashboard/bin/lib/less/tree/media.js new file mode 100644 index 00000000..2b7b26e5 --- /dev/null +++ b/dashboard/bin/lib/less/tree/media.js @@ -0,0 +1,114 @@ +(function (tree) { + +tree.Media = function (value, features) { + var el = new(tree.Element)('&', null, 0), + selectors = [new(tree.Selector)([el])]; + + this.features = new(tree.Value)(features); + this.ruleset = new(tree.Ruleset)(selectors, value); + this.ruleset.allowImports = true; +}; +tree.Media.prototype = { + toCSS: function (ctx, env) { + var features = this.features.toCSS(env); + + this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia); + return '@media ' + features + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + }, + eval: function (env) { + if (!env.mediaBlocks) { + env.mediaBlocks = []; + env.mediaPath = []; + } + + var blockIndex = env.mediaBlocks.length; + env.mediaPath.push(this); + env.mediaBlocks.push(this); + + var media = new(tree.Media)([], []); + media.features = this.features.eval(env); + + env.frames.unshift(this.ruleset); + media.ruleset = this.ruleset.eval(env); + env.frames.shift(); + + env.mediaBlocks[blockIndex] = media; + env.mediaPath.pop(); + + return env.mediaPath.length === 0 ? media.evalTop(env) : + media.evalNested(env) + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }, + + evalTop: function (env) { + var result = this; + + // Render all dependent Media blocks. + if (env.mediaBlocks.length > 1) { + var el = new(tree.Element)('&', null, 0); + var selectors = [new(tree.Selector)([el])]; + result = new(tree.Ruleset)(selectors, env.mediaBlocks); + result.multiMedia = true; + } + + delete env.mediaBlocks; + delete env.mediaPath; + + return result; + }, + evalNested: function (env) { + var i, value, + path = env.mediaPath.concat([this]); + + // Extract the media-query conditions separated with `,` (OR). + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof tree.Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + this.features = new(tree.Value)(this.permute(path).map(function (path) { + path = path.map(function (fragment) { + return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); + }); + + for(i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new(tree.Anonymous)("and")); + } + + return new(tree.Expression)(path); + })); + + // Fake a tree-node that doesn't output anything. + return new(tree.Ruleset)([], []); + }, + permute: function (arr) { + if (arr.length === 0) { + return []; + } else if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + var rest = this.permute(arr.slice(1)); + for (var i = 0; i < rest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push([arr[0][j]].concat(rest[i])); + } + } + return result; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/mixin.js b/dashboard/bin/lib/less/tree/mixin.js new file mode 100644 index 00000000..b441bf3b --- /dev/null +++ b/dashboard/bin/lib/less/tree/mixin.js @@ -0,0 +1,146 @@ +(function (tree) { + +tree.mixin = {}; +tree.mixin.Call = function (elements, args, index, filename, important) { + this.selector = new(tree.Selector)(elements); + this.arguments = args; + this.index = index; + this.filename = filename; + this.important = important; +}; +tree.mixin.Call.prototype = { + eval: function (env) { + var mixins, args, rules = [], match = false; + + for (var i = 0; i < env.frames.length; i++) { + if ((mixins = env.frames[i].find(this.selector)).length > 0) { + args = this.arguments && this.arguments.map(function (a) { + return { name: a.name, value: a.value.eval(env) }; + }); + for (var m = 0; m < mixins.length; m++) { + if (mixins[m].match(args, env)) { + try { + Array.prototype.push.apply( + rules, mixins[m].eval(env, this.arguments, this.important).rules); + match = true; + } catch (e) { + throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack }; + } + } + } + if (match) { + return rules; + } else { + throw { type: 'Runtime', + message: 'No matching definition was found for `' + + this.selector.toCSS().trim() + '(' + + this.arguments.map(function (a) { + return a.toCSS(); + }).join(', ') + ")`", + index: this.index, filename: this.filename }; + } + } + } + throw { type: 'Name', + message: this.selector.toCSS().trim() + " is undefined", + index: this.index, filename: this.filename }; + } +}; + +tree.mixin.Definition = function (name, params, rules, condition, variadic) { + this.name = name; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])]; + this.params = params; + this.condition = condition; + this.variadic = variadic; + this.arity = params.length; + this.rules = rules; + this._lookups = {}; + this.required = params.reduce(function (count, p) { + if (!p.name || (p.name && !p.value)) { return count + 1 } + else { return count } + }, 0); + this.parent = tree.Ruleset.prototype; + this.frames = []; +}; +tree.mixin.Definition.prototype = { + toCSS: function () { return "" }, + variable: function (name) { return this.parent.variable.call(this, name) }, + variables: function () { return this.parent.variables.call(this) }, + find: function () { return this.parent.find.apply(this, arguments) }, + rulesets: function () { return this.parent.rulesets.apply(this) }, + + evalParams: function (env, args) { + var frame = new(tree.Ruleset)(null, []), varargs, arg; + + for (var i = 0, val, name; i < this.params.length; i++) { + arg = args && args[i] + + if (arg && arg.name) { + frame.rules.unshift(new(tree.Rule)(arg.name, arg.value.eval(env))); + args.splice(i, 1); + i--; + continue; + } + + if (name = this.params[i].name) { + if (this.params[i].variadic && args) { + varargs = []; + for (var j = i; j < args.length; j++) { + varargs.push(args[j].value.eval(env)); + } + frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); + } else if (val = (arg && arg.value) || this.params[i].value) { + frame.rules.unshift(new(tree.Rule)(name, val.eval(env))); + } else { + throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + + ' (' + args.length + ' for ' + this.arity + ')' }; + } + } + } + return frame; + }, + eval: function (env, args, important) { + var frame = this.evalParams(env, args), context, _arguments = [], rules, start; + + for (var i = 0; i < Math.max(this.params.length, args && args.length); i++) { + _arguments.push((args[i] && args[i].value) || this.params[i].value); + } + frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); + + rules = important ? + this.rules.map(function (r) { + return new(tree.Rule)(r.name, r.value, '!important', r.index); + }) : this.rules.slice(0); + + return new(tree.Ruleset)(null, rules).eval({ + frames: [this, frame].concat(this.frames, env.frames) + }); + }, + match: function (args, env) { + var argsLength = (args && args.length) || 0, len, frame; + + if (! this.variadic) { + if (argsLength < this.required) { return false } + if (argsLength > this.params.length) { return false } + if ((this.required > 0) && (argsLength > this.params.length)) { return false } + } + + if (this.condition && !this.condition.eval({ + frames: [this.evalParams(env, args)].concat(env.frames) + })) { return false } + + len = Math.min(argsLength, this.arity); + + for (var i = 0; i < len; i++) { + if (!this.params[i].name) { + if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { + return false; + } + } + } + return true; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/operation.js b/dashboard/bin/lib/less/tree/operation.js new file mode 100644 index 00000000..1ce22fb0 --- /dev/null +++ b/dashboard/bin/lib/less/tree/operation.js @@ -0,0 +1,32 @@ +(function (tree) { + +tree.Operation = function (op, operands) { + this.op = op.trim(); + this.operands = operands; +}; +tree.Operation.prototype.eval = function (env) { + var a = this.operands[0].eval(env), + b = this.operands[1].eval(env), + temp; + + if (a instanceof tree.Dimension && b instanceof tree.Color) { + if (this.op === '*' || this.op === '+') { + temp = b, b = a, a = temp; + } else { + throw { name: "OperationError", + message: "Can't substract or divide a color from a number" }; + } + } + return a.operate(this.op, b); +}; + +tree.operate = function (op, a, b) { + switch (op) { + case '+': return a + b; + case '-': return a - b; + case '*': return a * b; + case '/': return a / b; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/paren.js b/dashboard/bin/lib/less/tree/paren.js new file mode 100644 index 00000000..384a43c7 --- /dev/null +++ b/dashboard/bin/lib/less/tree/paren.js @@ -0,0 +1,16 @@ + +(function (tree) { + +tree.Paren = function (node) { + this.value = node; +}; +tree.Paren.prototype = { + toCSS: function (env) { + return '(' + this.value.toCSS(env) + ')'; + }, + eval: function (env) { + return new(tree.Paren)(this.value.eval(env)); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/quoted.js b/dashboard/bin/lib/less/tree/quoted.js new file mode 100644 index 00000000..794bf4ce --- /dev/null +++ b/dashboard/bin/lib/less/tree/quoted.js @@ -0,0 +1,29 @@ +(function (tree) { + +tree.Quoted = function (str, content, escaped, i) { + this.escaped = escaped; + this.value = content || ''; + this.quote = str.charAt(0); + this.index = i; +}; +tree.Quoted.prototype = { + toCSS: function () { + if (this.escaped) { + return this.value; + } else { + return this.quote + this.value + this.quote; + } + }, + eval: function (env) { + var that = this; + var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { + return new(tree.JavaScript)(exp, that.index, true).eval(env).value; + }).replace(/@\{([\w-]+)\}/g, function (_, name) { + var v = new(tree.Variable)('@' + name, that.index).eval(env); + return ('value' in v) ? v.value : v.toCSS(); + }); + return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/rule.js b/dashboard/bin/lib/less/tree/rule.js new file mode 100644 index 00000000..9e4e54a3 --- /dev/null +++ b/dashboard/bin/lib/less/tree/rule.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Rule = function (name, value, important, index, inline) { + this.name = name; + this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.important = important ? ' ' + important.trim() : ''; + this.index = index; + this.inline = inline || false; + + if (name.charAt(0) === '@') { + this.variable = true; + } else { this.variable = false } +}; +tree.Rule.prototype.toCSS = function (env) { + if (this.variable) { return "" } + else { + return this.name + (env.compress ? ':' : ': ') + + this.value.toCSS(env) + + this.important + (this.inline ? "" : ";"); + } +}; + +tree.Rule.prototype.eval = function (context) { + return new(tree.Rule)(this.name, + this.value.eval(context), + this.important, + this.index, this.inline); +}; + +tree.Shorthand = function (a, b) { + this.a = a; + this.b = b; +}; + +tree.Shorthand.prototype = { + toCSS: function (env) { + return this.a.toCSS(env) + "/" + this.b.toCSS(env); + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/ruleset.js b/dashboard/bin/lib/less/tree/ruleset.js new file mode 100644 index 00000000..3100cc35 --- /dev/null +++ b/dashboard/bin/lib/less/tree/ruleset.js @@ -0,0 +1,225 @@ +(function (tree) { + +tree.Ruleset = function (selectors, rules, strictImports) { + this.selectors = selectors; + this.rules = rules; + this._lookups = {}; + this.strictImports = strictImports; +}; +tree.Ruleset.prototype = { + eval: function (env) { + var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) }); + var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); + + ruleset.root = this.root; + ruleset.allowImports = this.allowImports; + + // push the current ruleset to the frames stack + env.frames.unshift(ruleset); + + // Evaluate imports + if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + } + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + // Evaluate mixin calls. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Call) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + + // Evaluate everything else + for (var i = 0, rule; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; + + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } + } + + // Pop the stack + env.frames.shift(); + + return ruleset; + }, + match: function (args) { + return !args || args.length === 0; + }, + variables: function () { + if (this._variables) { return this._variables } + else { + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); + } + }, + variable: function (name) { + return this.variables()[name]; + }, + rulesets: function () { + if (this._rulesets) { return this._rulesets } + else { + return this._rulesets = this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); + } + }, + find: function (selector, self) { + self = self || this; + var rules = [], rule, match, + key = selector.toCSS(); + + if (key in this._lookups) { return this._lookups[key] } + + this.rulesets().forEach(function (rule) { + if (rule !== self) { + for (var j = 0; j < rule.selectors.length; j++) { + if (match = selector.match(rule.selectors[j])) { + if (selector.elements.length > rule.selectors[j].elements.length) { + Array.prototype.push.apply(rules, rule.find( + new(tree.Selector)(selector.elements.slice(1)), self)); + } else { + rules.push(rule); + } + break; + } + } + } + }); + return this._lookups[key] = rules; + }, + // + // Entry point for code generation + // + // `context` holds an array of arrays. + // + toCSS: function (context, env) { + var css = [], // The CSS output + rules = [], // node.Rule instances + _rules = [], // + rulesets = [], // node.Ruleset instances + paths = [], // Current selectors + selector, // The fully rendered selector + rule; + + if (! this.root) { + if (context.length === 0) { + paths = this.selectors.map(function (s) { return [s] }); + } else { + this.joinSelectors(paths, context, this.selectors); + } + } + + // Compile rules and rulesets + for (var i = 0; i < this.rules.length; i++) { + rule = this.rules[i]; + + if (rule.rules || (rule instanceof tree.Directive) || (rule instanceof tree.Media)) { + rulesets.push(rule.toCSS(paths, env)); + } else if (rule instanceof tree.Comment) { + if (!rule.silent) { + if (this.root) { + rulesets.push(rule.toCSS(env)); + } else { + rules.push(rule.toCSS(env)); + } + } + } else { + if (rule.toCSS && !rule.variable) { + rules.push(rule.toCSS(env)); + } else if (rule.value && !rule.variable) { + rules.push(rule.value.toString()); + } + } + } + + rulesets = rulesets.join(''); + + // If this is the root node, we don't render + // a selector, or {}. + // Otherwise, only output if this ruleset has rules. + if (this.root) { + css.push(rules.join(env.compress ? '' : '\n')); + } else { + if (rules.length > 0) { + selector = paths.map(function (p) { + return p.map(function (s) { + return s.toCSS(env); + }).join('').trim(); + }).join(env.compress ? ',' : ',\n'); + + // Remove duplicates + for (var i = rules.length - 1; i >= 0; i--) { + if (_rules.indexOf(rules[i]) === -1) { + _rules.unshift(rules[i]); + } + } + rules = _rules; + + css.push(selector, + (env.compress ? '{' : ' {\n ') + + rules.join(env.compress ? '' : '\n ') + + (env.compress ? '}' : '\n}\n')); + } + } + css.push(rulesets); + + return css.join('') + (env.compress ? '\n' : ''); + }, + + joinSelectors: function (paths, context, selectors) { + for (var s = 0; s < selectors.length; s++) { + this.joinSelector(paths, context, selectors[s]); + } + }, + + joinSelector: function (paths, context, selector) { + var before = [], after = [], beforeElements = [], + afterElements = [], hasParentSelector = false, el; + + for (var i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + if (el.combinator.value.charAt(0) === '&') { + hasParentSelector = true; + } + if (hasParentSelector) afterElements.push(el); + else beforeElements.push(el); + } + + if (! hasParentSelector) { + afterElements = beforeElements; + beforeElements = []; + } + + if (beforeElements.length > 0) { + before.push(new(tree.Selector)(beforeElements)); + } + + if (afterElements.length > 0) { + after.push(new(tree.Selector)(afterElements)); + } + + for (var c = 0; c < context.length; c++) { + paths.push(before.concat(context[c]).concat(after)); + } + } +}; +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/selector.js b/dashboard/bin/lib/less/tree/selector.js new file mode 100644 index 00000000..65abbb69 --- /dev/null +++ b/dashboard/bin/lib/less/tree/selector.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Selector = function (elements) { + this.elements = elements; + if (this.elements[0].combinator.value === "") { + this.elements[0].combinator.value = ' '; + } +}; +tree.Selector.prototype.match = function (other) { + var len = this.elements.length, + olen = other.elements.length, + max = Math.min(len, olen); + + if (len < olen) { + return false; + } else { + for (var i = 0; i < max; i++) { + if (this.elements[i].value !== other.elements[i].value) { + return false; + } + } + } + return true; +}; +tree.Selector.prototype.eval = function (env) { + return new(tree.Selector)(this.elements.map(function (e) { + return e.eval(env); + })); +}; +tree.Selector.prototype.toCSS = function (env) { + if (this._css) { return this._css } + + return this._css = this.elements.map(function (e) { + if (typeof(e) === 'string') { + return ' ' + e.trim(); + } else { + return e.toCSS(env); + } + }).join(''); +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/url.js b/dashboard/bin/lib/less/tree/url.js new file mode 100644 index 00000000..0caec345 --- /dev/null +++ b/dashboard/bin/lib/less/tree/url.js @@ -0,0 +1,25 @@ +(function (tree) { + +tree.URL = function (val, paths) { + if (val.data) { + this.attrs = val; + } else { + // Add the base path if the URL is relative and we are in the browser + if (typeof(window) !== 'undefined' && !/^(?:https?:\/\/|file:\/\/|data:|\/)/.test(val.value) && paths.length > 0) { + val.value = paths[0] + (val.value.charAt(0) === '/' ? val.value.slice(1) : val.value); + } + this.value = val; + this.paths = paths; + } +}; +tree.URL.prototype = { + toCSS: function () { + return "url(" + (this.attrs ? 'data:' + this.attrs.mime + this.attrs.charset + this.attrs.base64 + this.attrs.data + : this.value.toCSS()) + ")"; + }, + eval: function (ctx) { + return this.attrs ? this : new(tree.URL)(this.value.eval(ctx), this.paths); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/value.js b/dashboard/bin/lib/less/tree/value.js new file mode 100644 index 00000000..3c1eb29a --- /dev/null +++ b/dashboard/bin/lib/less/tree/value.js @@ -0,0 +1,24 @@ +(function (tree) { + +tree.Value = function (value) { + this.value = value; + this.is = 'value'; +}; +tree.Value.prototype = { + eval: function (env) { + if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return new(tree.Value)(this.value.map(function (v) { + return v.eval(env); + })); + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS(env); + }).join(env.compress ? ',' : ', '); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/variable.js b/dashboard/bin/lib/less/tree/variable.js new file mode 100644 index 00000000..ee557e1d --- /dev/null +++ b/dashboard/bin/lib/less/tree/variable.js @@ -0,0 +1,26 @@ +(function (tree) { + +tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file }; +tree.Variable.prototype = { + eval: function (env) { + var variable, v, name = this.name; + + if (name.indexOf('@@') == 0) { + name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; + } + + if (variable = tree.find(env.frames, function (frame) { + if (v = frame.variable(name)) { + return v.value.eval(env); + } + })) { return variable } + else { + throw { type: 'Name', + message: "variable " + name + " is undefined", + filename: this.file, + index: this.index }; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/doc/source/_static/.placeholder b/dashboard/doc/source/_static/.placeholder new file mode 100755 index 00000000..e69de29b diff --git a/dashboard/doc/source/_templates/.placeholder b/dashboard/doc/source/_templates/.placeholder new file mode 100755 index 00000000..e69de29b diff --git a/dashboard/doc/source/_theme/theme.conf b/dashboard/doc/source/_theme/theme.conf new file mode 100755 index 00000000..e8748699 --- /dev/null +++ b/dashboard/doc/source/_theme/theme.conf @@ -0,0 +1,2 @@ +[theme] +inherit = default diff --git a/dashboard/doc/source/conf.py b/dashboard/doc/source/conf.py new file mode 100644 index 00000000..be34f745 --- /dev/null +++ b/dashboard/doc/source/conf.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 OpenStack Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Portas documentation build configuration file, created by +# sphinx-quickstart on Tue February 28 13:50:15 2013. +# +# This file is execfile()'d with the current directory set to its containing +# dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path = [os.path.abspath('../../tabula'), + os.path.abspath('../..')] + sys.path + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', + 'sphinx.ext.ifconfig', + 'sphinx.ext.graphviz'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = [] +if os.getenv('HUDSON_PUBLISH_DOCS'): + templates_path = ['_ga', '_templates'] +else: + templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Tabula' +copyright = u'2013, Mirantis, Inc' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +from tabula.version import version_info as tabula_version +# The full version, including alpha/beta/rc tags. +release = tabula_version.version_string_with_vcs() +# The short X.Y version. +version = tabula_version.canonical_version_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['api'] + +# The reST default role (for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +modindex_common_prefix = ['tabula.'] + +# -- Options for man page output -------------------------------------------- + +# Grouping the document tree for man pages. +# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' + +man_pages = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme_path = ["."] +html_theme = '_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = ['_theme'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' +git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" +html_last_updated_fmt = os.popen(git_cmd).read() + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +html_use_index = False + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'tabuladoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', 'Tabula.tex', u'Tabula Documentation', + u'Keero Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('http://docs.python.org/', None)} diff --git a/dashboard/doc/source/index.rst b/dashboard/doc/source/index.rst new file mode 100644 index 00000000..0de7d53d --- /dev/null +++ b/dashboard/doc/source/index.rst @@ -0,0 +1,65 @@ +.. + Copyright 2010 OpenStack Foundation + All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +============================================== +Welcome to Tabula, the Keero Project Web UI! +============================================== + +Tabula is a project that provides Web UI to Keero Project. + +This document describes Tabula for contributors of the project, and assumes +that you are already familiar with Portas from an `end-user perspective`_. + +.. _`end-user perspective`: http://keero.mirantis.com/ + +This documentation is generated by the Sphinx toolkit and lives in the source +tree. + +Installation Guide +================== +Install +------- +1. Check out sources to some directory (/keero):: + + user@work:~/$ git clone ssh://@gerrit.mirantis.com:29418/keero/keero.git + +2. Install virtualenv:: + + user@work:~/$ cd keero/tabula && sudo python ./tools/install_venv.py + +Configure +--------- +1. Copy configuration file from template:: + + user@work:~/$ cp keero/tabula/tabula/local/local_settings.py.example keero/tabula/tabula/local/local_settings.py + +2. Open configuration file for editing:: + + user@work:~/$ cd keero/tabula/tabula/local/ && nano local_settings.py + +2. Configure according to you environment:: + + ... + SECRET_KEY = 'some_random_value' + ... + OPENSTACK_HOST = "localhost" + ... + +Run +---- +Run Tabula in virtualenv:: + + user@work:~/$ cd keero/tabula && sudo ./tools/with_venv.sh python manage.py runserver 0.0.0.0:8080 diff --git a/dashboard/manage.py b/dashboard/manage.py new file mode 100755 index 00000000..d674cef8 --- /dev/null +++ b/dashboard/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +import sys +import os + + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabula.settings") + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) diff --git a/dashboard/openstack-common.conf b/dashboard/openstack-common.conf new file mode 100644 index 00000000..507dc200 --- /dev/null +++ b/dashboard/openstack-common.conf @@ -0,0 +1,7 @@ +[DEFAULT] + +# The list of modules to copy from openstack-common +modules=setup,importutils,version + +# The base module to hold the copy of openstack.common +base=tabula diff --git a/dashboard/packages/python-portasclient-2013.1.a345.ga70b44e.tar.gz b/dashboard/packages/python-portasclient-2013.1.a345.ga70b44e.tar.gz new file mode 100644 index 00000000..92ff304b Binary files /dev/null and b/dashboard/packages/python-portasclient-2013.1.a345.ga70b44e.tar.gz differ diff --git a/dashboard/run_tests.sh b/dashboard/run_tests.sh new file mode 100755 index 00000000..6dac0d35 --- /dev/null +++ b/dashboard/run_tests.sh @@ -0,0 +1,442 @@ +#!/bin/bash + +set -o errexit + +# ---------------UPDATE ME-------------------------------# +# Increment me any time the environment should be rebuilt. +# This includes dependncy changes, directory renames, etc. +# Simple integer secuence: 1, 2, 3... +environment_version=31 +#--------------------------------------------------------# + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Horizon's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically" + echo " if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" + echo " environment" + echo " -c, --coverage Generate reports using Coverage" + echo " -f, --force Force a clean re-build of the virtual" + echo " environment. Useful when dependencies have" + echo " been added." + echo " -m, --manage Run a Django management command." + echo " --makemessages Update all translation files." + echo " --compilemessages Compile all translation files." + echo " -p, --pep8 Just run pep8" + echo " -t, --tabs Check for tab characters in files." + echo " -y, --pylint Just run pylint" + echo " -q, --quiet Run non-interactively. (Relatively) quiet." + echo " Implies -V if -N is not set." + echo " --only-selenium Run only the Selenium unit tests" + echo " --with-selenium Run unit tests including Selenium tests" + echo " --runserver Run the Django development server for" + echo " openstack_dashboard in the virtual" + echo " environment." + echo " --docs Just build the documentation" + echo " --backup-environment Make a backup of the environment on exit" + echo " --restore-environment Restore the environment before running" + echo " --destroy-environment DEstroy the environment and exit" + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in" + echo " a virtual environment, If no virtualenv is found, the script will ask" + echo " if you would like to create one. If you prefer to run tests NOT in a" + echo " virtual environment, simply pass the -N option." + exit +} + +# DEFAULTS FOR RUN_TESTS.SH +# +root=`pwd` +venv=$root/.venv +with_venv=tools/with_venv.sh +included_dirs="openstack_dashboard horizon" + +always_venv=0 +backup_env=0 +command_wrapper="" +destroy=0 +force=0 +just_pep8=0 +just_pylint=0 +just_docs=0 +just_tabs=0 +never_venv=0 +quiet=0 +restore_env=0 +runserver=0 +only_selenium=0 +with_selenium=0 +testopts="" +testargs="" +with_coverage=0 +makemessages=0 +compilemessages=0 +manage=0 + +# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" +[ "$JOB_NAME" ] || JOB_NAME="default" + +function process_option { + case "$1" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -p|--pep8) just_pep8=1;; + -y|--pylint) just_pylint=1;; + -f|--force) force=1;; + -t|--tabs) just_tabs=1;; + -q|--quiet) quiet=1;; + -c|--coverage) with_coverage=1;; + -m|--manage) manage=1;; + --makemessages) makemessages=1;; + --compilemessages) compilemessages=1;; + --only-selenium) only_selenium=1;; + --with-selenium) with_selenium=1;; + --docs) just_docs=1;; + --runserver) runserver=1;; + --backup-environment) backup_env=1;; + --restore-environment) restore_env=1;; + --destroy-environment) destroy=1;; + -*) testopts="$testopts $1";; + *) testargs="$testargs $1" + esac +} + +function run_management_command { + ${command_wrapper} python $root/manage.py $testopts $testargs +} + +function run_server { + echo "Starting Django development server..." + ${command_wrapper} python $root/manage.py runserver $testopts $testargs + echo "Server stopped." +} + +function run_pylint { + echo "Running pylint ..." + PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true + CODE=$? + grep Global -A2 pylint.txt + if [ $CODE -lt 32 ]; then + echo "Completed successfully." + exit 0 + else + echo "Completed with problems." + exit $CODE + fi +} + +function run_pep8 { + echo "Running pep8 ..." + ${command_wrapper} pep8 $included_dirs +} + +function run_sphinx { + echo "Building sphinx..." + export DJANGO_SETTINGS_MODULE=openstack_dashboard.settings + ${command_wrapper} sphinx-build -b html doc/source doc/build/html + echo "Build complete." +} + +function tab_check { + TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` + if [ $TAB_VIOLATIONS -gt 0 ]; then + echo "TABS! $TAB_VIOLATIONS of them! Oh no!" + HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` + for TABBED_FILE in $HORIZON_FILES + do + TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` + if [ $TAB_COUNT -gt 0 ]; then + echo "$TABBED_FILE: $TAB_COUNT" + fi + done + fi + return $TAB_VIOLATIONS; +} + +function destroy_venv { + echo "Cleaning environment..." + echo "Removing virtualenv..." + rm -rf $venv + echo "Virtualenv removed." + rm -f .environment_version + echo "Environment cleaned." +} + +function environment_check { + echo "Checking environment." + if [ -f .environment_version ]; then + ENV_VERS=`cat .environment_version` + if [ $ENV_VERS -eq $environment_version ]; then + if [ -e ${venv} ]; then + # If the environment exists and is up-to-date then set our variables + command_wrapper="${root}/${with_venv}" + echo "Environment is up to date." + return 0 + fi + fi + fi + + if [ $always_venv -eq 1 ]; then + install_venv + else + if [ ! -e ${venv} ]; then + echo -e "Environment not found. Install? (Y/n) \c" + else + echo -e "Your environment appears to be out of date. Update? (Y/n) \c" + fi + read update_env + if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then + install_venv + else + # Set our command wrapper anyway. + command_wrapper="${root}/${with_venv}" + fi + fi +} + +function sanity_check { + # Anything that should be determined prior to running the tests, server, etc. + # Don't sanity-check anything environment-related in -N flag is set + if [ $never_venv -eq 0 ]; then + if [ ! -e ${venv} ]; then + echo "Virtualenv not found at $venv. Did install_venv.py succeed?" + exit 1 + fi + fi + # Remove .pyc files. This is sanity checking because they can linger + # after old files are deleted. + find . -name "*.pyc" -exec rm -rf {} \; +} + +function backup_environment { + if [ $backup_env -eq 1 ]; then + echo "Backing up environment \"$JOB_NAME\"..." + if [ ! -e ${venv} ]; then + echo "Environment not installed. Cannot back up." + return 0 + fi + if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then + mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old + rm -rf /tmp/.horizon_environment/$JOB_NAME + fi + mkdir -p /tmp/.horizon_environment/$JOB_NAME + cp -r $venv /tmp/.horizon_environment/$JOB_NAME/ + cp .environment_version /tmp/.horizon_environment/$JOB_NAME/ + # Remove the backup now that we've completed successfully + rm -rf /tmp/.horizon_environment/$JOB_NAME.old + echo "Backup completed" + fi +} + +function restore_environment { + if [ $restore_env -eq 1 ]; then + echo "Restoring environment from backup..." + if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then + echo "No backup to restore from." + return 0 + fi + + cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true + cp -r /tmp/.horizon_environment/$JOB_NAME/.environment_version ./ || true + + echo "Environment restored successfully." + fi +} + +function install_venv { + # Install with install_venv.py + export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} + export PIP_USE_MIRRORS=true + if [ $quiet -eq 1 ]; then + export PIP_NO_INPUT=true + fi + echo "Fetching new src packages..." + rm -rf $venv/src + python tools/install_venv.py + command_wrapper="$root/${with_venv}" + # Make sure it worked and record the environment version + sanity_check + chmod -R 754 $venv + echo $environment_version > .environment_version +} + +function run_tests { + sanity_check + + if [ $with_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + elif [ $only_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + export SKIP_UNITTESTS=1 + fi + + if [ -z "$testargs" ]; then + run_tests_all + else + run_tests_subset + fi +} + +function run_tests_subset { + project=`echo $testargs | awk -F. '{print $1}'` + ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs +} + +function run_tests_all { + echo "Running Horizon application tests" + export NOSE_XUNIT_FILE=horizon/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='horizon_nose_results.html' + fi + ${command_wrapper} coverage erase + ${command_wrapper} coverage run -p $root/manage.py test horizon --settings=horizon.test.settings $testopts + # get results of the Horizon tests + HORIZON_RESULT=$? + + echo "Running openstack_dashboard tests" + export NOSE_XUNIT_FILE=openstack_dashboard/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='dashboard_nose_results.html' + fi + ${command_wrapper} coverage run -p $root/manage.py test openstack_dashboard --settings=openstack_dashboard.test.settings $testopts + # get results of the openstack_dashboard tests + DASHBOARD_RESULT=$? + + if [ $with_coverage -eq 1 ]; then + echo "Generating coverage reports" + ${command_wrapper} coverage combine + ${command_wrapper} coverage xml -i --omit='/usr*,setup.py,*egg*,.venv/*' + ${command_wrapper} coverage html -i --omit='/usr*,setup.py,*egg*,.venv/*' -d reports + fi + # Remove the leftover coverage files from the -p flag earlier. + rm -f .coverage.* + + if [ $(($HORIZON_RESULT || $DASHBOARD_RESULT)) -eq 0 ]; then + echo "Tests completed successfully." + else + echo "Tests failed." + fi + exit $(($HORIZON_RESULT || $DASHBOARD_RESULT)) +} + +function run_makemessages { + cd horizon + ${command_wrapper} $root/manage.py makemessages --all --no-obsolete + HORIZON_PY_RESULT=$? + ${command_wrapper} $root/manage.py makemessages -d djangojs --all --no-obsolete + HORIZON_JS_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py makemessages --all --no-obsolete + DASHBOARD_RESULT=$? + cd .. + exit $(($HORIZON_PY_RESULT || $HORIZON_JS_RESULT || $DASHBOARD_RESULT)) +} + +function run_compilemessages { + cd horizon + ${command_wrapper} $root/manage.py compilemessages + HORIZON_PY_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py compilemessages + DASHBOARD_RESULT=$? + cd .. + exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT)) +} + + +# ---------PREPARE THE ENVIRONMENT------------ # + +# PROCESS ARGUMENTS, OVERRIDE DEFAULTS +for arg in "$@"; do + process_option $arg +done + +if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] +then + always_venv=1 +fi + +# If destroy is set, just blow it away and exit. +if [ $destroy -eq 1 ]; then + destroy_venv + exit 0 +fi + +# Ignore all of this if the -N flag was set +if [ $never_venv -eq 0 ]; then + + # Restore previous environment if desired + if [ $restore_env -eq 1 ]; then + restore_environment + fi + + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + destroy_venv + fi + + # Then check if it's up-to-date + environment_check + + # Create a backup of the up-to-date environment if desired + if [ $backup_env -eq 1 ]; then + backup_environment + fi +fi + +# ---------EXERCISE THE CODE------------ # + +# Run management commands +if [ $manage -eq 1 ]; then + run_management_command + exit $? +fi + +# Build the docs +if [ $just_docs -eq 1 ]; then + run_sphinx + exit $? +fi + +# Update translation files +if [ $makemessages -eq 1 ]; then + run_makemessages + exit $? +fi + +# Compile translation files +if [ $compilemessages -eq 1 ]; then + run_compilemessages + exit $? +fi + +# PEP8 +if [ $just_pep8 -eq 1 ]; then + run_pep8 + exit $? +fi + +# Pylint +if [ $just_pylint -eq 1 ]; then + run_pylint + exit $? +fi + +# Tab checker +if [ $just_tabs -eq 1 ]; then + tab_check + exit $? +fi + +# Django development server +if [ $runserver -eq 1 ]; then + run_server + exit $? +fi + +# Full test suite +run_tests || exit diff --git a/dashboard/setup.cfg b/dashboard/setup.cfg new file mode 100644 index 00000000..79034b3c --- /dev/null +++ b/dashboard/setup.cfg @@ -0,0 +1,9 @@ +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source + +[nosetests] +verbosity=2 +detailed-errors=1 + diff --git a/dashboard/setup.py b/dashboard/setup.py new file mode 100755 index 00000000..16865fbb --- /dev/null +++ b/dashboard/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import setuptools + +from tabula.openstack.common import setup + +requires = setup.parse_requirements() +depend_links = setup.parse_dependency_links() +project = 'tabula' + +setuptools.setup( + name=project, + version=setup.get_version(project, '2013.1'), + description="The OpenStack Dashboard.", + license='Apache 2.0', + author='OpenStack', + author_email='horizon@lists.launchpad.net', + url='https://github.com/openstack/horizon/', + packages=setuptools.find_packages(exclude=['bin']), + cmdclass=setup.get_cmdclass(), + include_package_data=True, + install_requires=requires, + dependency_links=depend_links, + classifiers=['Development Status :: 5 - Production/Stable', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Environment :: OpenStack'] +) diff --git a/dashboard/tabula/__init__.py b/dashboard/tabula/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/tabula/local/__init__.py b/dashboard/tabula/local/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/tabula/local/local_settings.py.example b/dashboard/tabula/local/local_settings.py.example new file mode 100644 index 00000000..d841e6a2 --- /dev/null +++ b/dashboard/tabula/local/local_settings.py.example @@ -0,0 +1,147 @@ +import os + +from django.utils.translation import ugettext_lazy as _ + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +# Set SSL proxy settings: +# For Django 1.4+ pass this header from the proxy after terminating the SSL, +# and don't forget to strip it from the client's request. +# For more information see: +# https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') + +# Specify a regular expression to validate user passwords. +# HORIZON_CONFIG = { +# "password_validator": { +# "regex": '.*', +# "help_text": _("Your password does not meet the requirements.") +# }, +# 'help_url': "http://docs.openstack.org" +# } + +LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Set custom secret key: +# You can either set it to a specific value or you can let horizion generate a +# default secret key that is unique on this machine, e.i. regardless of the +# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there +# may be situations where you would want to set this explicitly, e.g. when +# multiple dashboard instances are distributed on different machines (usually +# behind a load-balancer). Either you have to make sure that a session gets all +# requests routed to the same dashboard instance or you set the same SECRET_KEY +# for all of them. +# from horizon.utils import secret_key +# SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, '.secret_key_store')) + +# We recommend you use memcached for development; otherwise after every reload +# of the django development server, you will have to login again. To use +# memcached set CACHE_BACKED to something like 'memcached://127.0.0.1:11211/' +CACHE_BACKEND = 'locmem://' + +# Send email to the console by default +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Or send them to /dev/null +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + +# Configure these for your outgoing email host +# EMAIL_HOST = 'smtp.my-company.com' +# EMAIL_PORT = 25 +# EMAIL_HOST_USER = 'djangomail' +# EMAIL_HOST_PASSWORD = 'top-secret!' + +# For multiple regions uncomment this configuration, and add (endpoint, title). +# AVAILABLE_REGIONS = [ +# ('http://cluster1.example.com:5000/v2.0', 'cluster1'), +# ('http://cluster2.example.com:5000/v2.0', 'cluster2'), +# ] + +OPENSTACK_HOST = "127.0.0.1" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" + +# Disable SSL certificate checks (useful for self-signed certificates): +# OPENSTACK_SSL_NO_VERIFY = True + +# The OPENSTACK_KEYSTONE_BACKEND settings can be used to identify the +# capabilities of the auth backend for Keystone. +# If Keystone has been configured to use LDAP as the auth backend then set +# can_edit_user to False and name to 'ldap'. +# +# TODO(tres): Remove these once Keystone has an API to identify auth backend. +OPENSTACK_KEYSTONE_BACKEND = { + 'name': 'native', + 'can_edit_user': True +} + +OPENSTACK_HYPERVISOR_FEATURES = { + 'can_set_mount_point': True +} + +# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is 'internalURL'. +#OPENSTACK_ENDPOINT_TYPE = "publicURL" + +# The number of objects (Swift containers/objects or images) to display +# on a single page before providing a paging element (a "more" link) +# to paginate results. +API_RESULT_LIMIT = 1000 +API_RESULT_PAGE_SIZE = 20 + +# The timezone of the server. This should correspond with the timezone +# of your entire OpenStack installation, and hopefully be in UTC. +TIME_ZONE = "UTC" + +LOGGING = { + 'version': 1, + # When set to True this will disable all logging except + # for loggers specified in this configuration dictionary. Note that + # if nothing is specified here and disable_existing_loggers is True, + # django.db.backends will still log unless it is disabled explicitly. + 'disable_existing_loggers': False, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + 'console': { + # Set the level to "DEBUG" for verbose output logging. + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + # Logging from django.db.backends is VERY verbose, send to null + # by default. + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + 'handlers': ['console'], + 'propagate': False, + }, + 'openstack_dashboard': { + 'handlers': ['console'], + 'propagate': False, + }, + 'novaclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'keystoneclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'glanceclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'nose.plugins.manager': { + 'handlers': ['console'], + 'propagate': False, + } + } +} diff --git a/dashboard/tabula/models.py b/dashboard/tabula/models.py new file mode 100644 index 00000000..1b3d5f9e --- /dev/null +++ b/dashboard/tabula/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/dashboard/tabula/openstack/__init__.py b/dashboard/tabula/openstack/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/tabula/openstack/common/__init__.py b/dashboard/tabula/openstack/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/tabula/openstack/common/importutils.py b/dashboard/tabula/openstack/common/importutils.py new file mode 100644 index 00000000..3bd277f4 --- /dev/null +++ b/dashboard/tabula/openstack/common/importutils.py @@ -0,0 +1,67 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Import related utilities and helper functions. +""" + +import sys +import traceback + + +def import_class(import_str): + """Returns a class from a string including module and class""" + mod_str, _sep, class_str = import_str.rpartition('.') + try: + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + except (ValueError, AttributeError): + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) + + +def import_object(import_str, *args, **kwargs): + """Import a class and return an instance of it.""" + return import_class(import_str)(*args, **kwargs) + + +def import_object_ns(name_space, import_str, *args, **kwargs): + """ + Import a class and return an instance of it, first by trying + to find the class in a default namespace, then failing back to + a full path if not found in the default namespace. + """ + import_value = "%s.%s" % (name_space, import_str) + try: + return import_class(import_value)(*args, **kwargs) + except ImportError: + return import_class(import_str)(*args, **kwargs) + + +def import_module(import_str): + """Import a module.""" + __import__(import_str) + return sys.modules[import_str] + + +def try_import(import_str, default=None): + """Try to import a module and if it fails return default.""" + try: + return import_module(import_str) + except ImportError: + return default diff --git a/dashboard/tabula/openstack/common/setup.py b/dashboard/tabula/openstack/common/setup.py new file mode 100644 index 00000000..dec74fd0 --- /dev/null +++ b/dashboard/tabula/openstack/common/setup.py @@ -0,0 +1,367 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack Foundation. +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Utilities with minimum-depends for use in setup.py +""" + +import email +import os +import re +import subprocess +import sys + +from setuptools.command import sdist + + +def parse_mailmap(mailmap='.mailmap'): + mapping = {} + if os.path.exists(mailmap): + with open(mailmap, 'r') as fp: + for l in fp: + try: + canonical_email, alias = re.match( + r'[^#]*?(<.+>).*(<.+>).*', l).groups() + except AttributeError: + continue + mapping[alias] = canonical_email + return mapping + + +def _parse_git_mailmap(git_dir, mailmap='.mailmap'): + mailmap = os.path.join(os.path.dirname(git_dir), mailmap) + return parse_mailmap(mailmap) + + +def canonicalize_emails(changelog, mapping): + """Takes in a string and an email alias mapping and replaces all + instances of the aliases in the string with their real email. + """ + for alias, email_address in mapping.iteritems(): + changelog = changelog.replace(alias, email_address) + return changelog + + +# Get requirements from the first file that exists +def get_reqs_from_files(requirements_files): + for requirements_file in requirements_files: + if os.path.exists(requirements_file): + with open(requirements_file, 'r') as fil: + return fil.read().split('\n') + return [] + + +def parse_requirements(requirements_files=['requirements.txt', + 'tools/pip-requires']): + requirements = [] + for line in get_reqs_from_files(requirements_files): + # For the requirements list, we need to inject only the portion + # after egg= so that distutils knows the package it's looking for + # such as: + # -e git://github.com/openstack/nova/master#egg=nova + if re.match(r'\s*-e\s+', line): + requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', + line)) + # such as: + # http://github.com/openstack/nova/zipball/master#egg=nova + elif re.match(r'\s*https?:', line): + requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', + line)) + # -f lines are for index locations, and don't get used here + elif re.match(r'\s*-f\s+', line): + pass + # argparse is part of the standard library starting with 2.7 + # adding it to the requirements list screws distro installs + elif line == 'argparse' and sys.version_info >= (2, 7): + pass + else: + requirements.append(line) + + return requirements + + +def parse_dependency_links(requirements_files=['requirements.txt', + 'tools/pip-requires']): + dependency_links = [] + # dependency_links inject alternate locations to find packages listed + # in requirements + for line in get_reqs_from_files(requirements_files): + # skip comments and blank lines + if re.match(r'(\s*#)|(\s*$)', line): + continue + # lines with -e or -f need the whole line, minus the flag + if re.match(r'\s*-[ef]\s+', line): + dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) + # lines that are only urls can go in unmolested + elif re.match(r'\s*https?:', line): + dependency_links.append(line) + return dependency_links + + +def _run_shell_command(cmd, throw_on_error=False): + if os.name == 'nt': + output = subprocess.Popen(["cmd.exe", "/C", cmd], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + else: + output = subprocess.Popen(["/bin/sh", "-c", cmd], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out = output.communicate() + if output.returncode and throw_on_error: + raise Exception("%s returned %d" % cmd, output.returncode) + if len(out) == 0: + return None + if len(out[0].strip()) == 0: + return None + return out[0].strip() + + +def _get_git_directory(): + parent_dir = os.path.dirname(__file__) + while True: + git_dir = os.path.join(parent_dir, '.git') + if os.path.exists(git_dir): + return git_dir + parent_dir, child = os.path.split(parent_dir) + if not child: # reached to root dir + return None + + +def write_git_changelog(): + """Write a changelog based on the git changelog.""" + new_changelog = 'ChangeLog' + git_dir = _get_git_directory() + if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): + if git_dir: + git_log_cmd = 'git --git-dir=%s log' % git_dir + changelog = _run_shell_command(git_log_cmd) + mailmap = _parse_git_mailmap(git_dir) + with open(new_changelog, "w") as changelog_file: + changelog_file.write(canonicalize_emails(changelog, mailmap)) + else: + open(new_changelog, 'w').close() + + +def generate_authors(): + """Create AUTHORS file using git commits.""" + jenkins_email = 'jenkins@review.(openstack|stackforge).org' + old_authors = 'AUTHORS.in' + new_authors = 'AUTHORS' + git_dir = _get_git_directory() + if not os.getenv('SKIP_GENERATE_AUTHORS'): + if git_dir: + # don't include jenkins email address in AUTHORS file + git_log_cmd = ("git --git-dir=" + git_dir + + " log --format='%aN <%aE>' | sort -u | " + "egrep -v '" + jenkins_email + "'") + changelog = _run_shell_command(git_log_cmd) + signed_cmd = ("git log --git-dir=" + git_dir + + " | grep -i Co-authored-by: | sort -u") + signed_entries = _run_shell_command(signed_cmd) + if signed_entries: + new_entries = "\n".join( + [signed.split(":", 1)[1].strip() + for signed in signed_entries.split("\n") if signed]) + changelog = "\n".join((changelog, new_entries)) + mailmap = _parse_git_mailmap(git_dir) + with open(new_authors, 'w') as new_authors_fh: + new_authors_fh.write(canonicalize_emails(changelog, mailmap)) + if os.path.exists(old_authors): + with open(old_authors, "r") as old_authors_fh: + new_authors_fh.write('\n' + old_authors_fh.read()) + else: + open(new_authors, 'w').close() + + +_rst_template = """%(heading)s +%(underline)s + +.. automodule:: %(module)s + :members: + :undoc-members: + :show-inheritance: +""" + + +def get_cmdclass(): + """Return dict of commands to run from setup.py.""" + + cmdclass = dict() + + def _find_modules(arg, dirname, files): + for filename in files: + if filename.endswith('.py') and filename != '__init__.py': + arg["%s.%s" % (dirname.replace('/', '.'), + filename[:-3])] = True + + class LocalSDist(sdist.sdist): + """Builds the ChangeLog and Authors files from VC first.""" + + def run(self): + write_git_changelog() + generate_authors() + # sdist.sdist is an old style class, can't use super() + sdist.sdist.run(self) + + cmdclass['sdist'] = LocalSDist + + # If Sphinx is installed on the box running setup.py, + # enable setup.py to build the documentation, otherwise, + # just ignore it + try: + from sphinx.setup_command import BuildDoc + + class LocalBuildDoc(BuildDoc): + + builders = ['html', 'man'] + + def generate_autoindex(self): + print "**Autodocumenting from %s" % os.path.abspath(os.curdir) + modules = {} + option_dict = self.distribution.get_option_dict('build_sphinx') + source_dir = os.path.join(option_dict['source_dir'][1], 'api') + if not os.path.exists(source_dir): + os.makedirs(source_dir) + for pkg in self.distribution.packages: + if '.' not in pkg: + os.path.walk(pkg, _find_modules, modules) + module_list = modules.keys() + module_list.sort() + autoindex_filename = os.path.join(source_dir, 'autoindex.rst') + with open(autoindex_filename, 'w') as autoindex: + autoindex.write(""".. toctree:: + :maxdepth: 1 + +""") + for module in module_list: + output_filename = os.path.join(source_dir, + "%s.rst" % module) + heading = "The :mod:`%s` Module" % module + underline = "=" * len(heading) + values = dict(module=module, heading=heading, + underline=underline) + + print "Generating %s" % output_filename + with open(output_filename, 'w') as output_file: + output_file.write(_rst_template % values) + autoindex.write(" %s.rst\n" % module) + + def run(self): + if not os.getenv('SPHINX_DEBUG'): + self.generate_autoindex() + + for builder in self.builders: + self.builder = builder + self.finalize_options() + self.project = self.distribution.get_name() + self.version = self.distribution.get_version() + self.release = self.distribution.get_version() + BuildDoc.run(self) + + class LocalBuildLatex(LocalBuildDoc): + builders = ['latex'] + + cmdclass['build_sphinx'] = LocalBuildDoc + cmdclass['build_sphinx_latex'] = LocalBuildLatex + except ImportError: + pass + + return cmdclass + + +def _get_revno(git_dir): + """Return the number of commits since the most recent tag. + + We use git-describe to find this out, but if there are no + tags then we fall back to counting commits since the beginning + of time. + """ + describe = _run_shell_command( + "git --git-dir=%s describe --always" % git_dir) + if "-" in describe: + return describe.rsplit("-", 2)[-2] + + # no tags found + revlist = _run_shell_command( + "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir) + return len(revlist.splitlines()) + + +def _get_version_from_git(pre_version): + """Return a version which is equal to the tag that's on the current + revision if there is one, or tag plus number of additional revisions + if the current revision has no tag.""" + + git_dir = _get_git_directory() + if git_dir: + if pre_version: + try: + return _run_shell_command( + "git --git-dir=" + git_dir + " describe --exact-match", + throw_on_error=True).replace('-', '.') + except Exception: + sha = _run_shell_command( + "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h") + return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha) + else: + return _run_shell_command( + "git --git-dir=" + git_dir + " describe --always").replace( + '-', '.') + return None + + +def _get_version_from_pkg_info(package_name): + """Get the version from PKG-INFO file if we can.""" + try: + pkg_info_file = open('PKG-INFO', 'r') + except (IOError, OSError): + return None + try: + pkg_info = email.message_from_file(pkg_info_file) + except email.MessageError: + return None + # Check to make sure we're in our own dir + if pkg_info.get('Name', None) != package_name: + return None + return pkg_info.get('Version', None) + + +def get_version(package_name, pre_version=None): + """Get the version of the project. First, try getting it from PKG-INFO, if + it exists. If it does, that means we're in a distribution tarball or that + install has happened. Otherwise, if there is no PKG-INFO file, pull the + version from git. + + We do not support setup.py version sanity in git archive tarballs, nor do + we support packagers directly sucking our git repo into theirs. We expect + that a source tarball be made from our git repo - or that if someone wants + to make a source tarball from a fork of our repo with additional tags in it + that they understand and desire the results of doing that. + """ + version = os.environ.get("OSLO_PACKAGE_VERSION", None) + if version: + return version + version = _get_version_from_pkg_info(package_name) + if version: + return version + version = _get_version_from_git(pre_version) + if version: + return version + raise Exception("Versioning for this project requires either an sdist" + " tarball, or access to an upstream git repository.") diff --git a/dashboard/tabula/openstack/common/version.py b/dashboard/tabula/openstack/common/version.py new file mode 100644 index 00000000..b2d4a79b --- /dev/null +++ b/dashboard/tabula/openstack/common/version.py @@ -0,0 +1,94 @@ + +# Copyright 2012 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Utilities for consuming the version from pkg_resources. +""" + +import pkg_resources + + +class VersionInfo(object): + + def __init__(self, package): + """Object that understands versioning for a package + :param package: name of the python package, such as glance, or + python-glanceclient + """ + self.package = package + self.release = None + self.version = None + self._cached_version = None + + def __str__(self): + """Make the VersionInfo object behave like a string.""" + return self.version_string() + + def __repr__(self): + """Include the name.""" + return "VersionInfo(%s:%s)" % (self.package, self.version_string()) + + def _get_version_from_pkg_resources(self): + """Get the version of the package from the pkg_resources record + associated with the package.""" + try: + requirement = pkg_resources.Requirement.parse(self.package) + provider = pkg_resources.get_provider(requirement) + return provider.version + except pkg_resources.DistributionNotFound: + # The most likely cause for this is running tests in a tree + # produced from a tarball where the package itself has not been + # installed into anything. Revert to setup-time logic. + from tabula.openstack.common import setup + return setup.get_version(self.package) + + def release_string(self): + """Return the full version of the package including suffixes indicating + VCS status. + """ + if self.release is None: + self.release = self._get_version_from_pkg_resources() + + return self.release + + def version_string(self): + """Return the short version minus any alpha/beta tags.""" + if self.version is None: + parts = [] + for part in self.release_string().split('.'): + if part[0].isdigit(): + parts.append(part) + else: + break + self.version = ".".join(parts) + + return self.version + + # Compatibility functions + canonical_version_string = version_string + version_string_with_vcs = release_string + + def cached_version_string(self, prefix=""): + """Generate an object which will expand in a string context to + the results of version_string(). We do this so that don't + call into pkg_resources every time we start up a program when + passing version information into the CONF constructor, but + rather only do the calculation when and if a version is requested + """ + if not self._cached_version: + self._cached_version = "%s%s" % (prefix, + self.version_string()) + return self._cached_version diff --git a/dashboard/tabula/settings.py b/dashboard/tabula/settings.py new file mode 100644 index 00000000..e2bb24a2 --- /dev/null +++ b/dashboard/tabula/settings.py @@ -0,0 +1,148 @@ +import logging +import os +import sys + +from openstack_dashboard import exceptions + +ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) +BIN_DIR = os.path.abspath(os.path.join(ROOT_PATH, '..', 'bin')) + +if ROOT_PATH not in sys.path: + sys.path.append(ROOT_PATH) + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +SITE_BRANDING = 'OpenStack Dashboard' + +LOGIN_URL = '/auth/login/' +LOGOUT_URL = '/auth/logout/' +# LOGIN_REDIRECT_URL can be used as an alternative for +# HORIZON_CONFIG.user_home, if user_home is not set. +# Do not set it to '/home/', as this will cause circular redirect loop +LOGIN_REDIRECT_URL = '/' + +MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media')) +MEDIA_URL = '/media/' +STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static')) +STATIC_URL = '/static/' +ADMIN_MEDIA_PREFIX = '/static/admin/' + +ROOT_URLCONF = 'openstack_dashboard.urls' + +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, + 'customization_module': 'tabula.overrides' +} + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'horizon.middleware.HorizonMiddleware', + 'django.middleware.doc.XViewMiddleware', + 'django.middleware.locale.LocaleMiddleware', +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.request', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.messages.context_processors.messages', + 'horizon.context_processors.horizon', +) + +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'horizon.loaders.TemplateLoader' +) + +TEMPLATE_DIRS = ( + os.path.join(ROOT_PATH, 'templates'), +) + +STATICFILES_FINDERS = ( + 'compressor.finders.CompressorFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + +less_binary = os.path.join(BIN_DIR, 'less', 'lessc') +COMPRESS_PRECOMPILERS = ( + ('text/less', (less_binary + ' {infile} {outfile}')), +) + +COMPRESS_CSS_FILTERS = ( + 'compressor.filters.css_default.CssAbsoluteFilter', +) + +COMPRESS_ENABLED = True +COMPRESS_OUTPUT_DIR = 'windc' +COMPRESS_CSS_HASHING_METHOD = 'hash' +COMPRESS_PARSER = 'compressor.parser.HtmlParser' + +INSTALLED_APPS = ( + 'openstack_dashboard', + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'compressor', + 'horizon', + 'openstack_dashboard.dashboards.project', + 'openstack_dashboard.dashboards.admin', + 'openstack_dashboard.dashboards.settings', + 'openstack_auth' +) + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',) +MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' + +SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' +SESSION_COOKIE_HTTPONLY = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +SESSION_COOKIE_SECURE = False + +gettext_noop = lambda s: s +LANGUAGES = ( + ('en', gettext_noop('English')), + ('it', gettext_noop('Italiano')), + ('es', gettext_noop('Spanish')), + ('fr', gettext_noop('French')), + ('ja', gettext_noop('Japanese')), + ('pt', gettext_noop('Portuguese')), + ('pl', gettext_noop('Polish')), + ('zh-cn', gettext_noop('Simplified Chinese')), + ('zh-tw', gettext_noop('Traditional Chinese')), +) +LANGUAGE_CODE = 'en' +USE_I18N = True +USE_L10N = True +USE_TZ = True + +OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' + +DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter' + +try: + from local.local_settings import * +except ImportError: + logging.warning("No local_settings file found.") + +if DEBUG: + logging.basicConfig(level=logging.DEBUG) diff --git a/dashboard/tabula/tabula/__init__.py b/dashboard/tabula/tabula/__init__.py new file mode 100644 index 00000000..eb97e8c1 --- /dev/null +++ b/dashboard/tabula/tabula/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/dashboard/tabula/tabula/api.py b/dashboard/tabula/tabula/api.py new file mode 100644 index 00000000..03130d0e --- /dev/null +++ b/dashboard/tabula/tabula/api.py @@ -0,0 +1,211 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from glazierclient.v1.client import Client as glazier_client + +log = logging.getLogger(__name__) + + +def glazierclient(request): + url = "http://127.0.0.1:8082" + log.debug('glazierclient connection created using token "%s" and url "%s"' + % (request.user.token, url)) + return glazier_client(endpoint=url, token=request.user.token.token['id']) + + +def environment_create(request, parameters): + env = glazierclient(request).environments.create(parameters.get('name', '')) + log.debug('Environment::Create {0}'.format(env)) + return env + + +def environment_delete(request, environment_id): + result = glazierclient(request).environments.delete(environment_id) + log.debug('Environment::Delete Id:{0}'.format(environment_id)) + return result + + +def environment_get(request, environment_id): + env = glazierclient(request).environments.get(environment_id) + log.debug('Environment::Get {0}'.format(env)) + return env + + +def environments_list(request): + log.debug('Environment::List') + return glazierclient(request).environments.list() + + +def environment_deploy(request, environment_id): + sessions = glazierclient(request).sessions.list(environment_id) + for session in sessions: + if session.state == 'open': + session_id = session.id + if not session_id: + return "Sorry, nothing to deploy." + log.debug('Obtained session with Id: {0}'.format(session_id)) + result = glazierclient(request).sessions.deploy(environment_id, session_id) + log.debug('Environment with Id: {0} deployed in session ' + 'with Id: {1}'.format(environment_id, session_id)) + return result + + +def service_create(request, environment_id, parameters): + session_id = None + sessions = glazierclient(request).sessions.list(environment_id) + + for s in sessions: + if s.state == 'open': + session_id = s.id + else: + glazierclient(request).sessions.delete(environment_id, s.id) + + if session_id is None: + session_id = glazierclient(request).sessions.configure(environment_id).id + + if parameters['service_type'] == 'Active Directory': + service = glazierclient(request)\ + .activeDirectories\ + .create(environment_id, session_id, parameters) + else: + service = glazierclient(request)\ + .webServers.create(environment_id, session_id, parameters) + + log.debug('Service::Create {0}'.format(service)) + return service + + +def get_time(obj): + return obj.updated + + +def services_list(request, environment_id): + services = [] + session_id = None + sessions = glazierclient(request).sessions.list(environment_id) + for s in sessions: + session_id = s.id + + if session_id: + services = glazierclient(request).activeDirectories.\ + list(environment_id, session_id) + services += glazierclient(request).webServers.\ + list(environment_id, session_id) + + for i in range(len(services)): + reports = glazierclient(request).sessions. \ + reports(environment_id, session_id, + services[i].id) + + for report in reports: + services[i].operation = report.text + + log.debug('Service::List') + return services + + +def get_active_directories(request, environment_id): + services = [] + session_id = None + sessions = glazierclient(request).sessions.list(environment_id) + + for s in sessions: + session_id = s.id + + if session_id: + services = glazierclient(request)\ + .activeDirectories\ + .list(environment_id, session_id) + + log.debug('Service::Active Directories::List') + return services + + +def service_get(request, environment_id, service_id): + services = services_list(request, environment_id) + + for service in services: + if service.id == service_id: + log.debug('Service::Get {0}'.format(service)) + return service + + +def get_data_center_id_for_service(request, service_id): + environments = environments_list(request) + + for environment in environments: + services = services_list(request, environment.id) + for service in services: + if service.id == service_id: + return environment.id + + +def get_service_datails(request, service_id): + environments = environments_list(request) + services = [] + for environment in environments: + services += services_list(request, environment.id) + + for service in services: + if service.id == service_id: + return service + + +def get_status_message_for_service(request, service_id): + environment_id = get_data_center_id_for_service(request, service_id) + session_id = None + sessions = glazierclient(request).sessions.list(environment_id) + + for s in sessions: + session_id = s.id + + if session_id: + reports = glazierclient(request).sessions.\ + reports(environment_id, session_id, service_id) + + result = 'Initialization.... \n' + for report in reports: + result += ' ' + str(report.text) + '\n' + + return result + + +def service_delete(request, environment_id, service_id): + log.debug('Service::Remove EnvId: {0} ' + 'SrvId: {1}'.format(environment_id, service_id)) + + services = services_list(request, environment_id) + + session_id = None + sessions = glazierclient(request).sessions.list(environment_id) + for session in sessions: + if session.state == 'open': + session_id = session.id + + if session_id is None: + raise Exception("Sorry, you can not delete this service now.") + + for service in services: + if service.id is service_id: + if service.type is 'Active Directory': + glazierclient(request).activeDirectories.delete(environment_id, + session_id, + service_id) + elif service.type is 'IIS': + glazierclient(request).webServers.delete(environment_id, + session_id, + service_id) diff --git a/dashboard/tabula/tabula/forms.py b/dashboard/tabula/tabula/forms.py new file mode 100644 index 00000000..e627f340 --- /dev/null +++ b/dashboard/tabula/tabula/forms.py @@ -0,0 +1,120 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import string + +from django import forms +from django.utils.translation import ugettext_lazy as _ +import re +from tabula.tabula import api + +log = logging.getLogger(__name__) + + +class PasswordField(forms.CharField): + # Setup the Field + def __init__(self, label, *args, **kwargs): + super(PasswordField, self).__init__(min_length=7, required=True, + label=label, + widget=forms.PasswordInput( + render_value=False), + *args, **kwargs) + + def clean(self, value): + + # Setup Our Lists of Characters and Numbers + characters = list(string.letters) + special_characters = '!@#$%^&*()_+|\/.,~?><:{}' + numbers = [str(i) for i in range(10)] + + # Assume False until Proven Otherwise + numCheck = False + charCheck = False + specCharCheck = False + + # Loop until we Match + for char in value: + if not charCheck: + if char in characters: + charCheck = True + if not specCharCheck: + if char in special_characters: + specCharCheck = True + if not numCheck: + if char in numbers: + numCheck = True + if numCheck and charCheck and specCharCheck: + break + + if not numCheck or not charCheck or not specCharCheck: + raise forms.ValidationError(u'Your password must include at least \ + one letter, at least one number and \ + at least one special character.') + + return super(PasswordField, self).clean(value) + + +class WizardFormServiceType(forms.Form): + service = forms.ChoiceField(label=_('Service Type'), + choices=[ + ('Active Directory', 'Active Directory'), + ('IIS', 'Internet Information Services') + ]) + + +class WizardFormConfiguration(forms.Form): + 'The functions for this class will dynamically create in views.py' + pass + + +class WizardFormADConfiguration(forms.Form): + dc_name = forms.CharField(label=_('Domain Name'), + required=True) + + dc_count = forms.IntegerField(label=_('Instances Count'), + required=True, + min_value=1, + max_value=100, + initial=1) + + adm_password = PasswordField(_('Administrator password')) + + recovery_password = PasswordField(_('Recovery password')) + + def __init__(self, request, *args, **kwargs): + super(WizardFormADConfiguration, self).__init__(*args, **kwargs) + + +class WizardFormIISConfiguration(forms.Form): + iis_name = forms.CharField(label=_('IIS Server Name'), + required=True) + + adm_password = PasswordField(_('Administrator password')) + + iis_domain = forms.ChoiceField(label=_('Member of the Domain'), + required=False) + + def __init__(self, request, *args, **kwargs): + super(WizardFormIISConfiguration, self).__init__(*args, **kwargs) + + link = request.__dict__['META']['HTTP_REFERER'] + environment_id = re.search('tabula/(\S+)', link).group(0)[6:-1] + + domains = api.get_active_directories(request, environment_id) + + self.fields['iis_domain'].choices = [("", "")] + \ + [(domain.name, domain.name) + for domain in domains] diff --git a/dashboard/tabula/tabula/overrides.py b/dashboard/tabula/tabula/overrides.py new file mode 100644 index 00000000..849eaefc --- /dev/null +++ b/dashboard/tabula/tabula/overrides.py @@ -0,0 +1,21 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import horizon + +from panel import tabula + +project = horizon.get_dashboard('project') +project.register(tabula) diff --git a/dashboard/tabula/tabula/panel.py b/dashboard/tabula/tabula/panel.py new file mode 100644 index 00000000..6acae86b --- /dev/null +++ b/dashboard/tabula/tabula/panel.py @@ -0,0 +1,26 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import horizon +from django.utils.translation import ugettext_lazy as _ +from openstack_dashboard.dashboards.project import dashboard + + +class tabula(horizon.Panel): + name = _("Environments") + slug = 'tabula' + + +dashboard.Project.register(tabula) diff --git a/dashboard/tabula/tabula/tables.py b/dashboard/tabula/tabula/tables.py new file mode 100644 index 00000000..48ae8a09 --- /dev/null +++ b/dashboard/tabula/tabula/tables.py @@ -0,0 +1,193 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re + +from django.utils.translation import ugettext_lazy as _ +from horizon import messages +from horizon import tables +from tabula.tabula import api + + +LOG = logging.getLogger(__name__) + + +class CreateService(tables.LinkAction): + name = 'CreateService' + verbose_name = _('Create Service') + url = 'horizon:project:tabula:create' + classes = ('btn-launch', 'ajax-modal') + + def allowed(self, request, datum): + return True + + def action(self, request, service): + api.service_create(request, service) + + +class CreateEnvironment(tables.LinkAction): + name = 'CreateEnvironment' + verbose_name = _('Create Environment') + url = 'horizon:project:tabula:create_dc' + classes = ('btn-launch', 'ajax-modal') + + def allowed(self, request, datum): + return True + + def action(self, request, environment): + api.environment_create(request, environment) + + +class DeleteEnvironment(tables.BatchAction): + name = 'delete' + action_present = _('Delete') + action_past = _('Delete') + data_type_singular = _('Environment') + data_type_plural = _('Environment') + classes = ('btn-danger', 'btn-terminate') + + def allowed(self, request, datum): + return True + + def action(self, request, environment_id): + api.environment_delete(request, environment_id) + + +class DeleteService(tables.BatchAction): + name = 'delete' + action_present = _('Delete') + action_past = _('Delete') + data_type_singular = _('Service') + data_type_plural = _('Service') + classes = ('btn-danger', 'btn-terminate') + + def allowed(self, request, datum): + return True + + def action(self, request, service_id): + link = request.__dict__['META']['HTTP_REFERER'] + datacenter_id = re.search('tabula/(\S+)', link).group(0)[6:-1] + + try: + api.service_delete(request, datacenter_id, service_id) + except: + messages.error(request, _('Sorry, you can not delete this ' + 'service right now.')) + + +class DeployEnvironment(tables.BatchAction): + name = 'deploy' + action_present = _('Deploy') + action_past = _('Deploy') + data_type_singular = _('Environment') + data_type_plural = _('Environment') + classes = 'btn-launch' + + def allowed(self, request, datum): + return True + + def action(self, request, environment_id): + return api.environment_deploy(request, environment_id) + + +class ShowDataCenterServices(tables.LinkAction): + name = 'edit' + verbose_name = _('Services') + url = 'horizon:project:tabula:services' + + def allowed(self, request, instance): + return True + + +class UpdateDCRow(tables.Row): + ajax = True + + def get_data(self, request, environment_id): + return api.environment_get(request, environment_id) + + +class UpdateServiceRow(tables.Row): + ajax = True + + def get_data(self, request, service_id): + + link = request.__dict__['META']['HTTP_REFERER'] + environment_id = re.search('tabula/(\S+)', link).group(0)[6:-1] + + service = api.service_get(request, environment_id, service_id) + + return service + + +STATUS_DISPLAY_CHOICES = ( + ('draft', 'Ready to deploy'), + ('pending', 'Wait for configuration'), + ('inprogress', 'Deploy in progress'), + ('finished', 'Active') +) + + +class DCTable(tables.DataTable): + STATUS_CHOICES = ( + (None, True), + ('Ready to deploy', True), + ('Active', True) + ) + + name = tables.Column('name', + link=('horizon:project:tabula:services'), + verbose_name=_('Name')) + + status = tables.Column('status', verbose_name=_('Status'), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES) + + class Meta: + name = 'tabula' + verbose_name = _('Environment') + row_class = UpdateDCRow + status_columns = ['status'] + table_actions = (CreateDataCenter,) + row_actions = (ShowDataCenterServices, DeleteDataCenter, + DeployDataCenter) + + +class ServicesTable(tables.DataTable): + STATUS_CHOICES = ( + (None, True), + ('Ready to deploy', True), + ('Active', True) + ) + + name = tables.Column('name', verbose_name=_('Name'), + link=('horizon:project:tabula:service_details')) + + _type = tables.Column('service_type', verbose_name=_('Type')) + + status = tables.Column('status', verbose_name=_('Status'), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES) + + operation = tables.Column('operation', verbose_name=_('Operation')) + + class Meta: + name = 'services' + verbose_name = _('Services') + row_class = UpdateServiceRow + status_columns = ['status'] + table_actions = (CreateService,) diff --git a/dashboard/tabula/tabula/tabs.py b/dashboard/tabula/tabula/tabs.py new file mode 100644 index 00000000..d47bbd80 --- /dev/null +++ b/dashboard/tabula/tabula/tabs.py @@ -0,0 +1,58 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs +import logging + +from tabula.tabula import api + + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Service") + slug = "_service" + template_name = '_services.html' + + def get_context_data(self, request): + data = self.tab_group.kwargs['service'] + + return {"service_name": data.name, + "service_status": data.status, + "service_type": data.service_type, + "service_domain": data.domain} + + +class LogsTab(tabs.Tab): + name = _("Logs") + slug = "_logs" + template_name = '_service_logs.html' + + def get_context_data(self, request): + data = self.tab_group.kwargs['service'] + + reports = api.get_status_message_for_service(request, data.id) + + return {"reports": reports} + + +class ServicesTabs(tabs.TabGroup): + slug = "services_details" + tabs = (OverviewTab, LogsTab) + sticky = True diff --git a/dashboard/tabula/tabula/urls.py b/dashboard/tabula/tabula/urls.py new file mode 100644 index 00000000..9969b396 --- /dev/null +++ b/dashboard/tabula/tabula/urls.py @@ -0,0 +1,36 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, Services, CreateDCView, DetailServiceView +from .views import Wizard +from .forms import WizardFormServiceType, WizardFormConfiguration + +VIEW_MOD = 'openstack_dashboard.dashboards.project.tabula.views' + +urlpatterns = patterns(VIEW_MOD, + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create$', + Wizard.as_view([WizardFormServiceType, + WizardFormConfiguration]), + name='create'), + url(r'^create_dc$', CreateDCView.as_view(), + name='create_dc'), + url(r'^(?P[^/]+)/$', + Services.as_view(), name='services'), + url(r'^(?P[^/]+)/details$', + DetailServiceView.as_view(), + name='service_details')) diff --git a/dashboard/tabula/tabula/views.py b/dashboard/tabula/tabula/views.py new file mode 100644 index 00000000..81fa5cd1 --- /dev/null +++ b/dashboard/tabula/tabula/views.py @@ -0,0 +1,204 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re + +from django.views import generic +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ +from django.contrib.formtools.wizard.views import SessionWizardView + +from horizon import exceptions +from horizon import tabs +from horizon import tables +from horizon import workflows +from horizon.forms.views import ModalFormMixin + +from tabula.tabula import api + +from .tables import DCTable, ServicesTable +from .workflows import CreateDC +from .tabs import ServicesTabs +from .forms import (WizardFormADConfiguration, WizardFormIISConfiguration) + +from horizon import messages + +from django.http import HttpResponseRedirect + +LOG = logging.getLogger(__name__) + + +class Wizard(ModalFormMixin, SessionWizardView, generic.FormView): + template_name = 'services_tabs.html' + + def done(self, form_list, **kwargs): + link = self.request.__dict__['META']['HTTP_REFERER'] + environment_id = re.search('tabula/(\S+)', link).group(0)[6:-1] + + url = "/project/tabula/%s/" % environment_id + + service_type = form_list[0].data.get('0-service', '') + parameters = {'service_type': service_type} + data = form_list[1].data + if service_type == 'Active Directory': + parameters['configuration'] = 'standalone' + parameters['name'] = str(data.get('1-dc_name', 'noname')) + parameters['domain'] = parameters['name'] # Fix Me in orchestrator + parameters['adminPassword'] = str(data.get('1-adm_password', '')) + dc_count = int(data.get('1-dc_count', 1)) + recovery_password = str(data.get('1-recovery_password', '')) + parameters['units'] = [] + parameters['units'].append({'isMaster': True, + 'recoveryPassword': recovery_password, + 'location': 'west-dc'}) + for dc in range(dc_count - 1): + parameters['units'].append({ + 'isMaster': False, + 'recoveryPassword': recovery_password, + 'location': 'west-dc' + }) + + elif service_type == 'IIS': + password = data.get('1-adm_password', '') + parameters['name'] = str(data.get('1-iis_name', 'noname')) + parameters['credentials'] = {'username': 'Administrator', + 'password': password} + parameters['domain'] = str(data.get('1-iis_domain', '')) + password = form_list[1].data.get('1-adm_password', '') + domain = form_list[1].data.get('1-iis_domain', '') + dc_user = form_list[1].data.get('1-domain_user_name', '') + dc_pass = form_list[1].data.get('1-domain_user_password', '') + parameters['name'] = str(form_list[1].data.get('1-iis_name', + 'noname')) + parameters['domain'] = parameters['name'] + parameters['credentials'] = {'username': 'Administrator', + 'password': password} + parameters['domain'] = str(domain) + parameters['location'] = 'west-dc' + + parameters['units'] = [] + parameters['units'].append({'id': '1', + 'endpoint': [{'host': '10.0.0.1'}], + 'location': 'west-dc'}) + + service = api.services_create(self.request, environment_id, parameters) + + message = "The %s service successfully created." % service_type + messages.success(self.request, message) + return HttpResponseRedirect(url) + + def get_form(self, step=None, data=None, files=None): + + form = super(Wizard, self).get_form(step, data, files) + if data: + self.service_type = data.get('0-service', '') + if self.service_type == 'Active Directory': + self.form_list['1'] = WizardFormADConfiguration + elif self.service_type == 'IIS': + self.form_list['1'] = WizardFormIISConfiguration + + return form + + def get_form_kwargs(self, step=None): + return {'request': self.request} if step == u'1' else {} + + def get_form_step_data(self, form): + LOG.debug(form.data) + return form.data + + def get_context_data(self, form, **kwargs): + context = super(Wizard, self).get_context_data(form=form, **kwargs) + if self.steps.index > 0: + context.update({'service_type': self.service_type}) + return context + + +class IndexView(tables.DataTableView): + table_class = DCTable + template_name = 'index.html' + + def get_data(self): + try: + environments = api.environments_list(self.request) + except: + environments = [] + exceptions.handle(self.request, + _('Unable to retrieve environments list.')) + return environments + + +class Services(tables.DataTableView): + table_class = ServicesTable + template_name = 'services.html' + + def get_context_data(self, **kwargs): + context = super(Services, self).get_context_data(**kwargs) + context['environment_name'] = self.environment_name + return context + + def get_data(self): + try: + self.environment_id = self.kwargs['environment_id'] + environment = api.environment_get(self.request, self.environment_id) + self.environment_name = environment.name + services = api.services_list(self.request, environment_id) + except: + services = [] + exceptions.handle(self.request, + _('Unable to retrieve list of services for ' + 'environment "%s".') % self.environment_name) + self._services = services + return self._services + + +class DetailServiceView(tabs.TabView): + tab_group_class = ServicesTabs + template_name = 'service_details.html' + + def get_context_data(self, **kwargs): + context = super(DetailServiceView, self).get_context_data(**kwargs) + context["service"] = self.get_data() + context["service_name"] = self.get_data().name + return context + + def get_data(self): + if not hasattr(self, "_service"): + try: + service_id = self.kwargs['service_id'] + service = api.get_service_datails(self.request, service_id) + except: + redirect = reverse('horizon:project:tabula:index') + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'service "%s".') % service_id, + redirect=redirect) + self._service = service + return self._service + + def get_tabs(self, request, *args, **kwargs): + service = self.get_data() + return self.tab_group_class(request, service=service, **kwargs) + + +class CreateDCView(workflows.WorkflowView): + workflow_class = CreateDC + template_name = 'create_dc.html' + + def get_initial(self): + initial = super(CreateDCView, self).get_initial() + initial['project_id'] = self.request.user.tenant_id + initial['user_id'] = self.request.user.id + return initial diff --git a/dashboard/tabula/tabula/workflows.py b/dashboard/tabula/tabula/workflows.py new file mode 100644 index 00000000..edc3956a --- /dev/null +++ b/dashboard/tabula/tabula/workflows.py @@ -0,0 +1,96 @@ +# Copyright (c) 2013 Mirantis Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import re + +from django.utils.text import normalize_newlines +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +from tabula.tabula import api + + +LOG = logging.getLogger(__name__) + + +class SelectProjectUserAction(workflows.Action): + project_id = forms.ChoiceField(label=_("Project")) + user_id = forms.ChoiceField(label=_("User")) + + def __init__(self, request, *args, **kwargs): + super(SelectProjectUserAction, self).__init__(request, *args, **kwargs) + # Set our project choices + projects = [(tenant.id, tenant.name) + for tenant in request.user.authorized_tenants] + self.fields['project_id'].choices = projects + + # Set our user options + users = [(request.user.id, request.user.username)] + self.fields['user_id'].choices = users + + class Meta: + name = _("Project & User") + # Unusable permission so this is always hidden. However, we + # keep this step in the workflow for validation/verification purposes. + permissions = ("!",) + + +class SelectProjectUser(workflows.Step): + action_class = SelectProjectUserAction + + +class ConfigureDCAction(workflows.Action): + name = forms.CharField(label=_("Environment Name"), required=True) + + class Meta: + name = _("Environment") + help_text_template = "_data_center_help.html" + + +class ConfigureDC(workflows.Step): + action_class = ConfigureDCAction + contibutes = ('name',) + + def contribute(self, data, context): + if data: + context['name'] = data.get('name', '') + return context + + +class CreateDC(workflows.Workflow): + slug = "create" + name = _("Create Environment") + finalize_button_name = _("Create") + success_message = _('Created environment "%s".') + failure_message = _('Unable to create environment "%s".') + success_url = "horizon:project:tabula:index" + default_steps = (SelectProjectUser, ConfigureDC) + + def format_status_message(self, message): + name = self.context.get('name', 'noname') + return message % name + + def handle(self, request, context): + try: + api.environment_create(request, context) + return True + except: + exceptions.handle(request) + return False diff --git a/dashboard/tabula/templates/_data_center_help.html b/dashboard/tabula/templates/_data_center_help.html new file mode 100644 index 00000000..68ffe5a4 --- /dev/null +++ b/dashboard/tabula/templates/_data_center_help.html @@ -0,0 +1,2 @@ +{% load i18n %} +

    {% blocktrans %}Data Center is an instance with different services.{% endblocktrans %}

    \ No newline at end of file diff --git a/dashboard/tabula/templates/_dc_help.html b/dashboard/tabula/templates/_dc_help.html new file mode 100644 index 00000000..5e29e2d2 --- /dev/null +++ b/dashboard/tabula/templates/_dc_help.html @@ -0,0 +1,3 @@ +{% load i18n %} +

    {% blocktrans %}You can deploy few Active Directory services with one domain name.{% endblocktrans %}

    +

    {% blocktrans %}The DNS service will automatically created for each Active Directory.{% endblocktrans %}

    \ No newline at end of file diff --git a/dashboard/tabula/templates/_iis_help.html b/dashboard/tabula/templates/_iis_help.html new file mode 100644 index 00000000..e8004c88 --- /dev/null +++ b/dashboard/tabula/templates/_iis_help.html @@ -0,0 +1,2 @@ +{% load i18n %} +

    {% blocktrans %}You can deploy few Internet Information Services in one domain.{% endblocktrans %}

    \ No newline at end of file diff --git a/dashboard/tabula/templates/_service_logs.html b/dashboard/tabula/templates/_service_logs.html new file mode 100644 index 00000000..dda752a0 --- /dev/null +++ b/dashboard/tabula/templates/_service_logs.html @@ -0,0 +1,7 @@ +{% load i18n %} +
    +

    {% trans "Service Logs" %}

    +
    +
    +  {{ reports }}
    +
    \ No newline at end of file diff --git a/dashboard/tabula/templates/_services.html b/dashboard/tabula/templates/_services.html new file mode 100644 index 00000000..ee5006f6 --- /dev/null +++ b/dashboard/tabula/templates/_services.html @@ -0,0 +1,16 @@ +{% load i18n %} + +
    +

    {% trans "Service Details" %}

    +
    +
    +
    {% trans "Name" %}
    +
    {{ service_name }}
    +
    {% trans "Type" %}
    +
    {{ service_type }}
    +
    {% trans "Domain" %}
    +
    {{ service_domain }}
    +
    {% trans "Status" %}
    +
    {{ service_status }}
    +
    +
    \ No newline at end of file diff --git a/dashboard/tabula/templates/_services_tabs.html b/dashboard/tabula/templates/_services_tabs.html new file mode 100644 index 00000000..49b3d244 --- /dev/null +++ b/dashboard/tabula/templates/_services_tabs.html @@ -0,0 +1,57 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_action %}{% url horizon:project:windc:create %}?{{ request.POST.urlencode }}{% endblock %} + +{% block modal_id %}create_service{% endblock %} +{% block modal-header %}{% trans "Create Service" %}{% endblock %} + +{% block modal-body %} +
    +
    +{% if wizard.steps.next %} +


    +{% endif %} + +{{ wizard.management_form }} +{% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} +{% else %} + {{ wizard.form }} +{% endif %} +{{ wizard.form.forms }} +
    +
    +
    + {% if wizard.steps.prev %} +

    {{ service_type }} Service

    + {% if service_type == 'Active Directory' %} +

    {% trans "Now you can set the parameters for Active Directory Service." %}

    +

    {% trans "You can create few Active Directory instances, in this case will be created one Main Active Directory server and few Secondary Active Directory servers." %}

    +

    {% trans "The DNS service will be automatically created on each Active Directory servers." %}

    + {% else %} +

    {% trans "Now you can set parameters for IIS Service." %}

    +

    {% trans "The IIS Service - it is the server with complex Internet Information Services infrastructure, which included to the domain infrasructure." %}

    +

    {% trans "Please, set the complex password for local administrator account." %}

    +

    {% trans "Also, you can add this IIS server to the existing domain and configure credentials for domain user." %}

    + {% endif %} + + {% else %} +

    {% trans "Description" %}:

    +

    {% trans "Now you can select the type of the service." %}

    +

    {% trans "The Active Directory Service allows to configure Domain Controllers with Active Directory and DNS infrastructure. You can create one Main Domain Controller and few Secondary Domain Controllers." %}

    +

    {% trans "The Internet Information Services allows to configure IIS servers, which can be included to the existing domain infrastructure." %}

    + {% endif %} +
    +{% endblock %} + +{% block modal-footer %} +{% if wizard.steps.prev %} + +{% else %} + +{% endif %} +{% endblock %} diff --git a/dashboard/tabula/templates/create.html b/dashboard/tabula/templates/create.html new file mode 100644 index 00000000..0508b68e --- /dev/null +++ b/dashboard/tabula/templates/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Windows Service" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Windows Service") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/dashboard/tabula/templates/create_dc.html b/dashboard/tabula/templates/create_dc.html new file mode 100644 index 00000000..2fc5894e --- /dev/null +++ b/dashboard/tabula/templates/create_dc.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Windows Data Center" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Windows Data Center") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/dashboard/tabula/templates/index.html b/dashboard/tabula/templates/index.html new file mode 100644 index 00000000..fa172abe --- /dev/null +++ b/dashboard/tabula/templates/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Windows Data Centers" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Windows Data Centers") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/dashboard/tabula/templates/service_details.html b/dashboard/tabula/templates/service_details.html new file mode 100644 index 00000000..bde27525 --- /dev/null +++ b/dashboard/tabula/templates/service_details.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n sizeformat %} +{% block title %}{% trans "Service Detail" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Service Detail: "|add:service.name %} +{% endblock page_header %} + +{% block main %} +
    +
    + {{ tab_group.render }} +
    +
    +{% endblock %} diff --git a/dashboard/tabula/templates/services.html b/dashboard/tabula/templates/services.html new file mode 100644 index 00000000..2c7af81f --- /dev/null +++ b/dashboard/tabula/templates/services.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Data Center Services" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Data Center "|add:dc_name %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/dashboard/tabula/templates/update.html b/dashboard/tabula/templates/update.html new file mode 100644 index 00000000..aba3dc9a --- /dev/null +++ b/dashboard/tabula/templates/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Instance" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Instance") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/instances/_update.html' %} +{% endblock %} diff --git a/dashboard/tabula/test/__init__.py b/dashboard/tabula/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/tabula/test/settings.py b/dashboard/tabula/test/settings.py new file mode 100644 index 00000000..501bc7f1 --- /dev/null +++ b/dashboard/tabula/test/settings.py @@ -0,0 +1,70 @@ +import socket + +from dashboard.settings import * + +socket.setdefaulttimeout(1) + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +SECRET_KEY = 'HELLA_SECRET!' + +DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}} + +TESTSERVER = 'http://testserver' + +INSTALLED_APPS += ('django_nose',) + +MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +NOSE_ARGS = ['--nocapture', + '--nologcapture', + '--cover-package=windc'] + +EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + +OPENSTACK_ADDRESS = "localhost" +OPENSTACK_ADMIN_TOKEN = "openstack" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_ADDRESS +OPENSTACK_KEYSTONE_ADMIN_URL = "http://%s:35357/v2.0" % OPENSTACK_ADDRESS +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" + +# Silence logging output during tests. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + }, + 'loggers': { + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + 'handlers': ['null'], + 'propagate': False, + }, + 'novaclient': { + 'handlers': ['null'], + 'propagate': False, + }, + 'keystoneclient': { + 'handlers': ['null'], + 'propagate': False, + }, + 'quantum': { + 'handlers': ['null'], + 'propagate': False, + }, + 'nose.plugins.manager': { + 'handlers': ['null'], + 'propagate': False, + } + } +} diff --git a/dashboard/tabula/version.py b/dashboard/tabula/version.py new file mode 100644 index 00000000..ac241ae7 --- /dev/null +++ b/dashboard/tabula/version.py @@ -0,0 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from tabula.openstack.common import version as common_version + +version_info = common_version.VersionInfo('tabula') diff --git a/dashboard/tools/install_venv.py b/dashboard/tools/install_venv.py new file mode 100644 index 00000000..2aa61068 --- /dev/null +++ b/dashboard/tools/install_venv.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack LLC. +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import subprocess +import sys + +import install_venv_common as install_venv + + +def print_help(): + help = """ + Tabula development environment setup is complete. + + Tabula development uses virtualenv to track and manage Python dependencies + while in development and testing. + + To activate the Tabula virtualenv for the extent of your current shell session + you can run: + + $ source .venv/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print help + + +def main(argv): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + venv = os.path.join(root, '.venv') + pip_requires = os.path.join(root, 'tools', 'pip-requires') + test_requires = os.path.join(root, 'tools', 'test-requires') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + project = 'tabula' + install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, + py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + install.post_process() + print_help() + +if __name__ == '__main__': + main(sys.argv) diff --git a/dashboard/tools/install_venv_common.py b/dashboard/tools/install_venv_common.py new file mode 100644 index 00000000..1ea3b2df --- /dev/null +++ b/dashboard/tools/install_venv_common.py @@ -0,0 +1,219 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack, LLC +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Synced in from openstack-common +""" + +import argparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, pip_requires, test_requires, py_version, + project): + self.root = root + self.venv = venv + self.pip_requires = pip_requires + self.test_requires = test_requires + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print >> sys.stderr, message % args + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora(self.root, self.venv, self.pip_requires, + self.test_requires, self.py_version, self.project) + else: + return Distro(self.root, self.venv, self.pip_requires, + self.test_requires, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print 'Creating venv...', + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print 'done.' + print 'Installing pip in venv...', + if not self.run_command(['tools/with_venv.sh', 'easy_install', + 'pip>1.0']).strip(): + self.die("Failed to install pip.") + print 'done.' + else: + print "venv already exists..." + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print 'Installing dependencies with pip (this can take a while)...' + + # First things first, make sure our venv has the latest pip and + # distribute. + # NOTE: we keep pip at version 1.1 since the most recent version causes + # the .venv creation to fail. See: + # https://bugs.launchpad.net/nova/+bug/1047120 + self.pip_install('pip==1.1') + self.pip_install('distribute') + + # Install greenlet by hand - just listing it in the requires file does + # not + # get it installed in the right order + self.pip_install('greenlet') + + self.pip_install('-r', self.test_requires) + self.pip_install('-r', self.pip_requires) + + def post_process(self): + self.get_distro().post_process() + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install") + return parser.parse_args(argv[1:]) + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print 'Installing virtualenv via easy_install...', + if self.run_command(['easy_install', 'virtualenv']): + print 'Succeeded' + return + else: + print 'Failed' + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + def post_process(self): + """Any distribution-specific post-processing gets done here. + + In particular, this is useful for applying patches to code inside + the venv. + """ + pass + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def yum_install(self, pkg, **kwargs): + print "Attempting to install '%s' via yum" % pkg + self.run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs) + + def apply_patch(self, originalfile, patchfile): + self.run_command(['patch', originalfile, patchfile]) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.yum_install('python-virtualenv', check_exit_code=False) + + super(Fedora, self).install_virtualenv() + + def post_process(self): + """Workaround for a bug in eventlet. + + This currently affects RHEL6.1, but the fix can safely be + applied to all RHEL and Fedora distributions. + + This can be removed when the fix is applied upstream. + + Nova: https://bugs.launchpad.net/nova/+bug/884915 + Upstream: https://bitbucket.org/which_linden/eventlet/issue/89 + """ + + # Install "patch" program if it's not there + if not self.check_pkg('patch'): + self.yum_install('patch') + + # Apply the eventlet patch + self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, + 'site-packages', + 'eventlet/green/subprocess.py'), + 'contrib/redhat-eventlet.patch') diff --git a/dashboard/tools/pip-requires b/dashboard/tools/pip-requires new file mode 100644 index 00000000..b8f9550a --- /dev/null +++ b/dashboard/tools/pip-requires @@ -0,0 +1,14 @@ +# Horizon +-e git+https://github.com/openstack/horizon.git#egg=horizon + +# Core Requirements +Django>=1.4,<1.5 + +#API +./packages/python-portasclient-2013.1.a345.ga70b44e.tar.gz + +anyjson + +#Fix for bug https://bugs.launchpad.net/python-keystoneclient/+bug/1116740 +backports.ssl_match_hostname +requests==0.14.2 diff --git a/dashboard/tools/rfc.sh b/dashboard/tools/rfc.sh new file mode 100755 index 00000000..15781e52 --- /dev/null +++ b/dashboard/tools/rfc.sh @@ -0,0 +1,145 @@ +#!/bin/sh -e +# Copyright (c) 2010-2011 Gluster, Inc. +# This initial version of this file was taken from the source tree +# of GlusterFS. It was not directly attributed, but is assumed to be +# Copyright (c) 2010-2011 Gluster, Inc and release GPLv3 +# Subsequent modifications are Copyright (c) 2012 OpenStack, LLC. +# +# GlusterFS is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; either version 3 of the License, +# or (at your option) any later version. +# +# GlusterFS is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# . + + +branch="master"; + +set_hooks_commit_msg() +{ + top_dir=`git rev-parse --show-toplevel` + f="${top_dir}/.git/hooks/commit-msg"; + u="https://review.openstack.org/tools/hooks/commit-msg"; + + if [ -x "$f" ]; then + return; + fi + + curl -o $f $u || wget -O $f $u; + + chmod +x $f; + + GIT_EDITOR=true git commit --amend +} + +add_remote() +{ + username=$1 + project=$2 + + echo "No remote set, testing ssh://$username@review.openstack.org:29418" + if project_list=`ssh -p29418 -o StrictHostKeyChecking=no $username@review.openstack.org gerrit ls-projects 2>/dev/null` + then + echo "$username@review.openstack.org:29418 worked." + if echo $project_list | grep $project >/dev/null + then + echo "Creating a git remote called gerrit that maps to:" + echo " ssh://$username@review.openstack.org:29418/$project" + git remote add gerrit ssh://$username@review.openstack.org:29418/$project + else + echo "The current project name, $project, is not a known project." + echo "Please either reclone from github/gerrit or create a" + echo "remote named gerrit that points to the intended project." + return 1 + fi + + return 0 + fi + return 1 +} + +check_remote() +{ + if ! git remote | grep gerrit >/dev/null 2>&1 + then + origin_project=`git remote show origin | grep 'Fetch URL' | perl -nle '@fields = split(m|[:/]|); $len = $#fields; print $fields[$len-1], "/", $fields[$len];'` + if add_remote $USERNAME $origin_project + then + return 0 + else + echo "Your local name doesn't work on Gerrit." + echo -n "Enter Gerrit username (same as launchpad): " + read gerrit_user + if add_remote $gerrit_user $origin_project + then + return 0 + else + echo "Can't infer where gerrit is - please set a remote named" + echo "gerrit manually and then try again." + echo + echo "For more information, please see:" + echo "\thttp://wiki.openstack.org/GerritWorkflow" + exit 1 + fi + fi + fi +} + +rebase_changes() +{ + git fetch; + + GIT_EDITOR=true git rebase -i origin/$branch || exit $?; +} + + +assert_diverge() +{ + if ! git diff origin/$branch..HEAD | grep -q . + then + echo "No changes between the current branch and origin/$branch." + exit 1 + fi +} + + +main() +{ + set_hooks_commit_msg; + + check_remote; + + rebase_changes; + + assert_diverge; + + bug=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)/) {print "$2"; exit}') + + bp=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)/) {print "$2"; exit}') + + if [ "$DRY_RUN" = 1 ]; then + drier='echo -e Please use the following command to send your commits to review:\n\n' + else + drier= + fi + + local_branch=`git branch | grep -Ei "\* (.*)" | cut -f2 -d' '` + if [ -z "$bug" ]; then + if [ -z "$bp" ]; then + $drier git push gerrit HEAD:refs/for/$branch/$local_branch; + else + $drier git push gerrit HEAD:refs/for/$branch/bp/$bp; + fi + else + $drier git push gerrit HEAD:refs/for/$branch/bug/$bug; + fi +} + +main "$@" diff --git a/dashboard/tools/test-requires b/dashboard/tools/test-requires new file mode 100644 index 00000000..c1da7cae --- /dev/null +++ b/dashboard/tools/test-requires @@ -0,0 +1,18 @@ +distribute>=0.6.24 + +# Testing Requirements +coverage +django-nose +mox +nose +nose-exclude +nosexcover +openstack.nose_plugin +nosehtmloutput +pep8>=1.3 +pylint +selenium + +# Docs Requirements +sphinx +docutils==0.9.1 # for bug 1091333, remove after sphinx >1.1.3 is released. diff --git a/dashboard/tools/with_venv.sh b/dashboard/tools/with_venv.sh new file mode 100755 index 00000000..c8d2940f --- /dev/null +++ b/dashboard/tools/with_venv.sh @@ -0,0 +1,4 @@ +#!/bin/bash +TOOLS=`dirname $0` +VENV=$TOOLS/../.venv +source $VENV/bin/activate && $@