=== added directory 'lava_markitup'
=== added file 'lava_markitup/__init__.py'
=== added file 'lava_markitup/models.py'
=== added directory 'lava_markitup/static'
=== added directory 'lava_markitup/static/lava_markitup'
=== added file 'lava_markitup/static/lava_markitup/jquery.markitup.js'
@@ -0,0 +1,593 @@
+// ----------------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// v 1.1.x
+// Dual licensed under the MIT and GPL licenses.
+// ----------------------------------------------------------------------------
+// Copyright (C) 2007-2011 Jay Salvat
+// http://markitup.jaysalvat.com/
+// ----------------------------------------------------------------------------
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+// ----------------------------------------------------------------------------
+(function($) {
+ $.fn.markItUp = function(settings, extraSettings) {
+ var options, ctrlKey, shiftKey, altKey;
+ ctrlKey = shiftKey = altKey = false;
+
+ options = { id: '',
+ nameSpace: '',
+ root: '',
+ previewInWindow: '', // 'width=800, height=600, resizable=yes, scrollbars=yes'
+ previewAutoRefresh: true,
+ previewPosition: 'after',
+ previewTemplatePath: '~/templates/preview.html',
+ previewParser: false,
+ previewParserPath: '',
+ previewParserVar: 'data',
+ resizeHandle: true,
+ beforeInsert: '',
+ afterInsert: '',
+ onEnter: {},
+ onShiftEnter: {},
+ onCtrlEnter: {},
+ onTab: {},
+ markupSet: [ { /* set */ } ]
+ };
+ $.extend(options, settings, extraSettings);
+
+ // compute markItUp! path
+ if (!options.root) {
+ $('script').each(function(a, tag) {
+ miuScript = $(tag).get(0).src.match(/(.*)jquery\.markitup(\.pack)?\.js$/);
+ if (miuScript !== null) {
+ options.root = miuScript[1];
+ }
+ });
+ }
+
+ return this.each(function() {
+ var $$, textarea, levels, scrollPosition, caretPosition, caretOffset,
+ clicked, hash, header, footer, previewWindow, template, iFrame, abort;
+ $$ = $(this);
+ textarea = this;
+ levels = [];
+ abort = false;
+ scrollPosition = caretPosition = 0;
+ caretOffset = -1;
+
+ options.previewParserPath = localize(options.previewParserPath);
+ options.previewTemplatePath = localize(options.previewTemplatePath);
+
+ // apply the computed path to ~/
+ function localize(data, inText) {
+ if (inText) {
+ return data.replace(/("|')~\//g, "$1"+options.root);
+ }
+ return data.replace(/^~\//, options.root);
+ }
+
+ // init and build editor
+ function init() {
+ id = ''; nameSpace = '';
+ if (options.id) {
+ id = 'id="'+options.id+'"';
+ } else if ($$.attr("id")) {
+ id = 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"';
+
+ }
+ if (options.nameSpace) {
+ nameSpace = 'class="'+options.nameSpace+'"';
+ }
+ $$.wrap('<div '+nameSpace+'></div>');
+ $$.wrap('<div '+id+' class="markItUp"></div>');
+ $$.wrap('<div class="markItUpContainer"></div>');
+ $$.addClass("markItUpEditor");
+
+ // add the header before the textarea
+ header = $('<div class="markItUpHeader"></div>').insertBefore($$);
+ $(dropMenus(options.markupSet)).appendTo(header);
+
+ // add the footer after the textarea
+ footer = $('<div class="markItUpFooter"></div>').insertAfter($$);
+
+ // add the resize handle after textarea
+ if (options.resizeHandle === true && $.browser.safari !== true) {
+ resizeHandle = $('<div class="markItUpResizeHandle"></div>')
+ .insertAfter($$)
+ .bind("mousedown", function(e) {
+ var h = $$.height(), y = e.clientY, mouseMove, mouseUp;
+ mouseMove = function(e) {
+ $$.css("height", Math.max(20, e.clientY+h-y)+"px");
+ return false;
+ };
+ mouseUp = function(e) {
+ $("html").unbind("mousemove", mouseMove).unbind("mouseup", mouseUp);
+ return false;
+ };
+ $("html").bind("mousemove", mouseMove).bind("mouseup", mouseUp);
+ });
+ footer.append(resizeHandle);
+ }
+
+ // listen key events
+ $$.keydown(keyPressed).keyup(keyPressed);
+
+ // bind an event to catch external calls
+ $$.bind("insertion", function(e, settings) {
+ if (settings.target !== false) {
+ get();
+ }
+ if (textarea === $.markItUp.focused) {
+ markup(settings);
+ }
+ });
+
+ // remember the last focus
+ $$.focus(function() {
+ $.markItUp.focused = this;
+ });
+ }
+
+ // recursively build header with dropMenus from markupset
+ function dropMenus(markupSet) {
+ var ul = $('<ul></ul>'), i = 0;
+ $('li:hover > ul', ul).css('display', 'block');
+ $.each(markupSet, function() {
+ var button = this, t = '', title, li, j;
+ title = (button.key) ? (button.name||'')+' [Ctrl+'+button.key+']' : (button.name||'');
+ key = (button.key) ? 'accesskey="'+button.key+'"' : '';
+ if (button.separator) {
+ li = $('<li class="markItUpSeparator">'+(button.separator||'')+'</li>').appendTo(ul);
+ } else {
+ i++;
+ for (j = levels.length -1; j >= 0; j--) {
+ t += levels[j]+"-";
+ }
+ li = $('<li class="markItUpButton markItUpButton'+t+(i)+' '+(button.className||'')+'"><a href="" '+key+' title="'+title+'">'+(button.name||'')+'</a></li>')
+ .bind("contextmenu", function() { // prevent contextmenu on mac and allow ctrl+click
+ return false;
+ }).click(function() {
+ return false;
+ }).bind("focusin", function(){
+ $$.focus();
+ }).mouseup(function() {
+ if (button.call) {
+ eval(button.call)();
+ }
+ setTimeout(function() { markup(button) },1);
+ return false;
+ }).hover(function() {
+ $('> ul', this).show();
+ $(document).one('click', function() { // close dropmenu if click outside
+ $('ul ul', header).hide();
+ }
+ );
+ }, function() {
+ $('> ul', this).hide();
+ }
+ ).appendTo(ul);
+ if (button.dropMenu) {
+ levels.push(i);
+ $(li).addClass('markItUpDropMenu').append(dropMenus(button.dropMenu));
+ }
+ }
+ });
+ levels.pop();
+ return ul;
+ }
+
+ // markItUp! markups
+ function magicMarkups(string) {
+ if (string) {
+ string = string.toString();
+ string = string.replace(/\(\!\(([\s\S]*?)\)\!\)/g,
+ function(x, a) {
+ var b = a.split('|!|');
+ if (altKey === true) {
+ return (b[1] !== undefined) ? b[1] : b[0];
+ } else {
+ return (b[1] === undefined) ? "" : b[0];
+ }
+ }
+ );
+ // [![prompt]!], [![prompt:!:value]!]
+ string = string.replace(/\[\!\[([\s\S]*?)\]\!\]/g,
+ function(x, a) {
+ var b = a.split(':!:');
+ if (abort === true) {
+ return false;
+ }
+ value = prompt(b[0], (b[1]) ? b[1] : '');
+ if (value === null) {
+ abort = true;
+ }
+ return value;
+ }
+ );
+ return string;
+ }
+ return "";
+ }
+
+ // prepare action
+ function prepare(action) {
+ if ($.isFunction(action)) {
+ action = action(hash);
+ }
+ return magicMarkups(action);
+ }
+
+ // build block to insert
+ function build(string) {
+ var openWith = prepare(clicked.openWith);
+ var placeHolder = prepare(clicked.placeHolder);
+ var replaceWith = prepare(clicked.replaceWith);
+ var closeWith = prepare(clicked.closeWith);
+ var openBlockWith = prepare(clicked.openBlockWith);
+ var closeBlockWith = prepare(clicked.closeBlockWith);
+ var multiline = clicked.multiline;
+
+ if (replaceWith !== "") {
+ block = openWith + replaceWith + closeWith;
+ } else if (selection === '' && placeHolder !== '') {
+ block = openWith + placeHolder + closeWith;
+ } else {
+ string = string || selection;
+
+ var lines = selection.split(/\r?\n/), blocks = [];
+
+ for (var l=0; l < lines.length; l++) {
+ line = lines[l];
+ var trailingSpaces;
+ if (trailingSpaces = line.match(/ *$/)) {
+ blocks.push(openWith + line.replace(/ *$/g, '') + closeWith + trailingSpaces);
+ } else {
+ blocks.push(openWith + line + closeWith);
+ }
+ }
+
+ block = blocks.join("\n");
+ }
+
+ block = openBlockWith + block + closeBlockWith;
+
+ return { block:block,
+ openWith:openWith,
+ replaceWith:replaceWith,
+ placeHolder:placeHolder,
+ closeWith:closeWith
+ };
+ }
+
+ // define markup to insert
+ function markup(button) {
+ var len, j, n, i;
+ hash = clicked = button;
+ get();
+ $.extend(hash, { line:"",
+ root:options.root,
+ textarea:textarea,
+ selection:(selection||''),
+ caretPosition:caretPosition,
+ ctrlKey:ctrlKey,
+ shiftKey:shiftKey,
+ altKey:altKey
+ }
+ );
+ // callbacks before insertion
+ prepare(options.beforeInsert);
+ prepare(clicked.beforeInsert);
+ if ((ctrlKey === true && shiftKey === true) || button.multiline === true) {
+ prepare(clicked.beforeMultiInsert);
+ }
+ $.extend(hash, { line:1 });
+
+ if ((ctrlKey === true && shiftKey === true)) {
+ lines = selection.split(/\r?\n/);
+ for (j = 0, n = lines.length, i = 0; i < n; i++) {
+ if ($.trim(lines[i]) !== '') {
+ $.extend(hash, { line:++j, selection:lines[i] } );
+ lines[i] = build(lines[i]).block;
+ } else {
+ lines[i] = "";
+ }
+ }
+ string = { block:lines.join('\n')};
+ start = caretPosition;
+ len = string.block.length + (($.browser.opera) ? n-1 : 0);
+ } else if (ctrlKey === true) {
+ string = build(selection);
+ start = caretPosition + string.openWith.length;
+ len = string.block.length - string.openWith.length - string.closeWith.length;
+ len = len - (string.block.match(/ $/) ? 1 : 0);
+ len -= fixIeBug(string.block);
+ } else if (shiftKey === true) {
+ string = build(selection);
+ start = caretPosition;
+ len = string.block.length;
+ len -= fixIeBug(string.block);
+ } else {
+ string = build(selection);
+ start = caretPosition + string.block.length ;
+ len = 0;
+ start -= fixIeBug(string.block);
+ }
+ if ((selection === '' && string.replaceWith === '')) {
+ caretOffset += fixOperaBug(string.block);
+
+ start = caretPosition + string.openWith.length;
+ len = string.block.length - string.openWith.length - string.closeWith.length;
+
+ caretOffset = $$.val().substring(caretPosition, $$.val().length).length;
+ caretOffset -= fixOperaBug($$.val().substring(0, caretPosition));
+ }
+ $.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } );
+
+ if (string.block !== selection && abort === false) {
+ insert(string.block);
+ set(start, len);
+ } else {
+ caretOffset = -1;
+ }
+ get();
+
+ $.extend(hash, { line:'', selection:selection });
+
+ // callbacks after insertion
+ if ((ctrlKey === true && shiftKey === true) || button.multiline === true) {
+ prepare(clicked.afterMultiInsert);
+ }
+ prepare(clicked.afterInsert);
+ prepare(options.afterInsert);
+
+ // refresh preview if opened
+ if (previewWindow && options.previewAutoRefresh) {
+ refreshPreview();
+ }
+
+ // reinit keyevent
+ shiftKey = altKey = ctrlKey = abort = false;
+ }
+
+ // Substract linefeed in Opera
+ function fixOperaBug(string) {
+ if ($.browser.opera) {
+ return string.length - string.replace(/\n*/g, '').length;
+ }
+ return 0;
+ }
+ // Substract linefeed in IE
+ function fixIeBug(string) {
+ if ($.browser.msie) {
+ return string.length - string.replace(/\r*/g, '').length;
+ }
+ return 0;
+ }
+
+ // add markup
+ function insert(block) {
+ if (document.selection) {
+ var newSelection = document.selection.createRange();
+ newSelection.text = block;
+ } else {
+ textarea.value = textarea.value.substring(0, caretPosition) + block + textarea.value.substring(caretPosition + selection.length, textarea.value.length);
+ }
+ }
+
+ // set a selection
+ function set(start, len) {
+ if (textarea.createTextRange){
+ // quick fix to make it work on Opera 9.5
+ if ($.browser.opera && $.browser.version >= 9.5 && len == 0) {
+ return false;
+ }
+ range = textarea.createTextRange();
+ range.collapse(true);
+ range.moveStart('character', start);
+ range.moveEnd('character', len);
+ range.select();
+ } else if (textarea.setSelectionRange ){
+ textarea.setSelectionRange(start, start + len);
+ }
+ textarea.scrollTop = scrollPosition;
+ textarea.focus();
+ }
+
+ // get the selection
+ function get() {
+ textarea.focus();
+
+ scrollPosition = textarea.scrollTop;
+ if (document.selection) {
+ selection = document.selection.createRange().text;
+ if ($.browser.msie) { // ie
+ var range = document.selection.createRange(), rangeCopy = range.duplicate();
+ rangeCopy.moveToElementText(textarea);
+ caretPosition = -1;
+ while(rangeCopy.inRange(range)) {
+ rangeCopy.moveStart('character');
+ caretPosition ++;
+ }
+ } else { // opera
+ caretPosition = textarea.selectionStart;
+ }
+ } else { // gecko & webkit
+ caretPosition = textarea.selectionStart;
+
+ selection = textarea.value.substring(caretPosition, textarea.selectionEnd);
+ }
+ return selection;
+ }
+
+ // open preview window
+ function preview() {
+ if (!previewWindow || previewWindow.closed) {
+ if (options.previewInWindow) {
+ previewWindow = window.open('', 'preview', options.previewInWindow);
+ $(window).unload(function() {
+ previewWindow.close();
+ });
+ } else {
+ iFrame = $('<iframe class="markItUpPreviewFrame"></iframe>');
+ if (options.previewPosition == 'after') {
+ iFrame.insertAfter(footer);
+ } else {
+ iFrame.insertBefore(header);
+ }
+ previewWindow = iFrame[iFrame.length - 1].contentWindow || frame[iFrame.length - 1];
+ }
+ } else if (altKey === true) {
+ if (iFrame) {
+ iFrame.remove();
+ } else {
+ previewWindow.close();
+ }
+ previewWindow = iFrame = false;
+ }
+ if (!options.previewAutoRefresh) {
+ refreshPreview();
+ }
+ if (options.previewInWindow) {
+ previewWindow.focus();
+ }
+ }
+
+ // refresh Preview window
+ function refreshPreview() {
+ renderPreview();
+ }
+
+ function renderPreview() {
+ var phtml;
+ if (options.previewParser && typeof options.previewParser === 'function') {
+ var data = options.previewParser( $$.val() );
+ writeInPreview( localize(data, 1) );
+ } else if (options.previewParserPath !== '') {
+ $.ajax({
+ type: 'POST',
+ dataType: 'text',
+ global: false,
+ url: options.previewParserPath,
+ data: options.previewParserVar+'='+encodeURIComponent($$.val()),
+ success: function(data) {
+ writeInPreview( localize(data, 1) );
+ }
+ });
+ } else {
+ if (!template) {
+ $.ajax({
+ url: options.previewTemplatePath,
+ dataType: 'text',
+ global: false,
+ success: function(data) {
+ writeInPreview( localize(data, 1).replace(/<!-- content -->/g, $$.val()) );
+ }
+ });
+ }
+ }
+ return false;
+ }
+
+ function writeInPreview(data) {
+ if (previewWindow.document) {
+ try {
+ sp = previewWindow.document.documentElement.scrollTop
+ } catch(e) {
+ sp = 0;
+ }
+ previewWindow.document.open();
+ previewWindow.document.write(data);
+ previewWindow.document.close();
+ previewWindow.document.documentElement.scrollTop = sp;
+ }
+ }
+
+ // set keys pressed
+ function keyPressed(e) {
+ shiftKey = e.shiftKey;
+ altKey = e.altKey;
+ ctrlKey = (!(e.altKey && e.ctrlKey)) ? (e.ctrlKey || e.metaKey) : false;
+
+ if (e.type === 'keydown') {
+ if (ctrlKey === true) {
+ li = $('a[accesskey="'+String.fromCharCode(e.keyCode)+'"]', header).parent('li');
+ if (li.length !== 0) {
+ ctrlKey = false;
+ setTimeout(function() {
+ li.triggerHandler('mouseup');
+ },1);
+ return false;
+ }
+ }
+ if (e.keyCode === 13 || e.keyCode === 10) { // Enter key
+ if (ctrlKey === true) { // Enter + Ctrl
+ ctrlKey = false;
+ markup(options.onCtrlEnter);
+ return options.onCtrlEnter.keepDefault;
+ } else if (shiftKey === true) { // Enter + Shift
+ shiftKey = false;
+ markup(options.onShiftEnter);
+ return options.onShiftEnter.keepDefault;
+ } else { // only Enter
+ markup(options.onEnter);
+ return options.onEnter.keepDefault;
+ }
+ }
+ if (e.keyCode === 9) { // Tab key
+ if (shiftKey == true || ctrlKey == true || altKey == true) {
+ return false;
+ }
+ if (caretOffset !== -1) {
+ get();
+ caretOffset = $$.val().length - caretOffset;
+ set(caretOffset, 0);
+ caretOffset = -1;
+ return false;
+ } else {
+ markup(options.onTab);
+ return options.onTab.keepDefault;
+ }
+ }
+ }
+ }
+
+ init();
+ });
+ };
+
+ $.fn.markItUpRemove = function() {
+ return this.each(function() {
+ var $$ = $(this).unbind().removeClass('markItUpEditor');
+ $$.parent('div').parent('div.markItUp').parent('div').replaceWith($$);
+ }
+ );
+ };
+
+ $.markItUp = function(settings) {
+ var options = { target:false };
+ $.extend(options, settings);
+ if (options.target) {
+ return $(options.target).each(function() {
+ $(this).focus();
+ $(this).trigger('insertion', [options]);
+ });
+ } else {
+ $('textarea').trigger('insertion', [options]);
+ }
+ };
+})(jQuery);
=== added file 'lava_markitup/static/lava_markitup/preview.css'
@@ -0,0 +1,50 @@
+body, input, th, td {
+ font-family: Ubuntu, Helvetica Neue, Verdana, sans-serif;
+ font-size: 10pt;
+}
+
+code {
+ font-family: UbuntuBeta Mono, Lucida Console, Consolas, monospace;
+ font-size: 10pt;
+}
+
+body {
+ color: #444;
+ padding: 1ex;
+ margin: 0;
+ background-color: white;
+}
+
+a {
+ color: #08C;
+}
+
+
+h1 {
+ color: #222;
+ border-bottom: 3pt solid #444;
+}
+
+h2 {
+ color: #222;
+ border-bottom: 2pt solid #444;
+}
+
+h3 {
+ color: #111;
+}
+
+blockquote {
+ border-left: 2pt solid #555;
+ padding-left: 1ex;
+ margin-left: 2ex;
+}
+
+
+code {
+ display: block;
+ font-family: UbuntuBeta Mono, monospace;
+ background-color: #888;
+ color: white;
+ padding: 0.5ex;
+}
=== added directory 'lava_markitup/static/lava_markitup/sets'
=== added directory 'lava_markitup/static/lava_markitup/sets/markdown'
=== added directory 'lava_markitup/static/lava_markitup/sets/markdown/images'
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/bold.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/bold.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/bold.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/code.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/code.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/code.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h1.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h1.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h1.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h2.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h2.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h2.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h3.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h3.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h3.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h4.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h4.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h4.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h5.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h5.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h5.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/h6.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/h6.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/h6.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/italic.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/italic.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/italic.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/link.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/link.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/link.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/list-bullet.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/list-bullet.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/list-bullet.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/list-numeric.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/list-numeric.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/list-numeric.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/picture.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/picture.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/picture.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/preview.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/preview.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/preview.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/images/quotes.png'
Binary files lava_markitup/static/lava_markitup/sets/markdown/images/quotes.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/sets/markdown/images/quotes.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/set.js'
@@ -0,0 +1,52 @@
+// -------------------------------------------------------------------
+// markItUp!
+// -------------------------------------------------------------------
+// Copyright (C) 2008 Jay Salvat
+// http://markitup.jaysalvat.com/
+// -------------------------------------------------------------------
+// MarkDown tags example
+// http://en.wikipedia.org/wiki/Markdown
+// http://daringfireball.net/projects/markdown/
+// -------------------------------------------------------------------
+// Feel free to add more tags
+// -------------------------------------------------------------------
+mySettings = {
+ previewParserPath: '',
+ onShiftEnter: {keepDefault:false, openWith:'\n\n'},
+ markupSet: [
+ {name:'First Level Heading', key:'1', placeHolder:'Your title here...', closeWith:function(markItUp) { return miu.markdownTitle(markItUp, '=') } },
+ {name:'Second Level Heading', key:'2', placeHolder:'Your title here...', closeWith:function(markItUp) { return miu.markdownTitle(markItUp, '-') } },
+ {name:'Heading 3', key:'3', openWith:'### ', placeHolder:'Your title here...' },
+ {name:'Heading 4', key:'4', openWith:'#### ', placeHolder:'Your title here...' },
+ {name:'Heading 5', key:'5', openWith:'##### ', placeHolder:'Your title here...' },
+ {name:'Heading 6', key:'6', openWith:'###### ', placeHolder:'Your title here...' },
+ {separator:'---------------' },
+ {name:'Bold', key:'B', openWith:'**', closeWith:'**'},
+ {name:'Italic', key:'I', openWith:'_', closeWith:'_'},
+ {separator:'---------------' },
+ {name:'Bulleted List', openWith:'- ' },
+ {name:'Numeric List', openWith:function(markItUp) {
+ return markItUp.line+'. ';
+ }},
+ {separator:'---------------' },
+ {name:'Picture', key:'P', replaceWith:'![[![Alternative text]!]]([![Url:!:http://]!] "[![Title]!]")'},
+ {name:'Link', key:'L', openWith:'[', closeWith:']([![Url:!:http://]!] "[![Title]!]")', placeHolder:'Your text to link here...' },
+ {separator:'---------------'},
+ {name:'Quotes', openWith:'> '},
+ {name:'Code Block / Code', openWith:'(!(\t|!|`)!)', closeWith:'(!(`)!)'},
+ {separator:'---------------'},
+ {name:'Preview', call:'preview', className:"preview"}
+ ]
+}
+
+// mIu nameSpace to avoid conflict.
+miu = {
+ markdownTitle: function(markItUp, char) {
+ heading = '';
+ n = $.trim(markItUp.selection||markItUp.placeHolder).length;
+ for(i = 0; i < n; i++) {
+ heading += char;
+ }
+ return '\n'+heading;
+ }
+}
\ No newline at end of file
=== added file 'lava_markitup/static/lava_markitup/sets/markdown/style.css'
@@ -0,0 +1,54 @@
+/* -------------------------------------------------------------------
+// markItUp!
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp .markItUpButton1 a {
+ background-image:url(images/h1.png);
+}
+.markItUp .markItUpButton2 a {
+ background-image:url(images/h2.png);
+}
+.markItUp .markItUpButton3 a {
+ background-image:url(images/h3.png);
+}
+.markItUp .markItUpButton4 a {
+ background-image:url(images/h4.png);
+}
+.markItUp .markItUpButton5 a {
+ background-image:url(images/h5.png);
+}
+.markItUp .markItUpButton6 a {
+ background-image:url(images/h6.png);
+}
+
+.markItUp .markItUpButton7 a {
+ background-image:url(images/bold.png);
+}
+.markItUp .markItUpButton8 a {
+ background-image:url(images/italic.png);
+}
+
+.markItUp .markItUpButton9 a {
+ background-image:url(images/list-bullet.png);
+}
+.markItUp .markItUpButton10 a {
+ background-image:url(images/list-numeric.png);
+}
+
+.markItUp .markItUpButton11 a {
+ background-image:url(images/picture.png);
+}
+.markItUp .markItUpButton12 a {
+ background-image:url(images/link.png);
+}
+
+.markItUp .markItUpButton13 a {
+ background-image:url(images/quotes.png);
+}
+.markItUp .markItUpButton14 a {
+ background-image:url(images/code.png);
+}
+
+.markItUp .preview a {
+ background-image:url(images/preview.png);
+}
\ No newline at end of file
=== added directory 'lava_markitup/static/lava_markitup/skins'
=== added directory 'lava_markitup/static/lava_markitup/skins/simple'
=== added directory 'lava_markitup/static/lava_markitup/skins/simple/images'
=== added file 'lava_markitup/static/lava_markitup/skins/simple/images/handle.png'
Binary files lava_markitup/static/lava_markitup/skins/simple/images/handle.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/skins/simple/images/handle.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/skins/simple/images/menu.png'
Binary files lava_markitup/static/lava_markitup/skins/simple/images/menu.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/skins/simple/images/menu.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/skins/simple/images/submenu.png'
Binary files lava_markitup/static/lava_markitup/skins/simple/images/submenu.png 1970-01-01 00:00:00 +0000 and lava_markitup/static/lava_markitup/skins/simple/images/submenu.png 2011-09-23 08:17:10 +0000 differ
=== added file 'lava_markitup/static/lava_markitup/skins/simple/style.css'
@@ -0,0 +1,118 @@
+/* -------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp * {
+ margin:0px; padding:0px;
+ outline:none;
+}
+.markItUp a:link,
+.markItUp a:visited {
+ color:#000;
+ text-decoration:none;
+}
+.markItUp {
+ width:700px;
+ margin:5px 0 5px 0;
+}
+.markItUpContainer {
+ font:11px Verdana, Arial, Helvetica, sans-serif;
+}
+.markItUpEditor {
+ font:12px 'Courier New', Courier, monospace;
+ padding:5px;
+ width:690px;
+ height:320px;
+ clear:both;
+ line-height:18px;
+ overflow:auto;
+}
+.markItUpPreviewFrame {
+ overflow:auto;
+ background-color:#FFF;
+ width:99.9%;
+ height:300px;
+ margin:5px 0;
+}
+.markItUpFooter {
+ width:100%;
+}
+.markItUpResizeHandle {
+ overflow:hidden;
+ width:22px; height:5px;
+ margin-left:auto;
+ margin-right:auto;
+ background-image:url(images/handle.png);
+ cursor:n-resize;
+}
+/***************************************************************************************/
+/* first row of buttons */
+.markItUpHeader ul li {
+ list-style:none;
+ float:left;
+ position:relative;
+}
+.markItUpHeader ul li:hover > ul{
+ display:block;
+}
+.markItUpHeader ul .markItUpDropMenu {
+ background:transparent url(images/menu.png) no-repeat 115% 50%;
+ margin-right:5px;
+}
+.markItUpHeader ul .markItUpDropMenu li {
+ margin-right:0px;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul {
+ display:none;
+ position:absolute;
+ top:18px; left:0px;
+ background:#FFF;
+ border:1px solid #000;
+}
+.markItUpHeader ul ul li {
+ float:none;
+ border-bottom:1px solid #000;
+}
+.markItUpHeader ul ul .markItUpDropMenu {
+ background:#FFF url(images/submenu.png) no-repeat 100% 50%;
+}
+.markItUpHeader ul .markItUpSeparator {
+ margin:0 10px;
+ width:1px;
+ height:16px;
+ overflow:hidden;
+ background-color:#CCC;
+}
+.markItUpHeader ul ul .markItUpSeparator {
+ width:auto; height:1px;
+ margin:0px;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul ul {
+ position:absolute;
+ top:-1px; left:150px;
+}
+.markItUpHeader ul ul ul li {
+ float:none;
+}
+.markItUpHeader ul a {
+ display:block;
+ width:16px; height:16px;
+ text-indent:-10000px;
+ background-repeat:no-repeat;
+ padding:3px;
+ margin:0px;
+}
+.markItUpHeader ul ul a {
+ display:block;
+ padding-left:0px;
+ text-indent:0;
+ width:120px;
+ padding:5px 5px 5px 25px;
+ background-position:2px 50%;
+}
+.markItUpHeader ul ul a:hover {
+ color:#FFF;
+ background-color:#000;
+}
=== added directory 'lava_markitup/static/lava_markitup/templates'
=== added file 'lava_markitup/static/lava_markitup/templates/preview.css'
@@ -0,0 +1,5 @@
+/* preview style examples */
+body {
+ background-color:#EFEFEF;
+ font:70% Verdana, Arial, Helvetica, sans-serif;
+}
\ No newline at end of file
=== added file 'lava_markitup/static/lava_markitup/templates/preview.html'
@@ -0,0 +1,11 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>markItUp! preview template</title>
+<link rel="stylesheet" type="text/css" href="~/templates/preview.css" />
+</head>
+<body>
+<!-- content -->
+</body>
+</html>
=== added directory 'lava_markitup/templates'
=== added directory 'lava_markitup/templates/lava_markitup'
=== added file 'lava_markitup/templates/lava_markitup/preview.html'
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ LANGUAGE_CODE }}" xml:lang="{{ LANGUAGE_CODE }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <title>Preview</title>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava_markitup/preview.css"/>
+ </head>
+ <body>
+ {{ markup|safe }}
+ </body>
+</html>
+
=== added file 'lava_markitup/tests.py'
=== added file 'lava_markitup/urls.py'
@@ -0,0 +1,7 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns(
+ 'lava_markitup.views',
+ url('^markdown/$', 'preview_markdown', name='lava.markitup.markdown'),
+)
=== added file 'lava_markitup/views.py'
@@ -0,0 +1,18 @@
+from django.contrib.csrf.middleware import csrf_exempt
+from django.http import HttpResponse
+from django.template import RequestContext, loader
+from markdown import markdown
+
+
+@csrf_exempt
+def preview_markdown(request):
+ """
+ Convert markdown to HTML
+ """
+ markup = request.POST.get("data", "")
+ template_name = "lava_markitup/preview.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'markup': markdown(markup, safe_mode="escape")
+ })
+ return HttpResponse(t.render(c))
=== added directory 'lava_projects'
=== added file 'lava_projects/__init__.py'
@@ -0,0 +1,19 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+__version__ = (0, 1, 0, "dev", 0)
=== added file 'lava_projects/admin.py'
@@ -0,0 +1,33 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from django.contrib import admin
+
+from lava_projects.models import Project, ProjectFormerIdentifier
+
+
+class ProjectAdmin(admin.ModelAdmin):
+ list_display=('__unicode__', 'identifier', 'registered_by', 'registered_on', 'is_public', 'owner')
+
+
+class ProjectFormerIdentifierAdmin(admin.ModelAdmin):
+ list_display=('former_identifier', 'project', 'renamed_on', 'renamed_by')
+
+
+admin.site.register(Project, ProjectAdmin)
+admin.site.register(ProjectFormerIdentifier, ProjectFormerIdentifierAdmin)
=== added file 'lava_projects/extension.py'
@@ -0,0 +1,47 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from lava_server.extension import LavaServerExtension
+
+
+class ProjectExtension(LavaServerExtension):
+ """
+ Extension adding project support
+ """
+
+ @property
+ def app_name(self):
+ return "lava_projects"
+
+ @property
+ def name(self):
+ return "Projects"
+
+ @property
+ def main_view_name(self):
+ return "lava.project.root"
+
+ @property
+ def description(self):
+ return "Project support for LAVA"
+
+ @property
+ def version(self):
+ from lava_projects import __version__
+ import versiontools
+ return versiontools.format_version(__version__)
=== added file 'lava_projects/forms.py'
@@ -0,0 +1,173 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from django import forms
+from django.contrib.auth.models import Group
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
+
+from lava_projects.models import Project
+
+
+class _ProjectForm(forms.Form):
+ """
+ Mix-in with common project fields.
+ """
+
+ name = forms.CharField(
+ label=_(u"Projet name"),
+ help_text=_(u"Any name of your liking, can be updated later if you"
+ u" change your mind"),
+ required=True,
+ max_length=100)
+
+ description = forms.CharField(
+ label=_(u"Description"),
+ help_text=_(u"Arbitrary text about the project, you can use markdown"
+ u" formatting to style it"),
+ widget=forms.widgets.Textarea(),
+ required=False)
+
+ group = forms.ModelChoiceField(
+ label=_(u"Group owner"),
+ help_text=_(u"Members of the selected group will co-own this project"),
+ empty_label=_("None, I'll be the owner"),
+ required=False,
+ queryset=Group.objects.all())
+
+ is_public = forms.BooleanField(
+ label=_(u"Project is public"),
+ help_text=_(u"If selected then this project will be visible to anyone."
+ u" Otherwise only owners can see the project"),
+ required=False)
+
+ is_aggregate = forms.BooleanField(
+ label=_(u"Project is an aggregation (distribution)"),
+ help_text=_(u"If selected the project will be treated like a"
+ u" distribution. Some UI elements are optimized for that case"
+ u" and behave differently."),
+ required=False)
+
+ def restrict_group_selection_for_user(self, user):
+ assert user is not None
+ self.fields['group'].queryset = user.groups.all()
+
+
+class ProjectRegistrationForm(_ProjectForm):
+ """
+ Form for registering new projects.
+ """
+
+ identifier = forms.CharField(
+ label=_(u"Identifier"),
+ help_text=_(u"A unique identifier built from restricted subset of"
+ u" characters (only basic lowercase letters, numbers and"
+ u" dash)"),
+ required=True,
+ max_length=100)
+
+ def __init__(self, *args, **kwargs):
+ super(ProjectRegistrationForm, self).__init__(*args, **kwargs)
+ self._reorder_fields(['name', 'identifier'])
+
+ def _reorder_fields(self, first):
+ for field in reversed(first):
+ self.fields.keyOrder.remove(field)
+ self.fields.keyOrder.insert(0, field)
+
+ def clean_identifier(self):
+ """
+ Check that the identifier is correct:
+
+ 1) It does not collide with other projects
+ 2) Or their past identifiers
+ """
+ value = self.cleaned_data['identifier']
+ try:
+ # Lookup project that is, or was, this identifier
+ project = Project.objects.all().get_by_identifier(value)
+ if project.identifier == value:
+ # Disallow current identifiers from other projects
+ raise ValidationError("Project %s is already using this identifier" % project)
+ else:
+ # Disallow past identifiers from other projects
+ raise ValidationError("Project %s was using this identifier in the past" % project)
+ except Project.DoesNotExist:
+ pass
+ return value
+
+
+class ProjectUpdateForm(_ProjectForm):
+ """
+ Form for updating project data
+ """
+
+
+class ProjectRenameForm(forms.Form):
+ """
+ Form for changing the project identifier
+ """
+
+ name = forms.CharField(
+ label=_(u"Projet name"),
+ help_text=_(u"The new project name, same limits as before (100 chars)"),
+ required=True,
+ max_length=100)
+
+ identifier = forms.CharField(
+ label=_(u"New identifier"),
+ help_text=_(u"The new identifier has to be different from any current"
+ u" or past identifier used by other projects."),
+ required=True,
+ max_length=100)
+
+ def __init__(self, project, *args, **kwargs):
+ super(ProjectRenameForm, self).__init__(*args, **kwargs)
+ self.project = project
+
+ def clean_identifier(self):
+ """
+ Check that new identifier is correct:
+
+ 1) It does not collide with other projects
+ 2) Or their past identifiers
+ 3) It is different than the one we are currently using
+ """
+ value = self.cleaned_data['identifier']
+ try:
+ # Lookup project that is, or was, using this identifier
+ project = Project.objects.all().get_by_identifier(value)
+ if project == self.project and project.identifier != value:
+ # Allow reusing identifiers inside one project
+ pass
+ elif project == self.project and project.identifier == value:
+ raise ValidationError(
+ _(u"The new identifier has to be different than the one"
+ u"you are currently using"))
+ elif project.identifier == value:
+ # Disallow current identifiers from other projects
+ raise ValidationError(
+ _(u"Project %s is already using this identifier") % project)
+ else:
+ # Disallow past identifiers from other projects
+ raise ValidationError(
+ _(u"Project %s was using this identifier in the past") %
+ project)
+ except Project.DoesNotExist:
+ pass
+ return value
=== added directory 'lava_projects/migrations'
=== added file 'lava_projects/migrations/0001_add_model_Project.py'
@@ -0,0 +1,91 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Project'
+ db.create_table('lava_projects_project', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], null=True, blank=True)),
+ ('is_public', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
+ ('identifier', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=100, db_index=True)),
+ ('description', self.gf('django.db.models.fields.TextField')(blank=True)),
+ ('is_aggregate', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('registered_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='projects', to=orm['auth.User'])),
+ ('registered_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('lava_projects', ['Project'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Project'
+ db.delete_table('lava_projects_project')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'lava_projects.project': {
+ 'Meta': {'object_name': 'Project'},
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}),
+ 'is_aggregate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'registered_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
+ 'registered_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'lava_projects.projectfomeridentifier': {
+ 'Meta': {'object_name': 'ProjectFomerIdentifier'},
+ 'former_identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'former_identifiers'", 'to': "orm['lava_projects.Project']"})
+ }
+ }
+
+ complete_apps = ['lava_projects']
=== added file 'lava_projects/migrations/0002_add_model_ProjectFormerIdentifier.py'
@@ -0,0 +1,88 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'ProjectFormerIdentifier'
+ db.create_table('lava_projects_projectformeridentifier', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='former_identifiers', to=orm['lava_projects.Project'])),
+ ('former_identifier', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=100, db_index=True)),
+ ('renamed_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='project_former_identifiers_created', to=orm['auth.User'])),
+ ('renamed_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('lava_projects', ['ProjectFormerIdentifier'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'ProjectFormerIdentifier'
+ db.delete_table('lava_projects_projectformeridentifier')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'lava_projects.project': {
+ 'Meta': {'object_name': 'Project'},
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}),
+ 'is_aggregate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'registered_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
+ 'registered_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'lava_projects.projectformeridentifier': {
+ 'Meta': {'object_name': 'ProjectFormerIdentifier'},
+ 'former_identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'former_identifiers'", 'to': "orm['lava_projects.Project']"}),
+ 'renamed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_former_identifiers_created'", 'to': "orm['auth.User']"}),
+ 'renamed_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['lava_projects']
=== added file 'lava_projects/migrations/__init__.py'
=== added file 'lava_projects/models.py'
@@ -0,0 +1,171 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import models
+from django.db.models.query import QuerySet
+from django.utils.translation import ugettext as _
+from django_restricted_resource.managers import RestrictedResourceManager
+from django_restricted_resource.models import RestrictedResource
+
+
+class ProjectQuerySet(QuerySet):
+ """
+ Query set with extra methods for projects
+ """
+
+ def recently_registered(self):
+ return self.order_by("-registered_on")[:10]
+
+ def get_by_identifier(self, identifier):
+ """
+ Get project by identifier, also searching for past
+ identifiers (project renames)
+ """
+ try:
+ return self.get(identifier=identifier)
+ except Project.DoesNotExist as no_such_project:
+ try:
+ project_former_identifier = ProjectFormerIdentifier.objects.get(former_identifier=identifier)
+ return project_former_identifier.project
+ except ProjectFormerIdentifier.DoesNotExist:
+ raise no_such_project
+
+
+class ProjectManager(RestrictedResourceManager):
+ """
+ Manager with custom query set for projects
+ """
+ use_for_related_fields = True
+
+ def get_query_set(self):
+ return ProjectQuerySet(self.model, using=self._db)
+
+ def recently_registered(self):
+ return self.get_query_set().recently_registered()
+
+ def get_by_identifier(self, identifier):
+ return self.get_query_set().get_by_identifier(identifier)
+
+
+class ProjectFormerIdentifier(models.Model):
+ """
+ Former identifier of a project. Allows users to change the project
+ identifier while keeping URLs working properly.
+ """
+
+ project = models.ForeignKey(
+ "Project",
+ null=False,
+ blank=False,
+ verbose_name=_(u"Project"),
+ related_name="former_identifiers")
+
+ former_identifier = models.SlugField(
+ null=False,
+ blank=False,
+ max_length=100,
+ verbose_name=_(u"Former identifier"),
+ help_text=_(u"A unique identifier built from restricted subset of"
+ u" characters (only basic letters, numbers and dash)"),
+ unique=True)
+
+ renamed_by = models.ForeignKey(
+ User,
+ related_name="project_former_identifiers_created",
+ blank=False,
+ null=False,
+ verbose_name=_(u"Renamed by"),
+ help_text=_(u"User who renamed the project"))
+
+ renamed_on = models.DateTimeField(
+ auto_now_add=True,
+ blank=False,
+ null=False,
+ verbose_name=_(u"Renamed on"),
+ help_text=_(u"Date and time of rename operation"))
+
+ def __unicode__(self):
+ return self.former_identifier
+
+
+class Project(RestrictedResource):
+ """
+ Project is a container of everything else. Projects are restricted
+ resources and thus belong to a particular user or group and have a "public"
+ flag.
+ """
+
+ name = models.CharField(
+ null=False,
+ blank=False,
+ max_length=100,
+ verbose_name=_(u"Name"),
+ help_text=_(u"A unique identifier built from restricted subset of"
+ u" characters (only basic lowercase letters, numbers and"
+ u" dash). Changing this field will break existing links"
+ u" and is not recommended"))
+
+ identifier = models.SlugField(
+ null=False,
+ blank=False,
+ max_length=100,
+ verbose_name=_(u"Identifier"),
+ help_text=_(u"A unique identifier built from restricted subset of"
+ u" characters (only basic letters, numbers and dash)"),
+ unique=True)
+
+ description = models.TextField(
+ null=False,
+ blank=True,
+ verbose_name = _(u"Description"),
+ help_text = _(u"Arbitrary text about the project, you can use markdown"
+ u" formatting to style it"))
+
+ is_aggregate = models.BooleanField(
+ blank=True,
+ null=False,
+ verbose_name=_(u"Aggregate"),
+ help_text=_(u"When selected the project will be treated like a"
+ u" distribution. Some UI elements are optimized for that case"
+ u" and behave differently."))
+
+ registered_by = models.ForeignKey(
+ User,
+ related_name="projects",
+ blank=False,
+ null=False,
+ verbose_name=_(u"Registered by"),
+ help_text=_(u"User who registered this project"))
+
+ registered_on = models.DateTimeField(
+ auto_now_add=True,
+ blank=False,
+ null=False,
+ verbose_name=_(u"Registered on"),
+ help_text=_(u"Date and time of registration"))
+
+ objects = ProjectManager()
+
+ def __unicode__(self):
+ return self.name
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('lava.project.detail', [self.identifier])
=== added directory 'lava_projects/templates'
=== added directory 'lava_projects/templates/lava_projects'
=== added file 'lava_projects/templates/lava_projects/_project_actions.html'
@@ -0,0 +1,5 @@
+<h3>Actions</h3>
+<ul>
+ <li><a href="{% url lava.project.list %}">Display a list of all projects</a></li>
+ <li><a href="{% url lava.project.register %}">Register a new project</a></li>
+</ul>
=== added file 'lava_projects/templates/lava_projects/_project_form.html'
@@ -0,0 +1,47 @@
+{% extends "layouts/content.html" %}
+
+
+{% block extrahead %}
+{{ block.super }}
+<script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery.slugify.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}lava_markitup/jquery.markitup.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}lava_markitup/sets/markdown/set.js"></script>
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava_markitup/skins/simple/style.css"/>
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava_markitup/sets/markdown/style.css"/>
+{% endblock %}
+
+
+{% block content %}
+<style type="text/css">
+ iframe { border: 1px dotted #333; }
+</style>
+<form method="post" action="">
+ {% block form_header %}
+ {% endblock %}
+ {% csrf_token %}
+ <table class="form_helper">
+ {% for field in form %}
+ {% block form_field %}
+ <tr>
+ <th><label for="{{ field.id_for_label }}">{{ field.label }}</label></th>
+ <td>
+ {% if field.errors %}
+ <div class="ui-state-error">
+ {{ field.errors }}
+ {{ field }}
+ </div>
+ {% else %}
+ {{ field }}
+ {% endif %}
+ <p class='help_text'>{{ field.help_text }}</p>
+ </td>
+ </tr>
+ {% endblock %}
+ {% endfor %}
+ </table>
+ {% block form_footer %}
+ <h2>Continue</h2>
+ <input type="submit" value="Save"/>
+ {% endblock %}
+</form>
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_detail.html'
@@ -0,0 +1,43 @@
+{% extends "layouts/content_with_sidebar.html" %}
+{% load markup %}
+
+
+{% block sidebar %}
+{% include "lava_projects/_project_actions.html" %}
+{% if belongs_to_user %}
+<h3>Project administration</h3>
+<ul>
+ <li><a href="{% url lava.project.update project.identifier %}">Update this project</a></li>
+</ul>
+{% endif %}
+{% endblock %}
+
+
+{% block content %}
+{% if former_identifier %}
+<div class="ui-state-highlight">
+ <p><span
+ class="ui-icon ui-icon-info"
+ style="float: left; margin: 2pt"></span>
+ This project was renamed to <strong>{{ project.identifier }}</strong>
+ {{ former_identifier.renamed_on|timesince }} ago. The old URL referring to
+ <strong>{{ former_identifier.former_identifier }}</strong> will redirect here
+ automatically but you should update your bookmarks or hyper-links</p>
+</div>
+{% endif %}
+<h1>{{ project }}</h1>
+<p>
+{% if project.user == project.registered_by %}
+Owned and registered by {{ project.owner }} on {{ project.registered_on|date }}
+{% else %}
+Owned by {{ project.owner }}
+{% if project.group and belongs_to_user %}
+<em>(you are a member of this group)</em>
+{% endif %}
+, registered by {{ project.registered_by }} on {{ project.registered_on|date }}
+{% endif %}
+</p>
+<div class="project-description">
+ {{ project.description|markdown }}
+</div>
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_list.html'
@@ -0,0 +1,19 @@
+{% extends "layouts/content_with_sidebar.html" %}
+
+
+{% block sidebar %}
+{% include "lava_projects/_project_actions.html" %}
+{% endblock %}
+
+
+{% block content %}
+<h2>Existing projects</h2>
+<ul>
+ {% for project in project_list %}
+ <li>
+ <a href="{{ project.get_absolute_url }}">{{ project }}</a>
+ {% if not project.is_public %}<strong>(private)</strong>{% endif %}
+ </li>
+ {% endfor %}
+</ul>
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_register_form.html'
@@ -0,0 +1,18 @@
+{% extends "lava_projects/_project_form.html" %}
+
+
+{% block form_header %}
+<h1>Register a project in LAVA</h1>
+{% endblock %}
+
+
+{% block form_footer %}
+{{ block.super }}
+<script type="text/javascript">
+ mySettings.previewParserPath = "{% url lava.markitup.markdown %}";
+ $(document).ready(function() {
+ $("#id_identifier").slugify($("#id_name"));
+ $("#id_description").markItUp(mySettings);
+ });
+</script>
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_rename_form.html'
@@ -0,0 +1,35 @@
+{% extends "lava_projects/_project_form.html" %}
+
+
+{% block form_header %}
+<h1>Rename {{ project }}</h1>
+<p>Renaming a project is always possible but should be done carefully.
+Each historic identifier is stored to ensure old URLs continue to work. They
+are also reserved to prevent new projects from using them.</p>
+{% endblock %}
+
+
+{% block form_field %}
+{% if field.name == "identifier" %}
+<tr>
+ <th>Old identifier</th>
+ <td>
+ <input type="text" disabled="disabled" value="{{ project.identifier }}"></input>
+ <p class='help_text'>This is your current project identifier</p>
+ </td>
+</tr>
+{% endif %}
+{{ block.super }}
+{% endblock %}
+
+
+{% block form_footer %}
+<h2>Continue</h2>
+<input type="submit" value="Save"/> or <a href="{{ project.get_absolute_url }}">cancel</a>
+<script type="text/javascript">
+ $(document).ready(function() {
+ // Remove existing value, enable slugify helper
+ $("#id_identifier").val("").slugify($("#id_name"));
+ });
+</script>
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_root.html'
@@ -0,0 +1,30 @@
+{% extends "layouts/content_with_sidebar.html" %}
+
+
+{% block sidebar %}
+{% include "lava_projects/_project_actions.html" %}
+{% endblock %}
+
+
+{% block content %}
+<h2>Projects</h2>
+<p>LAVA uses projects to organize other data structures. Projects are similar
+to the project concept in other development tools and web applications.</p>
+
+<p>Anyone is free to <a href="{% url lava.project.register %}">register a
+ project</a> and use that to coordinate testing efforts.</p>
+
+{% if recent_project_list %}
+<h3>Recently registered projects</h3>
+<ul>
+ {% for project in recent_project_list %}
+ <li>
+ <a href="{{ project.get_absolute_url }}">{{ project }}</a>,
+ registered {{ project.registered_on|timesince }} ago
+ {% if not project.is_public %}<strong>(private)</strong>{% endif %}
+ </li>
+ {% endfor %}
+</ul>
+<p><a href="{% url lava.project.list %}">See all projects</a></p>
+{% endif %}
+{% endblock %}
=== added file 'lava_projects/templates/lava_projects/project_update_form.html'
@@ -0,0 +1,33 @@
+{% extends "lava_projects/_project_form.html" %}
+
+
+{% block form_header %}
+<h1>Update {{ project }}</h1>
+{% endblock %}
+
+
+{% block form_field %}
+{{ block.super }}
+{% if field.name == "name" %}
+<tr>
+ <th>Identifier<br/><small>(read only)</small></th>
+ <td>
+ <input type="text" disabled="disabled" value="{{ project.identifier }}"></input>
+ <p class='help_text'>
+ <a href="{% url lava.project.rename project.identifier %}">Rename this project</a> if you wish to change the identifier</p>
+ </td>
+</tr>
+{% endif %}
+{% endblock %}
+
+
+{% block form_footer %}
+<h2>Continue</h2>
+<input type="submit" value="Save"/> or <a href="{{ project.get_absolute_url }}">cancel</a>
+<script type="text/javascript">
+ mySettings.previewParserPath = "{% url lava.markitup.markdown %}";
+ $(document).ready(function() {
+ $("#id_description").markItUp(mySettings);
+ });
+</script>
+{% endblock %}
=== added file 'lava_projects/urls.py'
@@ -0,0 +1,30 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns(
+ 'lava_projects.views',
+ url('^$', 'project_root', name='lava.project.root'),
+ url(r'^\+list/$', 'project_list', name='lava.project.list'),
+ url(r'^\+register/$', 'project_register', name='lava.project.register'),
+ url(r'^(?P<identifier>[a-z0-9-]+)/$', 'project_detail', name='lava.project.detail'),
+ url(r'^(?P<identifier>[a-z0-9-]+)/\+update/$', 'project_update', name='lava.project.update'),
+ url(r'^(?P<identifier>[a-z0-9-]+)/\+rename/$', 'project_rename', name='lava.project.rename'),
+)
=== added file 'lava_projects/views.py'
@@ -0,0 +1,179 @@
+# Copyright (C) 2010, 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of LAVA Server.
+#
+# LAVA Server is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# LAVA Server 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 Affero General Public License
+# along with LAVA Server. If not, see <http://www.gnu.org/licenses/>.
+
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.template import RequestContext, loader
+from django.utils.translation import ugettext as _
+from django.views.generic.list_detail import object_list, object_detail
+
+from lava_projects.models import (
+ Project,
+ ProjectFormerIdentifier,
+)
+from lava_projects.forms import (
+ ProjectRenameForm,
+ ProjectRegistrationForm,
+ ProjectUpdateForm,
+)
+
+
+def project_root(request):
+ template_name = "lava_projects/project_root.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'recent_project_list': Project.objects.accessible_by_principal(request.user).recently_registered()
+ })
+ return HttpResponse(t.render(c))
+
+
+def project_list(request):
+ return object_list(
+ request,
+ queryset=Project.objects.accessible_by_principal(request.user),
+ template_name="lava_projects/project_list.html",
+ template_object_name="project")
+
+
+def project_detail(request, identifier):
+ # A get by identifier, looking at renames, if needed.
+ try:
+ project = Project.objects.accessible_by_principal(request.user).get_by_identifier(identifier)
+ except Project.DoesNotExist:
+ raise Http404("No such project")
+ # Redirect users to proper URL of this project if using one of the older names.
+ if project.identifier != identifier:
+ return HttpResponseRedirect(project.get_absolute_url() + "?former_identifier=" + identifier)
+ # Lookup former identifier if we have been redirected
+ former_identifier = None
+ if request.GET.get("former_identifier"):
+ try:
+ former_identifier = ProjectFormerIdentifier.objects.get(
+ former_identifier=request.GET.get("former_identifier"))
+ except ProjectFormerIdentifier.DoesNotExist:
+ pass
+ # Render to template
+ template_name = "lava_projects/project_detail.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'project': project,
+ 'former_identifier': former_identifier,
+ 'belongs_to_user': project.is_owned_by(request.user),
+ })
+ return HttpResponse(t.render(c))
+
+
+@login_required
+def project_register(request):
+ if request.method == 'POST':
+ form = ProjectRegistrationForm(request.POST, request.FILES)
+ form.restrict_group_selection_for_user(request.user)
+ # Check the form
+ if form.is_valid():
+ # And make a project instance
+ project = Project.objects.create(
+ name=form.cleaned_data['name'],
+ identifier=form.cleaned_data['identifier'],
+ description=form.cleaned_data['description'],
+ is_aggregate=form.cleaned_data['is_aggregate'],
+ owner=form.cleaned_data['group'] or request.user,
+ is_public=form.cleaned_data['is_public'],
+ registered_by=request.user)
+ return HttpResponseRedirect(project.get_absolute_url())
+ else:
+ form = ProjectRegistrationForm()
+ # Render to template
+ template_name = "lava_projects/project_register_form.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'form': form,
+ })
+ return HttpResponse(t.render(c))
+
+
+@login_required
+def project_update(request, identifier):
+ project = get_object_or_404(
+ Project.objects.accessible_by_principal(request.user),
+ identifier=identifier)
+ if not project.is_owned_by(request.user):
+ return HttpResponseForbidden("You cannot update this project")
+ if request.method == 'POST':
+ form = ProjectUpdateForm(request.POST, request.FILES)
+ form.restrict_group_selection_for_user(request.user)
+ if form.is_valid():
+ project.name = form.cleaned_data['name']
+ project.description = form.cleaned_data['description']
+ project.is_aggregate = form.cleaned_data['is_aggregate']
+ project.owner = form.cleaned_data['group'] or request.user
+ project.is_public = form.cleaned_data['is_public']
+ project.save()
+ return HttpResponseRedirect(project.get_absolute_url())
+ else:
+ form = ProjectUpdateForm(initial=dict(
+ name=project.name,
+ description=project.description,
+ is_aggregate=project.is_aggregate,
+ group=project.group,
+ is_public=project.is_public))
+ template_name = "lava_projects/project_update_form.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'form': form,
+ 'project': project,
+ })
+ return HttpResponse(t.render(c))
+
+
+@login_required
+def project_rename(request, identifier):
+ project = get_object_or_404(
+ Project.objects.accessible_by_principal(request.user),
+ identifier=identifier)
+ if not project.is_owned_by(request.user):
+ return HttpResponseForbidden("You cannot update this project")
+ if request.method == 'POST':
+ form = ProjectRenameForm(project, request.POST)
+ if form.is_valid():
+ # Remove old entry if we are reusing our older identifier
+ pfi = ProjectFormerIdentifier.objects.filter(
+ former_identifier=form.cleaned_data['identifier'],
+ project=project.pk).delete()
+ # Record the change taking place
+ ProjectFormerIdentifier.objects.create(
+ project=project,
+ former_identifier=project.identifier,
+ renamed_by=request.user)
+ # And update the project
+ project.name = form.cleaned_data['name']
+ project.identifier = form.cleaned_data['identifier']
+ project.save()
+ return HttpResponseRedirect(project.get_absolute_url())
+ else:
+ form = ProjectRenameForm(
+ project, initial={
+ 'name': project.name,
+ 'identifier': project.identifier})
+ template_name = "lava_projects/project_rename_form.html"
+ t = loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'form': form,
+ 'project': project,
+ })
+ return HttpResponse(t.render(c))
=== modified file 'lava_server/htdocs/css/Aristo/jquery-ui-1.8.7.custom.css'
@@ -56,9 +56,9 @@
/* Component containers
----------------------------------*/
-.ui-widget { font-family: Helvetica,Arial,sans-serif; font-size: 1.1em; }
+.ui-widget { font-family: Arial,sans-serif; font-size: 1.1em; }
.ui-widget .ui-widget { font-size: 1em; }
-.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Helvetica,Arial,sans-serif; font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Arial,sans-serif; font-size: 1em; }
.ui-widget-content { border: 1px solid #B6B6B6; background: #ffffff; color: #4F4F4F; }
.ui-widget-content a { color: #4F4F4F; }
.ui-widget-header { border: 1px solid #B6B6B6; color: #4F4F4F; font-weight: bold; }
@@ -98,6 +98,7 @@
);
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset;
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset;
+ box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset;
}
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #4F4F4F; text-decoration: none; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #9D9D9D; font-weight: normal; color: #313131; }
@@ -107,11 +108,11 @@
color: #1c4257; border: 1px solid #7096ab;
background: url(images/bg_fallback.png) 0 -50px repeat-x;
background: -webkit-gradient(
- linear,
- left bottom,
- left top,
- color-stop(1, rgb(185,224,245)),
- color-stop(0, rgb(146,189,214))
+ linear,
+ left bottom,
+ left top,
+ color-stop(1, rgb(185,224,245)),
+ color-stop(0, rgb(146,189,214))
);
background: -moz-linear-gradient(
center top,
@@ -120,6 +121,7 @@
);
-webkit-box-shadow: none;
-moz-box-shadow: none;
+ box-shadow: none;
}
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #313131; text-decoration: none; }
.ui-widget :active { outline: none; }
@@ -401,7 +403,7 @@
* http://docs.jquery.com/UI/Autocomplete#theming
*/
.ui-autocomplete {
- position: absolute; cursor: default; z-index: 3 !important;
+ position: absolute; cursor: default; z-index: 3;
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
-moz-box-shadow: 0 1px 5px rgba(0,0,0,0.3);
-webkit-box-shadow: 0 1px 5px rgba(0,0,0,0.3);
@@ -471,33 +473,36 @@
/* button animation properties */
.ui-button {
- -webkit-transition: all 0.25s ease-in-out;
- -moz-transition: all 0.25s ease-in-out;
- -o-transition: all 0.25s ease-in-out;
+ -webkit-transition: -webkit-box-shadow 0.25s ease-in-out;
+ -moz-transition: -moz-box-shadow 0.25s ease-in-out;
+ -o-transition: -o-box-shadow 0.25s ease-in-out;
}
/*states*/
.ui-button.ui-state-hover {
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
}
.ui-button.ui-state-focus {
outline: none;
color: #1c4257; border-color: #7096ab;
- background-image: -webkit-gradient(
- linear,
- left bottom,
- left top,
- color-stop(1, rgb(185,224,245)),
- color-stop(0, rgb(146,189,214))
+ background: url(images/bg_fallback.png) 0 -50px repeat-x;
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ color-stop(1, rgb(185,224,245)),
+ color-stop(0, rgb(146,189,214))
);
- background-image: -moz-linear-gradient(
+ background: -moz-linear-gradient(
center top,
rgb(185,224,245),
rgb(146,189,214)
);
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
+ -moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
+ -webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset;
}
/*button text element */
@@ -508,7 +513,19 @@
.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
/* no icon support for input elements, provide padding by default */
-input.ui-button { padding: .4em 1em; }
+input.ui-button { font-size: 14px; font-weight: bold; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); padding: 0 1em !important; height: 33px; }
+/*remove submit button internal padding in Firefox*/
+input.ui-button::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+/* fix webkits handling of the box model */
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ input.ui-button {
+ height: 31px !important;
+ }
+}
+
/*button icon element(s) */
.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
@@ -523,11 +540,11 @@
.ui-buttonset .ui-button.ui-state-active { color: #1c4257; border-color: #7096ab; }
.ui-buttonset .ui-button.ui-state-active {
background-image: -webkit-gradient(
- linear,
- left bottom,
- left top,
- color-stop(1, rgb(185,224,245)),
- color-stop(0, rgb(146,189,214))
+ linear,
+ left bottom,
+ left top,
+ color-stop(1, rgb(185,224,245)),
+ color-stop(0, rgb(146,189,214))
);
background-image: -moz-linear-gradient(
center top,
@@ -536,6 +553,7 @@
);
-webkit-box-shadow: none;
-moz-box-shadow: none;
+ box-shadow: none;
}
/* workarounds */
@@ -575,12 +593,12 @@
*
* http://docs.jquery.com/UI/Slider#theming
*/
-.ui-slider { position: relative; text-align: left; background: #d7d7d7; }
-.ui-slider { -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; }
-.ui-slider .ui-slider-handle { background: url(images/slider_handles.png) 0px -23px no-repeat; position: absolute; z-index: 2; width: 23px; height: 23px; cursor: default; border: none; outline: none; -moz-box-shadow: none; -webkit-box-shadow: none; }
+.ui-slider { position: relative; text-align: left; background: #d7d7d7; z-index: 1; }
+.ui-slider { -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; }
+.ui-slider .ui-slider-handle { background: url(images/slider_handles.png) 0px -23px no-repeat; position: absolute; z-index: 2; width: 23px; height: 23px; cursor: default; border: none; outline: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }
.ui-slider .ui-state-hover, .ui-slider .ui-state-active { background-position: 0 0; }
.ui-slider .ui-slider-range { background: #a3cae0; position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
-.ui-slider .ui-slider-range { -moz-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; -webkit-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; }
+.ui-slider .ui-slider-range { -moz-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; -webkit-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; }
.ui-slider-horizontal { height: 5px; }
@@ -602,7 +620,7 @@
*
* http://docs.jquery.com/UI/Tabs#theming
*/
-.ui-tabs { position: relative; zoom: 1; border: 0; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs { position: relative; zoom: 1; border: 0; background: transparent; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
.ui-tabs .ui-tabs-nav { margin: 0; padding: 0; background: transparent; border-width: 0 0 1px 0; }
.ui-tabs .ui-tabs-nav {
-moz-border-radius: 0;
@@ -615,7 +633,7 @@
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; outline: none; }
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
.ui-tabs .ui-tabs-panel { display: block; border-width: 0 1px 1px 1px; padding: 1em 1.4em; background: none; }
-.ui-tabs .ui-tabs-panel {
+.ui-tabs .ui-tabs-panel { background: #FFF;
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
@@ -710,7 +728,7 @@
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; background: url(images/progress_bar.gif) 0 0 repeat-x; }
/* Extra Input Field Styling */
-textarea, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
+.ui-form textarea, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]) {
padding: 3px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
@@ -724,13 +742,13 @@
-moz-transition: all 0.25 ease-in-out;
-o-transition: all 0.25s ease-in-out;
}
-textarea:hover, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover {
+.ui-form textarea:hover, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):hover {
border: 1px solid #bdbdbd;
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2);
-moz-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2);
box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2);
}
-textarea:focus, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus {
+.ui-form textarea:focus, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):focus {
border: 1px solid #95bdd4;
-webkit-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2);
-moz-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2);
=== modified file 'lava_server/htdocs/css/default.css'
@@ -61,14 +61,31 @@
/* User Box (part of header) */
#lava-server-user-box {
- text-align: right;
font-size: 9pt;
+ /* Don't inherit font family, we're moved around and header uses ubuntu light */
+ font-family: "Ubuntu", Helvetica Neue, Verdana, sans-serif !important;
}
#lava-server-user-box span.username {
font-weight: bold;
}
+#lava-server-header #lava-server-user-box {
+ /* Float right only when in the user box.
+ * When relocated to the sidebar it will
+ * Vnot be floating anymore */
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 2pt 2pt 5pt 5pt;
+}
+
+#lava-server-sidebar #lava-server-user-box {
+ text-align: right;
+ text-shadow: none;
+}
+
+
/* Navigation (part of header) */
#lava-server-navigation ul,
@@ -126,20 +143,74 @@
content: "";
}
+/* Sidebar */
+
+#lava-server-sidebar {
+}
+
+#lava-server-sidebar.classic {
+ background-color: white;
+ border-radius: 3px;
+ border: 1px solid #999;
+ color: #444;
+ font-family: Ubuntu, Helvetica Neue, Verdana, sans-serif;
+ font-size: 10pt;
+ margin: 1ex;
+ padding: 1ex;
+}
+
+#lava-server-sidebar .lava-sidebar-controls {
+ margin-top: 1em;
+}
+
+#lava-server-sidebar.lava-sidebar {
+ color: black;
+ height: 100%;
+ padding: 2pt 2pt 5pt 5pt;
+ border: none;
+ border-radius: 0;
+ z-index: 1000; /* otherwise data-tables seem to override this */
+ border-left: 1px solid gray;
+ box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.2);
+ background: white;
+ top: 0;
+}
+
+/* Hidden sidebar should be opaque. Removing the border and keeping the shadow
+ * adds a nice dramatic effect. */
+#lava-server-sidebar.lava-sidebar-hidden {
+ opacity: 0.5;
+}
+
+/* Hidden sidebar that users hover should be highlighted nicely */
+#lava-server-sidebar.lava-sidebar-hidden:hover {
+ opacity: 1;
+ background: rgba(0, 136, 204, 0.5);
+ text-shadow: 1px 1px 1px #000;
+ color: white !important;
+}
+
+#lava-server-sidebar.lava-sidebar-hidden:hover h1,
+#lava-server-sidebar.lava-sidebar-hidden:hover h2,
+#lava-server-sidebar.lava-sidebar-hidden:hover h3,
+#lava-server-sidebar.lava-sidebar-hidden:hover h4,
+#lava-server-sidebar.lava-sidebar-hidden:hover a {
+ color: inherit !important;
+ border-color: inherit !important;
+}
+
+
/* Content */
#lava-server-content {
color: #444;
- padding: 1ex 3em;
+ padding: 1ex;
border: 1px solid #999;
- Border-radius: 3px;
+ border-radius: 3px;
margin: 1ex;
- min-width: 800px;
background-color: white;
}
-#lava-server-sidebar {
-}
h1 {
color: #222;
@@ -239,3 +310,42 @@
position: relative;
top: 3px;
}
+
+/* Form styles */
+table.form_helper {
+ border-collapse: collapse;
+}
+
+table.form_helper th {
+ vertical-align: top;
+ text-align: right;
+ padding: 1ex;
+}
+
+table.form_helper td {
+ padding: 1ex;
+}
+
+table.form_helper input[type=text] {
+ width: 100%;
+}
+
+table.form_helper p.help_text {
+ margin: 0.2ex 1ex;
+ color: #888;
+}
+
+table.form_helper tr {
+ border-spacing: 0;
+ padding-top: 1ex;
+}
+
+table.form_helper div.ui-state-error {
+ padding: 1ex;
+}
+
+table.form_helper ul.errorlist {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
=== added file 'lava_server/htdocs/css/jquery.lava.sidebar.css'
@@ -0,0 +1,30 @@
+.lava-sidebar {
+ position: fixed;
+ right: 0;
+ width: 30em;
+}
+
+.lava-sidebar-hidden {
+ right: -27em;
+}
+
+.lava-sidebar-hidden * {
+ visibility: hidden;
+}
+
+.lava-sidebar-hidden:hover {
+ cursor: pointer;
+}
+
+.lava-sidebar-controls {
+ text-align: right;
+ border-bottom: 1px dotted gray;
+}
+
+.lava-sidebar-controls .ui-button {
+ width: 32px;
+ height: 32px;
+ margin-left: 3px;
+}
+
+
=== added file 'lava_server/htdocs/js/jquery.cookie.js'
@@ -0,0 +1,41 @@
+/**
+ * jQuery Cookie plugin
+ *
+ * Copyright (c) 2010 Klaus Hartl (stilbuero.de)
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ */
+jQuery.cookie = function (key, value, options) {
+
+ // key and at least value given, set cookie...
+ if (arguments.length > 1 && String(value) !== "[object Object]") {
+ options = jQuery.extend({}, options);
+
+ if (value === null || value === undefined) {
+ options.expires = -1;
+ }
+
+ if (typeof options.expires === 'number') {
+ var days = options.expires, t = options.expires = new Date();
+ t.setDate(t.getDate() + days);
+ }
+
+ value = String(value);
+
+ return (document.cookie = [
+ encodeURIComponent(key), '=',
+ options.raw ? value : encodeURIComponent(value),
+ options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+ options.path ? '; path=' + options.path : '',
+ options.domain ? '; domain=' + options.domain : '',
+ options.secure ? '; secure' : ''
+ ].join(''));
+ }
+
+ // key and possibly options given, get cookie...
+ options = value || {};
+ var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
+ return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
+};
=== added file 'lava_server/htdocs/js/jquery.lava.sidebar.js'
@@ -0,0 +1,174 @@
+/**
+* LAVA Sidebar plugin for jQuery
+* http://launchpad.net/lava-server/
+* Copyright (c): 2011 Zygmunt Krynicki
+* Licensed under AGPLv3.
+**/
+(function($) {
+ var baseClass = "lava-sidebar";
+ var hiddenClass = "lava-sidebar-hidden";
+
+ $.widget("lava.sidebar", {
+ options: {
+ // hidden by default (unless pinned)
+ hidden: true,
+ // not pinned by default
+ pinned: false,
+ pinnedCookieName: "lava-sidebar-pinned",
+ pinnedCookiePath: "/",
+ extraControls: [],
+ onShow: null,
+ onHide: null,
+ onDestroy: null,
+ onPin: null,
+ onUnpin: null,
+ },
+ _pin_button: null,
+ _close_button: null,
+ _controls: null,
+ controls: function() {
+ /* Container of sidebar controls */
+ return this._controls;
+ },
+ _create: function() {
+ /* Constructor */
+ var self = this;
+ if (this.element.html().trim() == "") {
+ // If the sidebar was empty then just hide it
+ this.element.hide();
+ } else {
+ // Load cookie to check for pin status
+ this._load_pin_option();
+ // Initialize basic stuff:
+ // 1) css class
+ // 2) click handler
+ this.element
+ .addClass(baseClass)
+ .bind("click.sidebar", function(event) {
+ if (self.element.hasClass(hiddenClass)) {
+ self.show();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ });
+ // Initialize controls bar
+ self._controls = $("<div/>", {
+ "class": "lava-sidebar-controls"
+ })
+ .prependTo(this.element);
+ // Initialize pin button
+ self._pin_button = $("<span/>", {
+ title: "Pin/unpin the sidebar"
+ })
+ .button({
+ text: false,
+ icons: { primary: this.options.pinned ? 'ui-icon-pin-s' : 'ui-icon-pin-w'}
+ })
+ .click(function(event) {
+ self.element.sidebar("option", "pinned", !self.options.pinned);
+ })
+ .appendTo(self._controls);
+ // Initialize close button
+ self._close_button = $("<span/>", {
+ title: "Collapse the sidebar"
+ })
+ .button({
+ text: false,
+ icons: { primary: 'ui-icon-circle-triangle-e'}
+ })
+ .bind("click", function(event) {
+ self.hide();
+ event.preventDefault();
+ event.stopPropagation();
+ })
+ .appendTo(self._controls);
+ $.each(self.options.extraControls, function(index, control) {
+ control.appendTo(self._controls);
+ });
+ }
+ },
+ _init: function() {
+ /* Initializer */
+ if (this.options.pinned == false && this.options.hidden == true) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ },
+ destroy: function() {
+ /* Destructor */
+ this.element.removeClass(baseClass);
+ this.element.removeClass(hiddenClass);
+ this.element.unbind("click.sidebar");
+ this._controls.remove();
+ $.Widget.prototype.destroy.call(this);
+ if (this.options.onDestroy) {
+ this.options.onDestroy();
+ }
+ },
+ show: function() {
+ /* Show the sidebar */
+ this.element.removeClass(hiddenClass);
+ this.options.hidden = false;
+ if (this.options.onShow) {
+ this.options.onShow();
+ }
+ },
+ hide: function() {
+ /* Hide the sidebar */
+ this.element.addClass(hiddenClass);
+ this.options.hidden = true;
+ if (this.options.onHide) {
+ this.options.onHide();
+ }
+ },
+ pin: function() {
+ /* Pin the sidebar so that it is shown by default */
+ this._set_pinned(true);
+ },
+ unpin: function() {
+ /* Unpin the sidebar */
+ this._set_pinned(false);
+ },
+ _set_pinned: function(pin_state) {
+ this.options.pinned = (pin_state == true);
+ this._pin_button.button("option", "icons", { primary: this.options.pinned ? 'ui-icon-pin-s' : 'ui-icon-pin-w'});
+ this._save_pin_option();
+ if (this.options.pinned == true && this.options.onPin) {
+ this.options.onPin();
+ }
+ if (this.options.pinned == false && this.options.onUnpin) {
+ this.options.onUnpin();
+ }
+ },
+ _load_pin_option: function() {
+ this.options.pinned = $.cookie(
+ this.options.pinnedCookieName, {
+ path: this.optionsPinnedCookiePath,
+ expires: 365
+ }) == "pinned";
+ },
+ _save_pin_option: function() {
+ $.cookie(
+ this.options.pinnedCookieName,
+ this.options.pinned ? "pinned" : "", {
+ path: this.options.pinnedCookiePath,
+ expires: 365
+ });
+ },
+ _setOption: function(key, value) {
+ /* jQueryUI widget method for implementing option setter */
+ $.Widget.prototype._setOption.apply(this, arguments);
+ if (key == "hidden") {
+ if (value) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+ if (key == "pinned") {
+ this._set_pinned(value);
+ }
+ }
+ });
+})(jQuery);
=== added file 'lava_server/htdocs/js/jquery.slugify.js'
@@ -0,0 +1,58 @@
+/*
+ * Taken from git://github.com/pmcelhaney/jQuery-Slugify-Plugin.git
+ */
+(function ($) {
+
+ $.fn.slugify = function (source, options) {
+ var $target = this;
+ var $source = $(source);
+
+ var settings = $.extend({
+ slugFunc: (function (val, originalFunc) { return originalFunc(val); })
+ }, options);
+
+
+ var convertToSlug = function(val) {
+ return settings.slugFunc(val,
+ (function(v) {
+ if (!v) return '';
+ var from = "ıİöÖüÜçÇğĞşŞâÂêÊîÎôÔûÛĘęÓ󥹌śŁłŻżŹźĆćŃń";
+ var to = "iIoOuUcCgGsSaAeEiIoOuUEeOoAaSsLlZzZzCcNn";
+
+ for (var i=0, l=from.length ; i<l ; i++) {
+ v = v.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
+ }
+
+ return v.replace(/'/g, '').replace(/\s*&\s*/g, ' and ').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
+ })
+ );
+ }
+
+ var setLock = function () {
+ if($target.val() != null && $target.val() != '') {
+ $target.addClass('slugify-locked');
+ } else {
+ $target.removeClass('slugify-locked');
+ }
+ }
+
+ var updateSlug = function () {
+ var slug = convertToSlug($(this).val());
+ $target.filter(':not(.slugify-locked)').val(slug).text(slug);
+ }
+
+
+ $source.keyup( updateSlug ).change( updateSlug );
+
+ $target.change(function () {
+ var slug = convertToSlug($(this).val());
+ $target.val(slug).text(slug);
+ setLock();
+ });
+
+ setLock();
+
+ return this;
+ };
+
+})(jQuery);
=== modified file 'lava_server/manage.py'
@@ -108,3 +108,16 @@
def main():
find_sources()
run_with_dispatcher_class(LAVAServerDispatcher)
+
+
+def legacy_main():
+ find_sources()
+ settings_module = "lava_server.settings.development"
+ settings = __import__(settings_module, fromlist=[''])
+ from django.core.management import execute_manager
+ execute_manager(settings)
+
+
+if __name__ == "__main__":
+ legacy_main()
+
=== modified file 'lava_server/settings/common.py'
@@ -91,6 +91,7 @@
# 'django.contrib.admindocs',
# Admin docs disabled due to: https://code.djangoproject.com/ticket/6681
'linaro_django_xmlrpc',
+ 'lava_markitup', # Support app for MarkItUp in LAVA
'south',
]
=== modified file 'lava_server/settings/development.py'
@@ -28,20 +28,8 @@
# uses virtualenv and look for a variable virtualenv injects into the
# environment.
LOCALENV_DIR = os.getenv("VIRTUAL_ENV")
-if LOCALENV_DIR is None:
- raise ImproperlyConfigured("Development mode REQUIRES VIRTUAL_ENV to be set")
-
-# Top-level directory for nonvolatile files, as used by lava-dev-tool. It is a
-# sibling directory to localenv so it's easier to define its location as
-# relative to LOCALENV_DIR.
-PRECIOUS_DIR = os.path.join(LOCALENV_DIR, "../precious")
-
-# Top-level directory of the precious project state.
-#
-# In short: this is where your non-source content ends up at, this place should
-# keep the database file(s), user uploaded media files as well as the cache of
-# static files, if built.
-PROJECT_STATE_DIR = os.path.join(PRECIOUS_DIR, "var/lib/lava-server/")
+if not LOCALENV_DIR:
+ print "Not using lava-dev-tool? Well okay, but just be careful with your imports and python path"
# Top-level directory of the project.
#
@@ -54,6 +42,29 @@
os.path.abspath(__file__)),
".."))
+# Top-level directory for nonvolatile files, as used by lava-dev-tool. It is a
+# sibling directory to localenv so it's easier to define its location as
+# relative to LOCALENV_DIR.
+if LOCALENV_DIR:
+ PRECIOUS_DIR = os.path.join(LOCALENV_DIR, "../precious")
+else:
+ PRECIOUS_DIR = os.path.join(PROJECT_SRC_DIR, "precious")
+
+# Create precious directory if needed
+if not os.path.exists(PRECIOUS_DIR):
+ os.makedirs(PRECIOUS_DIR)
+
+# Top-level directory of the precious project state.
+#
+# In short: this is where your non-source content ends up at, this place should
+# keep the database file(s), user uploaded media files as well as the cache of
+# static files, if built.
+PROJECT_STATE_DIR = os.path.join(PRECIOUS_DIR, "var/lib/lava-server/")
+
+# Create state directory if needed
+if not os.path.exists(PROJECT_STATE_DIR):
+ os.makedirs(PROJECT_STATE_DIR)
+
DEBUG = True
DEBUG_PROPAGATE_EXCEPTIONS = True
TEMPLATE_DEBUG = DEBUG
=== modified file 'lava_server/templates/index.html'
@@ -1,4 +1,4 @@
-{% extends "layouts/content.html" %}
+{% extends "layouts/content_with_sidebar.html" %}
{% load i18n %}
=== modified file 'lava_server/templates/layouts/base.html'
@@ -6,8 +6,11 @@
{% block scripts_and_styles %}
<script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery-ui-1.8.12.custom.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery.cookie.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery.lava.sidebar.js"></script>
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava-server/css/Aristo/jquery-ui-1.8.7.custom.css"/>
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava-server/css/default.css"/>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava-server/css/jquery.lava.sidebar.css"/>
{% endblock %}
{% block extrahead %}{% endblock %}
</head>
=== modified file 'lava_server/templates/layouts/content_with_sidebar.html'
@@ -3,15 +3,113 @@
{% block content-and-sidebar %}
<div id="lava-server-content-and-sidebar">
+ <!-- sidebar -->
+ <div id="lava-server-sidebar" class="classic">
+ {% block sidebar %}{% endblock %}
+ </div>
<!-- content -->
<div id="lava-server-content">
- <!-- sidebar -->
- <div id="lava-server-sidebar">
- {% block sidebar %}{% endblock %}
- </div>
<!-- !sidebar -->
{% block content %}{% endblock %}
</div>
<!-- !content -->
</div>
+<script type="text/javascript">
+ $(window).load(function() {
+ var preferDockedCookieName = "lava-server-prefer-docked";
+ var sidebar = $("#lava-server-sidebar");
+ var dock = $("<span/>")
+ .button({
+ label: "Dock the sidebar",
+ icons: { secondary: "ui-icon-circle-triangle-e" }
+ })
+ .css("float", "right")
+ .prependTo(sidebar)
+ .click(function() {
+ dock_the_sidebar();
+ });
+
+ function dock_the_sidebar() {
+ var user_box = $("#lava-server-user-box");
+ var content = $("#lava-server-content");
+ var header = $("#lava-server-header");
+ // Compute how much margin should remain once the sidebar is toggled
+ var base_margin = (
+ $("#lava-server-content").outerWidth(true)
+ - $("#lava-server-content").outerWidth(false)) / 2;
+ // Remove the 'try new sidebar' button
+ dock.hide();
+
+ // As for the sidebar
+ sidebar
+ // Remove the 'classic' class
+ .removeClass("classic")
+ // Enable the new looks
+ .sidebar({
+ onShow: function() {
+ user_box.prependTo(sidebar);
+ content.css("marginRight", base_margin + sidebar.outerWidth(true) + "px");
+ },
+ onHide: function() {
+ user_box.prependTo(header);
+ content.css("marginRight", base_margin + "px");
+ },
+ onDestroy: function() {
+ dock.show();
+ user_box.prependTo(header);
+ content.css("marginRight", base_margin + "px");
+ sidebar.addClass("classic")
+ },
+ onPin: function() {
+ var heart = $("#heart");
+ if (!heart.attr("checked")) {
+ heart.attr("checked", "checked");
+ heart.change(); // jQuery does not seem to fire this manually.
+ heart.parent("div").effect("highlight", {}, 3000);
+ }
+ },
+ extraControls: [
+ // Undock button
+ $("<span/>", {
+ title: "Undock the sidebar"
+ })
+ .css("float", "left")
+ .button({
+ label: "Undock",
+ text: false,
+ icons: { primary: 'ui-icon-circle-triangle-w' }
+ })
+ .click(function () { sidebar.sidebar("destroy"); }),
+ // Preferences
+ $('<div/>')
+ .css({
+ textAlign: "left",
+ marginTop: "1ex"
+ })
+ .append(
+ $('<input/>', {
+ type: "checkbox",
+ checked: $.cookie(preferDockedCookieName, {path: '/'}) == 'true',
+ id: "heart",
+ title: "Dock the sidebar automatically"
+ })
+ .change(function() {
+ $.cookie(preferDockedCookieName, $(this).attr("checked") ? true : '', {path: '/'})
+ })
+ )
+ .append(
+ $('<label/>', { "for": "heart" })
+ .append($("<span/>")
+ .text("Dock the sidebar automatically")
+ )
+ ),
+ ]
+ });
+ }
+
+ if ($.cookie(preferDockedCookieName) == "true") {
+ dock_the_sidebar();
+ }
+ });
+</script>
{% endblock %}
=== modified file 'lava_server/templates/linaro_django_xmlrpc/_base.html'
@@ -1,4 +1,4 @@
-{% extends "layouts/content.html" %}
+{% extends "layouts/content_with_sidebar.html" %}
{% load i18n %}
=== modified file 'lava_server/urls.py'
@@ -52,6 +52,7 @@
url(r'^' + settings.APP_URL_PREFIX + r'api/', include(api_urls.token_urlpatterns)),
# XXX: This is not needed but without it linaro-django-xmlrpc tests fail
url(r'^' + settings.APP_URL_PREFIX + r'api/', include(api_urls.default_mapper_urlpatterns)),
+ url(r'^' + settings.APP_URL_PREFIX + r'utils/markitup/', include('lava_markitup.urls')),
)
=== modified file 'setup.py'
@@ -32,6 +32,8 @@
lava-server = lava_server.manage:main
[lava_server.commands]
manage=lava_server.manage:manage
+ [lava_server.extensions]
+ project=lava_projects.extension:ProjectExtension
""",
test_suite="lava_server.tests.run_tests",
license="AGPL",
@@ -44,12 +46,13 @@
""",
url='https://launchpad.net/lava-server',
classifiers=[
- "Development Status :: 3 - Alpha",
+ "Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Affero General Public License v3",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
"Topic :: Software Development :: Testing",
],
install_requires=[
@@ -61,6 +64,7 @@
'python-openid >= 2.2.4', # this should be a part of django-openid-auth deps
'south >= 0.7.3',
'versiontools >= 1.3.1',
+ 'markdown >= 2.0.3',
],
setup_requires=[
'versiontools >= 1.3.1',