[Branch,~linaro-validation/lava-scheduler/trunk] Rev 248: Add GUI for job submissions. Reviewed by stylesen.

Message ID 20130628094715.27278.76815.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Stevan Radakovic June 28, 2013, 9:47 a.m.
Merge authors:
  Stevan Radaković (stevanr)
Related merge proposals:
  https://code.launchpad.net/~stevanr/lava-scheduler/gui-job-submission/+merge/171571
  proposed by: Stevan Radaković (stevanr)
  review: Approve - Senthil Kumaran S (stylesen)
------------------------------------------------------------
revno: 248 [merge]
committer: Stevan Radakovic <stevan.radakovic@linaro.org>
branch nick: trunk
timestamp: Fri 2013-06-28 11:46:21 +0200
message:
  Add GUI for job submissions. Reviewed by stylesen.
added:
  lava_scheduler_app/static/lava_scheduler_app/css/jquery-linedtextarea.css
  lava_scheduler_app/static/lava_scheduler_app/js/job-submit.js
  lava_scheduler_app/static/lava_scheduler_app/js/jquery-linedtextarea.js
  lava_scheduler_app/templates/lava_scheduler_app/job_submit.html
modified:
  lava_scheduler_app/extension.py
  lava_scheduler_app/models.py
  lava_scheduler_app/static/lava_scheduler_app/css/scheduler.css
  lava_scheduler_app/urls.py
  lava_scheduler_app/views.py


--
lp:lava-scheduler
https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk

You are subscribed to branch lp:lava-scheduler.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk/+edit-subscription

Patch

=== modified file 'lava_scheduler_app/extension.py'
--- lava_scheduler_app/extension.py	2012-06-16 03:04:57 +0000
+++ lava_scheduler_app/extension.py	2013-06-24 13:26:57 +0000
@@ -49,6 +49,7 @@ 
             Menu("Status", reverse("lava.scheduler")),
             Menu("Jobs", reverse("lava.scheduler.job.list")),
             Menu("Reports", reverse("lava.scheduler.reports")),
+            Menu("Submit Job", reverse("lava.scheduler.job.submit")),
         ]
         return menu
 

=== modified file 'lava_scheduler_app/models.py'
--- lava_scheduler_app/models.py	2013-02-10 21:26:06 +0000
+++ lava_scheduler_app/models.py	2013-06-26 12:57:41 +0000
@@ -41,7 +41,7 @@ 
         ob = simplejson.loads(data)
         validate_job_data(ob)
     except ValueError, e:
-        raise ValidationError(str(e))
+        raise ValidationError(e)
 
 
 class DeviceType(models.Model):

=== added file 'lava_scheduler_app/static/lava_scheduler_app/css/jquery-linedtextarea.css'
--- lava_scheduler_app/static/lava_scheduler_app/css/jquery-linedtextarea.css	1970-01-01 00:00:00 +0000
+++ lava_scheduler_app/static/lava_scheduler_app/css/jquery-linedtextarea.css	2013-06-26 12:57:41 +0000
@@ -0,0 +1,74 @@ 
+/**
+ * jQuery Lined Textarea Plugin
+ *   http://alan.blog-city.com/jquerylinedtextarea.htm
+ *
+ * Copyright (c) 2010 Alan Williamson
+ *
+ * Contribution done by Ryan Zielke (neoalchemy@gmail.com)
+ *
+ * Released under the MIT License:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Usage:
+ *   Displays a line number count column to the left of the textarea
+ *
+ *   Class up your textarea with a given class, or target it directly
+ *   with JQuery Selectors
+ *
+ *   $(".lined").linedtextarea({
+ *   	selectedLine: 10,
+ *    selectedClass: 'lineselect'
+ *   });
+ *
+ */
+
+textarea { resize:both; }
+
+.linedwrap {
+	border: 1px solid #c0c0c0;
+	padding: 3px;
+	display: inline-block;
+}
+
+.linedtextarea {
+	padding: 0px;
+	margin: 0px;
+}
+
+.linedtextarea textarea, .linedwrap .codelines .lineno {
+	font-size: 9pt;
+	font-family: monospace;
+	line-height: normal !important;
+}
+
+.linedtextarea textarea {
+	padding-right:0.3em;
+	padding-top:0.3em;
+	border: 0;
+}
+
+.linedwrap .lines {
+	margin-top: 0px;
+	width: 50px;
+	float: left;
+	overflow: hidden;
+	border-right: 1px solid #c0c0c0;
+	margin-right: 10px;
+}
+
+.linedwrap .codelines {
+	padding-top: 3px;
+}
+
+.linedwrap .codelines .lineno {
+	color:#AAAAAA;
+	padding-right: 0.5em;
+	padding-top: 0.0em;
+	text-align: right;
+	white-space: nowrap;
+}
+
+.linedwrap .codelines .lineselect {
+	color: white;
+        background-color: red;
+}
\ No newline at end of file

