=== modified file 'lava_scheduler_app/extension.py'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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):