commit 4f51cd3107bc1f9e65be587aeb3b41f7a584e3d7 Author: Arturo Filastò art@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):