=== modified file 'lava_scheduler_app/static/lava_scheduler_app/css/scheduler.css'
--- lava_scheduler_app/static/lava_scheduler_app/css/scheduler.css	2012-03-15 09:27:48 +0000
+++ lava_scheduler_app/static/lava_scheduler_app/css/scheduler.css	2013-06-28 09:45:22 +0000
@@ -44,4 +44,34 @@ 
 
 .logbuttons .ui-button-text {
     padding: 0.1em 0.4em;
-}
\ No newline at end of file
+}
+
+#json-input {
+    width: 900px;
+    height: 400px;
+    margin: 0px;
+    resize: both;
+}
+
+#submit-container {
+    margin-top: 10px;
+}
+
+#json-valid-container {
+    border: 1px solid #cccccc;
+    display: none;
+    margin-top: 10px;
+    width: 500px;
+    padding: 10px;
+}
+
+#job-success {
+    font-size: 16px;
+    font-weight: bold;
+}
+
+#job-error {
+    font-size: 14px;
+    font-weight: bold;
+    color: red;
+}

=== added file 'lava_scheduler_app/static/lava_scheduler_app/js/job-submit.js'
--- lava_scheduler_app/static/lava_scheduler_app/js/job-submit.js	1970-01-01 00:00:00 +0000
+++ lava_scheduler_app/static/lava_scheduler_app/js/job-submit.js	2013-06-27 11:51:21 +0000
@@ -0,0 +1,107 @@ 
+$(window).ready(
+    function () {
+        $("#json-input").linedtextarea();
+
+        $("#json-input").bind('paste', function() {
+            // Need a timeout since paste event does not give the content
+            // of the clipboard.
+            setTimeout(function(){
+                validate_input($("#json-input").val());
+            },100);
+        });
+
+        $("#json-input").blur(function() {
+            validate_input($("#json-input").val());
+        });
+
+        $("#submit").attr("disabled", "disabled");
+    });
+
+validate_input = function(json_input) {
+
+    if ($("#json-input").val().split("\n").length == 1) {
+        load_url();
+    } else {
+        validate_job_data(json_input);
+    }
+}
+
+load_url = function() {
+    // Loads JSON content if URL is provided in the json text area.
+    if ($("#json-input").val().split("\n").length == 1) {
+        $.ajax({
+            type: "POST",
+            url: remote_json_url,
+            data: {
+                "url": $("#json-input").val().trim(),
+                "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()
+            },
+            success: function(data) {
+                try {
+                    $.parseJSON(data);
+                    $("#json-input").val(data);
+                    validate_job_data(data);
+                } catch (e) {
+                    $("#json-valid-container").html("Invalid JSON: " + data);
+                    valid_json_css(false);
+                    $("#submit").attr("disabled", "disabled");
+                }
+            }});
+    }
+}
+
+validate_job_data = function(data) {
+    $.post(window.location.pathname,
+           {"json-input": data,
+            "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()},
+           function(data) {
+               if (data == "success") {
+                   $("#json-valid-container").html("Valid JSON.");
+                   valid_json_css(true);
+                   $("#submit").removeAttr("disabled");
+                   unselect_error_line();
+               } else {
+                   $("#json-valid-container").html(
+                       data.replace("[u'", "").replace("']", "").
+                           replace('[u"', "").replace('"]', ""));
+                   valid_json_css(false);
+                   $("#submit").attr("disabled", "disabled");
+                   select_error_line(data);
+               }
+           }, "json");
+}
+
+
+valid_json_css = function(success) {
+    // Updates the css of the json validation container with appropriate msg.
+    if (success) {
+        $("#json-valid-container").css("backgound-color", "#50ef53");
+        $("#json-valid-container").css("color", "#139a16");
+        $("#json-valid-container").css("border-color", "#139a16");
+        $("#json-valid-container").show();
+    } else {
+        $("#json-valid-container").css("backgound-color", "#ff8383");
+        $("#json-valid-container").css("color", "#da110a");
+        $("#json-valid-container").css("border-color", "#da110a");
+        $("#json-valid-container").show();
+    }
+}
+
+unselect_error_line = function() {
+    // Unselect any potential previously selected lines.
+    $(".lineno").removeClass("lineselect");
+}
+
+select_error_line = function(error) {
+    // Selects the appropriate line in text area based on the parsed error msg.
+    line_string = error.split(": ")[1];
+    line_number = parseInt(line_string.split(" ")[1]);
+
+    $(".lineno").removeClass("lineselect");
+    $("#lineno"+line_number).addClass("lineselect");
+
+    // Scroll the textarea to the highlighted line.
+    $("#json-input").scrollTop(
+        line_number * (parseInt($("#lineno1").css(
+            "height")) - 1) - ($("#json-input").height() / 2));
+}

=== added file 'lava_scheduler_app/static/lava_scheduler_app/js/jquery-linedtextarea.js'
--- lava_scheduler_app/static/lava_scheduler_app/js/jquery-linedtextarea.js	1970-01-01 00:00:00 +0000
+++ lava_scheduler_app/static/lava_scheduler_app/js/jquery-linedtextarea.js	2013-06-26 11:39:05 +0000
@@ -0,0 +1,119 @@ 
+/**
+ * jQuery Lined Textarea Plugin
+ *   http://alan.blog-city.com/jquerylinedtextarea.htm
+ *
+ * Copyright (c) 2010 Alan Williamson
+ *
+ * Contributions done by Ryan Zielke (NeoAlchemy@gmail.com)
+ *
+ * Version:
+ *    $Id: jquery-linedtextarea.js 464 2010-01-08 10:36:33Z alan $
+ *
+ * Released under the MIT License:
+ *    http://www.opensource.org/licenses/mit-license.php
+ *
+ * Usage:
+ *   Displays a line number count column to the left of the textarea
+ *
+ *   Class up your textarea with a given class, or target it directly
+ *   with JQuery Selectors
+ *
+ *   $(".lined").linedtextarea({
+ *   	selectedLine: 10,
+ *    selectedClass: 'lineselect'
+ *   });
+ */
+
+(function($) {
+    $.fn.linedtextarea = function(options) {
+	// Get the Options
+	var opts = $.extend({}, $.fn.linedtextarea.defaults, options);
+
+	/*
+	 * Helper function to make sure the line numbers are always
+	 * kept up to the current system
+	 */
+	var fillOutLines = function(codeLines, h, lineNo){
+	    while ( (codeLines.height() - h ) <= 0 ){
+		if ( lineNo == opts.selectedLine )
+		    codeLines.append("<div id='lineno" + lineNo + "' class='lineno lineselect'>" + lineNo + "</div>");
+		else
+		    codeLines.append("<div id='lineno" + lineNo + "' class='lineno'>" + lineNo + "</div>");
+
+		lineNo++;
+	    }
+	    return lineNo;
+	};
+
+	/*
+	 * Iterate through each of the elements are to be applied to
+	 */
+	return this.each(function() {
+	    var lineNo = 1;
+	    var textarea = $(this);
+
+	    /* Turn off the wrapping of as we don't want to screw up the line numbers */
+	    textarea.attr("wrap", "off");
+	    textarea.css({resize:'both'});
+	    var originalTextAreaWidth = textarea.outerWidth();
+
+	    /* Wrap the text area in the elements we need */
+	    var linedTextAreaDiv = textarea.wrap("<div class='linedwrap'></div>");
+	    var linedWrapDiv = linedTextAreaDiv.parent();
+
+	    linedWrapDiv.prepend("<div class='lines' style='width:50px'></div>");
+
+	    var linesDiv = linedWrapDiv.find(".lines");
+
+	    /* Draw the number bar; filling it out where necessary */
+	    linesDiv.append("<div class='codelines'></div>");
+	    var codeLinesDiv = linesDiv.find(".codelines");
+	    lineNo = fillOutLines( codeLinesDiv, linesDiv.height(), 1 );
+
+	    /* Move the textarea to the selected line */
+	    if ( opts.selectedLine != -1 && !isNaN(opts.selectedLine) ){
+		var fontSize = parseInt( textarea.height() / (lineNo-2) );
+		var position = parseInt( fontSize * opts.selectedLine ) - (textarea.height()/2);
+		textarea[0].scrollTop = position;
+	    }
+
+	    /* Set the width */
+	    var sidebarWidth = linesDiv.outerWidth(true);
+	    var paddingHorizontal = parseInt( linedWrapDiv.css("border-left-width") ) + parseInt( linedWrapDiv.css("border-right-width") ) + parseInt( linedWrapDiv.css("padding-left") ) + parseInt( linedWrapDiv.css("padding-right") );
+	    var linedWrapDivNewWidth = originalTextAreaWidth - paddingHorizontal;
+	    var textareaNewWidth = originalTextAreaWidth - sidebarWidth - paddingHorizontal;
+
+	    textarea.width(textareaNewWidth);
+	    textarea.css({maxWidth: textareaNewWidth - 6}); //TODO make this calculated
+
+	    /* React to the scroll event */
+	    textarea.scroll( function(tn){
+		var domTextArea = $(this)[0];
+		var scrollTop = domTextArea.scrollTop;
+		var clientHeight = domTextArea.clientHeight;
+		codeLinesDiv.css({'margin-top': (-1*scrollTop) + "px"});
+		lineNo = fillOutLines(codeLinesDiv, scrollTop + clientHeight, lineNo);
+	    });
+
+	    /* Should the textarea get resized outside of our control */
+	    textarea.resize( function(tn){
+		var domTextArea	= $(this)[0];
+		linesDiv.height(domTextArea.clientHeight + 6);
+	    });
+
+	    window.setInterval( function(tn) {
+		linesDiv.height(textarea.height());
+		var scrollTop = textarea[0].scrollTop;
+		var clientHeight = textarea[0].clientHeight;
+		codeLinesDiv.css({'margin-top': (-1*scrollTop) + "px"});
+		lineNo = fillOutLines(codeLinesDiv, scrollTop + clientHeight, lineNo);
+	    },10);
+	});
+    };
+
+    // default options
+    $.fn.linedtextarea.defaults = {
+  	selectedLine: -1,
+  	selectedClass: 'lineselect'
+    };
+})(jQuery);

=== added file 'lava_scheduler_app/templates/lava_scheduler_app/job_submit.html'
--- lava_scheduler_app/templates/lava_scheduler_app/job_submit.html	1970-01-01 00:00:00 +0000
+++ lava_scheduler_app/templates/lava_scheduler_app/job_submit.html	2013-06-28 09:43:44 +0000
@@ -0,0 +1,67 @@ 
+{% extends "lava_scheduler_app/_content.html" %}
+
+{% block extrahead %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava_scheduler_app/css/jquery-linedtextarea.css"/>
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava_scheduler_app/css/scheduler.css"/>
+<script type="text/javascript" src="{{ STATIC_URL }}lava_scheduler_app/js/jquery-linedtextarea.js"></script>
+{% url lava.scheduler.get_remote_json as remote_json_url %}
+<script type="text/javascript">
+  var remote_json_url = '{{ remote_json_url }}';
+</script>
+<script type="text/javascript" src="{{ STATIC_URL }}lava_scheduler_app/js/job-submit.js"></script>
+{% endblock %}
+
+
+{% block content %}
+<h2>Submit Job</h2>
+
+{% if is_authorized %}
+
+{% if job_id %}
+
+{% url lava.scheduler.job.detail job_id as detail_url %}
+{% url lava.scheduler.job.list as list_url %}
+
+<div id="job-success">Job submission successfull!
+<br>
+<br>
+Job with ID <a href="{{ detail_url }}">{{ job_id }}</a> has been created.
+<br>
+To view the full job list click <a href="{{ list_url }}">here</a>.
+</div>
+
+{% else %}
+
+{% if error %}
+<div id="job-error">
+  Job submission error: {{ error }}
+</div>
+{% endif %}
+
+<p>Paste your job definition JSON here. Alternatively, you can paste a URL to your job definition file:</p>
+
+<form action="" method="post">
+  {% csrf_token %}
+  <textarea id="json-input" name="json-input" placeholder="Enter your job definition or link to a job definition here.">
+{% if json_input %}{{ json_input }}{% endif %}
+  </textarea>
+  <div id="json-valid-container">
+  </div>
+  <div id="submit-container">
+    <input id="submit" type="submit" value="Submit">
+  </div>
+</form>
+
+{% endif %}
+
+{% else %}
+<h3>
+Error:
+</h3>
+<p>Permission denied. You not have the required permissions to submit new jobs.
+<br>
+Please contact the administrators.</p>
+{% endif %}
+
+{% endblock %}

=== modified file 'lava_scheduler_app/urls.py'
--- lava_scheduler_app/urls.py	2013-01-15 17:44:36 +0000
+++ lava_scheduler_app/urls.py	2013-06-27 11:01:30 +0000
@@ -24,6 +24,9 @@ 
     url(r'^alljobs$',
         'job_list',
         name='lava.scheduler.job.list'),
+    url(r'^jobsubmit$',
+        'job_submit',
+        name='lava.scheduler.job.submit'),
     url(r'^alljobs_json$',
         'alljobs_json',
         name='lava.scheduler.job.list_json'),
@@ -105,4 +108,7 @@ 
     url(r'^job/(?P<pk>[0-9]+)/full_log_incremental$',
         'job_full_log_incremental',
         name='lava.scheduler.job.full_log_incremental'),
+    url(r'^get-remote-json',
+        'get_remote_json',
+        name='lava.scheduler.get_remote_json'),
     )

=== modified file 'lava_scheduler_app/views.py'
--- lava_scheduler_app/views.py	2013-05-02 09:00:04 +0000
+++ lava_scheduler_app/views.py	2013-06-27 11:28:22 +0000
@@ -4,6 +4,7 @@ 
 import simplejson
 import StringIO
 import datetime
+import urllib2
 from dateutil.relativedelta import relativedelta
 
 from django import forms
@@ -51,7 +52,9 @@ 
     DeviceType,
     DeviceStateTransition,
     JobFailureTag,
+    JSONDataError,
     TestJob,
+    validate_job_json,
     )
 
 
