commit 4f51cd3107bc1f9e65be587aeb3b41f7a584e3d7
Author: Arturo Filastò <art(a)fuffa.org>
Date: Thu Apr 25 13:33:46 2013 +0200
Add support for viewing test results and uploading inputs
---
data/ui/app/index.html | 1 +
data/ui/app/libs/ng-upload/ng-upload.js | 107 +++++++++++++++++++++++++++++++
data/ui/app/scripts/app.js | 21 +++---
data/ui/app/scripts/controllers.js | 36 ++++++++---
data/ui/app/scripts/services.js | 9 ++-
data/ui/app/styles/app.css | 5 ++
data/ui/app/views/inputs.html | 31 +++++++++
data/ui/app/views/sidebar.html | 4 ++
data/ui/app/views/test-list.html | 38 -----------
data/ui/app/views/test-status.html | 7 --
data/ui/app/views/test.html | 42 ++++++++++++
ooni/api/spec.py | 6 +-
12 files changed, 239 insertions(+), 68 deletions(-)
diff --git a/data/ui/app/index.html b/data/ui/app/index.html
index a903ceb..c304066 100644
--- a/data/ui/app/index.html
+++ b/data/ui/app/index.html
@@ -28,6 +28,7 @@
<script src="libs/angular/angular.js"></script>
<script src="libs/angular-resource/angular-resource.js"></script>
+ <script src="libs/ng-upload/ng-upload.js"></script>
<script src="scripts/app.js"></script>
<script src="scripts/services.js"></script>
<script src="scripts/controllers.js"></script>
diff --git a/data/ui/app/libs/ng-upload/ng-upload.js b/data/ui/app/libs/ng-upload/ng-upload.js
new file mode 100644
index 0000000..fae7a44
--- /dev/null
+++ b/data/ui/app/libs/ng-upload/ng-upload.js
@@ -0,0 +1,107 @@
+// Version 0.3.2
+// AngularJS simple file upload directive
+// this directive uses an iframe as a target
+// to enable the uploading of files without
+// losing focus in the ng-app.
+//
+// <div ng-app="app">
+// <div ng-controller="mainCtrl">
+// <form action="/uploads" ng-upload>
+// <input type="file" name="avatar"></input>
+// <input type="submit" value="Upload"
+// upload-submit="submited(content, completed)"></input>
+// </form>
+// </div>
+// </div>
+//
+// angular.module('app', ['ngUpload'])
+// .controller('mainCtrl', function($scope) {
+// $scope.submited = function(content, completed) {
+// if (completed) {
+// console.log(content);
+// }
+// }
+// });
+//
+angular.module('ngUpload', [])
+ .directive('uploadSubmit', ['$parse', function($parse) {
+ return {
+ restrict: 'AC',
+ link: function(scope, element, attrs) {
+ // Options (just 1 for now)
+ // Each option should be prefixed with 'upload-options-' or 'uploadOptions'
+ // {
+ // // specify whether to enable the submit button when uploading forms
+ // enableControls: bool
+ // }
+ var options = {};
+ options.enableControls = attrs.uploadOptionsEnableControls;
+
+ // submit the form - requires jQuery
+ var form = element.parents('form[ng-upload]') || element.parents('form.ng-upload');
+
+ // Retrieve the callback function
+ var fn = $parse(attrs.uploadSubmit);
+
+ if (!angular.isFunction(fn)) {
+ var message = "The expression on the ngUpload directive does not point to a valid function.";
+ throw message + "\n";
+ }
+
+ element.bind('click', function($event) {
+ // prevent default behavior of click
+ $event.preventDefault = true;
+ // create a new iframe
+ var iframe = angular.element("<iframe id='upload_iframe' name='upload_iframe' border='0' width='0' height='0' style='width: 0px; height: 0px; border: none; display: none' />");
+
+ // attach function to load event of the iframe
+ iframe.bind('load', function () {
+ // get content - requires jQuery
+ var content = iframe.contents().find('body').text();
+ // execute the upload response function in the active scope
+ scope.$apply(function () {
+ fn(scope, { content: content, completed: true});
+ });
+ // remove iframe
+ if (content !== "") { // Fixes a bug in Google Chrome that dispose the iframe before content is ready.
+ setTimeout(function () { iframe.remove(); }, 250);
+ }
+ element.attr('disabled', null);
+ element.attr('title', 'Click to start upload.');
+ });
+
+ // add the new iframe to application
+ form.parent().append(iframe);
+
+ scope.$apply(function () {
+ fn(scope, {content: "Please wait...", completed: false });
+ });
+
+ var enabled = true;
+ if (!options.enableControls) {
+ // disable the submit control on click
+ element.attr('disabled', 'disabled');
+ enabled = false;
+ }
+ // why do we need this???
+ element.attr('title', (enabled ? '[ENABLED]: ' : '[DISABLED]: ') + 'Uploading, please wait...');
+
+ form.submit();
+
+ }).attr('title', 'Click to start upload.');
+ }
+ };
+ }])
+ .directive('ngUpload', ['$parse', function ($parse) {
+ return {
+ restrict: 'AC',
+ link: function (scope, element, attrs) {
+ element.attr("target", "upload_iframe");
+ element.attr("method", "post");
+ // Append a timestamp field to the url to prevent browser caching results
+ element.attr("action", element.attr("action") + "?_t=" + new Date().getTime());
+ element.attr("enctype", "multipart/form-data");
+ element.attr("encoding", "multipart/form-data");
+ }
+ };
+ }]);
\ No newline at end of file
diff --git a/data/ui/app/scripts/app.js b/data/ui/app/scripts/app.js
index 5fb8fdf..d36a17c 100644
--- a/data/ui/app/scripts/app.js
+++ b/data/ui/app/scripts/app.js
@@ -2,28 +2,29 @@
// Declare app level module which depends on filters, and services
-var ooniprobe = angular.module('ooniprobe', ['ooniprobe.services']).
+var ooniprobe = angular.module('ooniprobe', ['ngUpload', 'ooniprobe.services']).
config(['$routeProvider', function($routeProvider) {
- $routeProvider.when('/test-status',
+
+ $routeProvider.when('/inputs',
{
- templateUrl: 'views/test-status.html',
- controller: 'PageCtrl'
+ templateUrl: 'views/inputs.html',
+ controller: 'InputsCtrl'
}
);
- $routeProvider.when('/test-list',
+ $routeProvider.when('/settings',
{
- templateUrl: 'views/test-list.html',
- controller: 'TestListCtrl'
+ templateUrl: 'views/settings.html',
+ controller: 'SettingsCtrl'
}
);
$routeProvider.when('/test/:testID',
{
- templateUrl: 'views/test-list.html',
- controller: 'TestListCtrl'
+ templateUrl: 'views/test.html',
+ controller: 'TestCtrl'
}
);
- $routeProvider.otherwise({redirectTo: '/test-status'});
+ $routeProvider.otherwise({redirectTo: '/settings'});
}]);
diff --git a/data/ui/app/scripts/controllers.js b/data/ui/app/scripts/controllers.js
index 489dfd2..5ba24e1 100644
--- a/data/ui/app/scripts/controllers.js
+++ b/data/ui/app/scripts/controllers.js
@@ -3,16 +3,16 @@
ooniprobe.controller('PageCtrl', ['$scope', function($scope) {
}]);
-ooniprobe.controller('TestListCtrl', ['$scope', '$routeParams', 'testStatus',
- function($scope, $routeParams, testStatus) {
+ooniprobe.controller('SettingsCtrl', ['$scope',
+ function($scope) {
+}]);
- var testID = $routeParams['testID'];
- $scope.updateTestStatus = function() {
- testStatus(testID).success(function(testDetails){
- $scope.testDetails = testDetails;
- });
+ooniprobe.controller('InputsCtrl', ['$scope', 'Inputs',
+ function($scope, Inputs) {
+ $scope.inputs = Inputs.query();
+ $scope.uploadComplete = function(contents, completed) {
+ return;
}
- $scope.updateTestStatus();
}]);
@@ -30,13 +30,31 @@ ooniprobe.controller('SideBarCtrl', ['$scope', 'listTests', '$location',
}]);
+ooniprobe.controller('TestCtrl', ['$scope', '$routeParams', 'testStatus', 'Inputs',
+ function($scope, $routeParams, testStatus, Inputs) {
+
+ var testID = $routeParams['testID'];
+
+ $scope.inputs = Inputs.query();
+
+ $scope.updateTestStatus = function() {
+ testStatus(testID).success(function(testDetails){
+ $scope.testDetails = testDetails;
+ });
+ }
+ $scope.updateTestStatus();
+
+
+}]);
+
ooniprobe.controller('TestBoxCtrl', ['$scope', 'startTest',
function($scope, startTest) {
$scope.startTest = function() {
var options = {};
- angular.forEach($scope.testDetails.arguments, function(option, key){
+ angular.forEach($scope.testDetails.arguments,
+ function(option, key) {
options[key] = option.value;
});
diff --git a/data/ui/app/scripts/services.js b/data/ui/app/scripts/services.js
index 3d6721d..ea013ee 100644
--- a/data/ui/app/scripts/services.js
+++ b/data/ui/app/scripts/services.js
@@ -2,7 +2,7 @@
angular.module('ooniprobe.services', ['ngResource']).
factory('listTests', ['$resource',
- function($resource){
+ function($resource) {
return $resource('/test');
}]).
factory('testStatus', ['$http', function($http){
@@ -10,11 +10,16 @@ angular.module('ooniprobe.services', ['ngResource']).
return $http.get('/test/' + testID);
}
}]).
- factory('startTest', ['$http', function($http){
+ factory('startTest', ['$http',
+ function($http) {
return function(testID, options) {
return $http.post('/test/' + testID + '/start', options);
}
}]).
+ factory('Inputs', ['$resource',
+ function($resource) {
+ return $resource('/inputs');
+}]).
factory('status', ['$resource',
function($resource) {
return $resource('/status');
diff --git a/data/ui/app/styles/app.css b/data/ui/app/styles/app.css
index 5fe454f..b4a0fb8 100644
--- a/data/ui/app/styles/app.css
+++ b/data/ui/app/styles/app.css
@@ -14,3 +14,8 @@
background-color: rgb(240, 240, 240);
}
+.testResult {
+ height: 200px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
diff --git a/data/ui/app/views/inputs.html b/data/ui/app/views/inputs.html
new file mode 100644
index 0000000..767e00e
--- /dev/null
+++ b/data/ui/app/views/inputs.html
@@ -0,0 +1,31 @@
+<div class="row">
+ <div class="span8">
+ <h2>Inputs</h2>
+ <ul class="unstyled">
+ <li ng-repeat="input in inputs">{{input.filename}}
+ <!-- button class="btn btn-small btn-danger" ng-click="input.$delete()">delete</button -->
+ </li>
+ </ul>
+ <form ng-upload action="/inputs">
+
+ <h4>Add file</h4>
+ <label>file</label>
+ <input type="file" name="file" />
+ <br/>
+ <button class="btn"
+ upload-submit="uploadComplete(contents, completed)">Upload</button>
+
+ <!-- h4>Add filename</h4>
+ <label>filename</label>
+ <input type="text" ng-model="fileName" />
+
+ <label>content</label>
+ <textarea ng-model="fileContent"></textarea>
+ <br/>
+ <button class="btn">Add</button -->
+ </form>
+
+
+ </div>
+</div>
+
diff --git a/data/ui/app/views/settings.html b/data/ui/app/views/settings.html
new file mode 100644
index 0000000..e69de29
diff --git a/data/ui/app/views/sidebar.html b/data/ui/app/views/sidebar.html
index 172a51e..5ab4630 100644
--- a/data/ui/app/views/sidebar.html
+++ b/data/ui/app/views/sidebar.html
@@ -3,4 +3,8 @@
<li ng-repeat="test in test_list" ng-class="{'active': testSelected(test.id)}">
<a href="#/test/{{test.id}}">{{test.name}}</a>
</li>
+
+ <li class="nav-header">Configuration</li>
+ <li><a href="#/inputs">Inputs</a></li>
+ <li><a href="#/settings">Settings</a></li>
</ul>
diff --git a/data/ui/app/views/test-list.html b/data/ui/app/views/test-list.html
deleted file mode 100644
index ffcf3fb..0000000
--- a/data/ui/app/views/test-list.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<div class="row">
- <div class="span8">
- <h2>{{testDetails.name}}</h2>
- <div class="netTest" ng-controller="TestBoxCtrl">
- version: <span class="badge badge-success">{{testDetails.version}}</span>
- <p>{{testDetails.description}}</p>
- <form name="testOptions">
- <div ng-repeat="(name, options) in testDetails.arguments">
- <div ng-switch on="options.type">
-
- <div ng-switch-when="file">
- <label>{{name}}</label>
- <input type="file" name="{{name}}">
- </div>
-
- <div ng-switch-default>
- <label>{{name}}</label>
- <input ng-model="testDetails.arguments[name].value" type="{{options.type}}"
- value="{{options.default}}">
- </div>
-
- </div>
- </div>
- </form>
- <button class="btn btn-primary" ng-click="startTest()">Start Test</button>
- </div>
- </div>
-</div>
-
-<div class="row">
- <div class="span8">
- <h3>Test results</h3>
- <div class="testResult" ng-repeat="result in testDetails.results">
- <h4>{{result.name}}</h4>
- <pre>{{result.content}}</pre>
- </div>
- </div>
-</div>
diff --git a/data/ui/app/views/test-status.html b/data/ui/app/views/test-status.html
deleted file mode 100644
index a457119..0000000
--- a/data/ui/app/views/test-status.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<h2>Test Status</h2>
-<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
-tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
-quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
-consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
-cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
-proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
diff --git a/data/ui/app/views/test.html b/data/ui/app/views/test.html
new file mode 100644
index 0000000..f92461a
--- /dev/null
+++ b/data/ui/app/views/test.html
@@ -0,0 +1,42 @@
+<div class="row">
+ <div class="span8">
+ <h2>{{testDetails.name}}</h2>
+ <div class="netTest" ng-controller="TestBoxCtrl">
+ version: <span class="badge badge-success">{{testDetails.version}}</span>
+ <p>{{testDetails.description}}</p>
+ <form name="testOptions">
+ <div ng-repeat="(name, options) in testDetails.arguments">
+ <div ng-switch on="options.type">
+
+ <div ng-switch-when="file">
+ <label>{{name}}</label>
+ <select ng-model="testDetails.arguments[name].value">
+ <option ng-repeat="input in inputs" value="input.filename">{{input.filename}}</option>
+ </select>
+ </div>
+
+ <div ng-switch-default>
+ <label>{{name}}</label>
+ <input ng-model="testDetails.arguments[name].value" type="{{options.type}}"
+ value="{{options.default}}">
+ </div>
+
+ </div>
+ </div>
+ </form>
+ <button class="btn btn-primary" ng-click="startTest()">Start Test</button>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="span8">
+ <h3>Test results</h3>
+ <button class="btn" ng-click="updateTestStatus()">
+ <i class="icon-refresh"></i>Reload</button>
+ <div ng-repeat="result in testDetails.results">
+ <h4>{{result.name}}</h4>
+ <pre class="testResult">{{result.content}}</pre>
+ </div>
+ </div>
+</div>
diff --git a/ooni/api/spec.py b/ooni/api/spec.py
index ec4cf4b..39df2fd 100644
--- a/ooni/api/spec.py
+++ b/ooni/api/spec.py
@@ -48,14 +48,15 @@ class Inputs(ORequestHandler):
self.write(input_list)
def post(self):
- filename = self.get_argument("fullname", None)
+ input_file = self.request.files.get("file")[0]
+ filename = input_file['filename']
+
if not filename or not re.match('(\w.*\.\w.*).*', filename):
raise InvalidInputFilename
if os.path.exists(filename):
raise FilenameExists
- input_file = self.request.files.get("input_file")
content_type = input_file["content_type"]
body = input_file["body"]
@@ -120,6 +121,7 @@ def get_test_results(test_id):
test_content = ''.join(f.readlines())
test_results.append({'name': test_result,
'content': test_content})
+ test_results.reverse()
return test_results
class TestStatus(ORequestHandler):