Using Picos as the server-side for a Single Page Application

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 single page application

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.

application page with no data

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.

application page with two entries; one finished

Angular HTML

<!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}} &middot;
    {{timing.name}} &middot;
    {{timing.time_out}} &middot;
    {{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">&middot; {{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.

Angular JavaScript

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.

The pico

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.

Pico ruleset in KRL

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.

© CC-BY Bruce Conrad
April 14, 2017