@@ -555,6 +558,51 @@ 
         RequestContext(request))
 
 
+@BreadCrumb("Submit Job", parent=index)
+def job_submit(request):
+
+    is_authorized = False
+    if request.user and request.user.has_perm(
+            'lava_scheduler_app.add_testjob'):
+        is_authorized = True
+
+    response_data = {
+        'is_authorized': is_authorized,
+        'bread_crumb_trail': BreadCrumbTrail.leading_to(job_submit),
+        }
+
+    if request.method == "POST" and is_authorized:
+        if request.is_ajax():
+            try:
+                validate_job_json(request.POST.get("json-input"))
+                return HttpResponse(simplejson.dumps("success"))
+            except Exception as e:
+                return HttpResponse(simplejson.dumps(str(e)),
+                                    mimetype="application/json")
+
+        else:
+            try:
+                job = TestJob.from_json_and_user(
+                       request.POST.get("json-input"), request.user)
+
+                response_data["job_id"] = job.id
+                return render_to_response(
+                    "lava_scheduler_app/job_submit.html",
+                    response_data, RequestContext(request))
+
+            except Exception as e:
+                response_data["error"] = str(e)
+                response_data["json_input"] = request.POST.get("json-input")
+                return render_to_response(
+                    "lava_scheduler_app/job_submit.html",
+                    response_data, RequestContext(request))
+
+    else:
+        return render_to_response(
+            "lava_scheduler_app/job_submit.html",
+            response_data, RequestContext(request))
+
+
 @BreadCrumb("Job #{pk}", parent=index, needs=['pk'])
 def job_detail(request, pk):
     job = get_restricted_job(request.user, pk)
@@ -779,6 +827,23 @@ 
     return HttpResponse(json_text, content_type=content_type)
 
 
+@post_only
+def get_remote_json(request):
+    """Fetches remote json file."""
+    url = request.POST.get("url")
+
+    try:
+        data = urllib2.urlopen(url).read()
+        # Validate that the data at the location is really JSON.
+        # This is security based check so noone can misuse this url.
+        simplejson.loads(data)
+    except Exception as e:
+        return HttpResponse(simplejson.dumps(str(e)),
+                            mimetype="application/json")
+
+    return HttpResponse(data)
+
+
 class RecentJobsTable(JobTable):
 
     def get_queryset(self, device):