This is an experience report. A professor once needed a simple web application that he and his TA's could use to track over 80 students, who were themselves creating a single page application in a three hour time period for a midterm exam. Students could arrive during a certain time period, and then had to finish within 3 hours to pass off their work.
Since the class was "Introduction to Web Programming," obviously the professor would be able to create this web application. The class used the "M.E.A.N. stack" but for speed of development, the professor chose to use what might be called the "A.P." stack. [Mongo, Express, Angular, Node vs. Angular, Picos]
Picos are available for use by anyone. Download the pico engine and/or study the documentation at the Picolabs website (picolabs.io). Picolabs is an open source project, and your contributions and use are invited.
The application has fields for entering the number (ex. N005) of the examination paper, and the name of the student. The TA providing the exam paper enters the data and clicks the "timing started" button.
A screenshot before any exam papers have been given out.
A screenshot after two exam papers have been given out, one of which has been returned. When the student with paper number "n2" returns, the TA will click the link "timing finished" and begin grading the work.
<!doctype html>
<html>
<head>
<title>Timing</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
<script src="timing.js"></script>
<style type="text/css">
span.finished { cursor: pointer; color: blue; text-decoration: underline; }
div.eci input { visibility: hidden; }
div.eci:hover input { visibility: visible; }
</style>
</head>
<body ng-app="timing" ng-controller="MainCtrl">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-header">
<h1>Timing</h1>
</div>
<form ng-submit="addTiming()" style = "margin-top:30px;">
<input type="text" ng-model="number" placeholder="number"></input>
<input type="text" ng-model="name" placeholder="name"></input>
<button type="submit">timing started</button>
</form>
<div ng-repeat="timing in timings | orderBy: '-ordinal'">
{{timing.number}} ·
{{timing.name}} ·
{{timing.time_out}} ·
{{timing.time_in}}
<span class="finished" ng-click="finished(timing.number)" ng-if="!timing.time_in">timing finished</span>
<span ng-if="!!timing.time_in">· {{timeDiff(timing)}}</span>
</div>
<div class="eci">
<input type="text" ng-model="eci" size="29"></input>
</div>
</div>
</div>
</body>
</html>
Lines 20-24 collect the data when a timing starts,
obligating us to provide a function addTiming
in the JavaScript code.
Lines 25-32 display the data, with one div
per timing,
and indicate that we will provide the data in $scope.timings
in the JavaScript code.
Line 30 appears only when there is not yet a finish time,
and provides the link (styled by line 9),
and indicates that we will provide a function finished
in the JavaScript code.
Line 31 appears only when there is a finish time
and indicates that we will provide a function timeDiff
in the JavaScript code.
Lines 33-35 provide a way to enter an event channel identifier (ECI)
for the pico that we will be using.
Lines 10-11 style the div
so that it is only visible when
the user hovers over it.
The code provides the promised definitions of
function addTiming
(lines 8-16),
function finished
(lines 18-24),
function getAll
(lines 26-31), and
function timeDiff
(lines 35-47).
Additionally, the code provides a default value for the ECI in line 6,
and makes the initial call to the function getAll
in line 33.
angular.module('timing', [])
.controller('MainCtrl', [
'$scope','$http',
function($scope,$http){
$scope.timings = [];
$scope.eci = "cj1i5z6240003s5ddpomhahb6";
var bURL = '/sky/event/'+$scope.eci+'/eid/timing/started';
$scope.addTiming = function() {
var pURL = bURL + "?number=" + $scope.number + "&name=" + $scope.name;
return $http.post(pURL).success(function(data){
$scope.getAll();
$scope.number='';
$scope.name='';
});
};
var iURL = '/sky/event/'+$scope.eci+'/eid/timing/finished';
$scope.finished = function(number) {
var pURL = iURL + "?number=" + number;
return $http.post(pURL).success(function(data){
$scope.getAll();
});
};
var gURL = '/sky/cloud/'+$scope.eci+'/timing_tracker/entries';
$scope.getAll = function() {
return $http.get(gURL).success(function(data){
angular.copy(data, $scope.timings);
});
};
$scope.getAll();
$scope.timeDiff = function(timing) {
var bgn_sec = Math.floor(Date.parse(timing.time_out)/1000);
var end_sec = Math.floor(Date.parse(timing.time_in)/1000);
var sec_num = end_sec - bgn_sec;
var hours = Math.floor(sec_num / 3600);
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
var seconds = sec_num - (hours * 3600) - (minutes * 60);
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
if (seconds < 10) {seconds = "0"+seconds;}
return hours+':'+minutes+':'+seconds;
}
}
]);
Function addTiming
sends to the pico a
timing:started
event.
This obligates us to provide a ruleset containing a rule
that selects on this event.
Function finished
sends to the pico a
timing:finished
event.
This obligates us to provide a ruleset containing a rule
that selects on this event.
Function getAll
sends to the pico a
query to run a KRL function entries
.
This obligates us to name the ruleset timing_tracker
that shares a function named entries
.
We choose a pico engine which is available on the internet,
provide each TA with an ECI (might be the same or different
(so that we can revoke them separately)) for the pico.
We install the ruleset named timing_tracker
into the pico.
The ruleset shares (line 3) the function entries
(defined in lines 6-8), which simply returns the entity variable
ent:entries
or the empty array.
Rule timing_started
(lines 10-27) selects on event domain
timing
and type started
when there is an event attribute number
that
matches the regular expression of line 11
(the letter "N" followed by any number of leading zeros and at least
one other digit).
It extracts the digits of number
as a number
into the value named ordinal
.
If there is not already a timing with that ordinal, the rule fires.
Finally, it appends to the array in ent:entries
a map containing the data for this timing.
Note that the element names in this map exactly match those
expected by the Angular JavaScript code.
ruleset timing_tracker {
meta {
shares entries
}
global {
entries = function() {
ent:timings.defaultsTo([])
}
}
rule timing_started {
select when timing started number re#(n0*\d+)#i setting(number)
pre {
name = event:attr("name")
ordinal = number.extract(re#n0*(\d+)#i)[0].as("Number")
time_out = time:now()
exists = ent:timings.defaultsTo([])
.filter(function(v){v{"ordinal"} == ordinal})
}
if exists.length() == 0 then noop()
fired {
ent:timings := ent:timings.defaultsTo([]).append({
"ordinal": ordinal,
"number": number,
"name": name,
"time_out": time_out })
}
}
rule timing_finished {
select when timing finished number re#n0*(\d+)#i setting(ordinal_string)
foreach ent:timings setting(v,k)
pre {
ordinal = ordinal_string.as("Number")
this_one = ordinal == v{"ordinal"}
}
if this_one then noop()
fired {
ent:timings{[k,"time_in"]} := time:now()
}
}
}
Rule timing_finished
(lines 28-39)
selects when the event with
domain timing
and type finished
occurs, provided it has an attribute number
matching
the regular expression in line 29.
If it does, the rule is selected with ordinal_string
set to
the number.
The rule examines each timing in ent:entries
and fires for the one with matching ordinal
.
When it fires, it sets an attribute time_in
on the map for that timing.