From d78d6c00ad18e2c335d4ba484c573e89c8389e1f Mon Sep 17 00:00:00 2001
From: Philip ABBET <philip.abbet@idiap.ch>
Date: Wed, 31 Jan 2018 09:17:18 +0100
Subject: [PATCH] [algorithms] Add the notion of 'sequential' and 'autonomous'
 algorithms to the editor

---
 .../migrations/0004_algorithm_type.py         |  54 +++++
 beat/web/algorithms/models.py                 |  20 ++
 beat/web/algorithms/serializers.py            |   0
 .../algorithms/static/algorithms/js/editor.js | 213 ++++++++++++++++--
 .../templates/algorithms/edition.html         |  70 +++++-
 .../templates/algorithms/panels/editor.html   |  25 +-
 6 files changed, 354 insertions(+), 28 deletions(-)
 create mode 100644 beat/web/algorithms/migrations/0004_algorithm_type.py
 mode change 100644 => 100755 beat/web/algorithms/serializers.py

diff --git a/beat/web/algorithms/migrations/0004_algorithm_type.py b/beat/web/algorithms/migrations/0004_algorithm_type.py
new file mode 100644
index 000000000..dcb560736
--- /dev/null
+++ b/beat/web/algorithms/migrations/0004_algorithm_type.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.13 on 2018-01-30 10:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.conf import settings
+
+import simplejson
+
+
+def determine_types(apps, schema_editor):
+    '''Refreshes each database so datasets/outputs are recreated'''
+
+    Algorithm = apps.get_model("algorithms", "Algorithm")
+
+    if Algorithm.objects.count():
+        print('')
+
+    for algorithm in Algorithm.objects.order_by('id'):
+        print("Determining type of algorithm '%s/%s/%d'..." % \
+                (algorithm.author.username, algorithm.name, algorithm.version))
+
+        print algorithm.declaration_file.path
+
+        with open(algorithm.declaration_file.path, 'r') as f:
+            declaration = simplejson.load(f)
+
+        if 'type' in declaration:
+            if declaration['type'] == 'sequential':
+                algorithm.type = 'S'
+            elif declaration['type'] == 'autonomous':
+                algorithm.type = 'A'
+            else:
+                algorithm.type = 'L'
+        else:
+            algorithm.type = 'L'
+
+        algorithm.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('algorithms', '0003_auto_20161123_1218'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='algorithm',
+            name='type',
+            field=models.CharField(choices=[(b'L', b'Legacy'), (b'S', b'Sequential'), (b'A', b'Autonomous')], default=b'S', max_length=1),
+        ),
+        migrations.RunPython(determine_types),
+    ]
diff --git a/beat/web/algorithms/models.py b/beat/web/algorithms/models.py
index 15c44ef09..2b7d0969a 100755
--- a/beat/web/algorithms/models.py
+++ b/beat/web/algorithms/models.py
@@ -108,6 +108,17 @@ class AlgorithmManager(CodeManager):
 
 class Algorithm(Code):
 
+    LEGACY     = 'L'
+    SEQUENTIAL = 'S'
+    AUTONOMOUS = 'A'
+
+    TYPES = (
+        (LEGACY,     'Legacy'),
+        (SEQUENTIAL, 'Sequential'),
+        (AUTONOMOUS, 'Autonomous'),
+    )
+
+
     #_____ Fields __________
 
     declaration_file = models.FileField(
@@ -140,6 +151,7 @@ class Algorithm(Code):
     splittable        = models.BooleanField(default=False,
                                             help_text='Defines if the code can be executed ' \
                                             'in multiple instances')
+    type              = models.CharField(max_length=1, choices=TYPES, default=SEQUENTIAL)
 
     referenced_libraries = models.ManyToManyField(Library,
                                                   blank=True, related_name='used_by_algorithms',
@@ -251,6 +263,14 @@ class Algorithm(Code):
         # Set splittability
         self.splittable = wrapper.splittable
 
+        # Set type
+        if wrapper.type == 'sequential':
+            self.type = Algorithm.SEQUENTIAL
+        elif wrapper.type == 'autonomous':
+            self.type = Algorithm.AUTONOMOUS
+        else:
+            self.type = Algorithm.LEGACY
+
         # Invoke the base implementation
         super(Algorithm, self).save(*args, **kwargs)
 
diff --git a/beat/web/algorithms/serializers.py b/beat/web/algorithms/serializers.py
old mode 100644
new mode 100755
diff --git a/beat/web/algorithms/static/algorithms/js/editor.js b/beat/web/algorithms/static/algorithms/js/editor.js
index c06bc00b8..cfe7dc221 100644
--- a/beat/web/algorithms/static/algorithms/js/editor.js
+++ b/beat/web/algorithms/static/algorithms/js/editor.js
@@ -36,13 +36,35 @@ if (beat.contributions.editor === undefined)
     beat.contributions.editor = {}
 
 
-
-beat.contributions.editor.PROCESS_METHOD_ALGORITHM = 'def process(self, inputs, outputs):';
-beat.contributions.editor.PROCESS_METHOD_ANALYZER  = 'def process(self, inputs, output):';
-beat.contributions.editor.SETUP_METHOD_SIGNATURE   = 'def setup(self, parameters):';
-beat.contributions.editor.SETUP_METHOD_CONTENT     = [ '# TODO: Use the parameters',
-                                                    'return True'
-                                                  ]
+beat.contributions.editor.LEGACY     = 'legacy';
+beat.contributions.editor.SEQUENTIAL = 'sequential';
+beat.contributions.editor.AUTONOMOUS = 'autonomous';
+
+beat.contributions.editor.PROCESS_METHOD_ALGORITHM_LEGACY     = 'def process(self, inputs, outputs):';
+beat.contributions.editor.PROCESS_METHOD_ALGORITHM_SEQUENTIAL = 'def process(self, inputs, data_loaders, outputs):';
+beat.contributions.editor.PROCESS_METHOD_ALGORITHM_AUTONOMOUS = 'def process(self, data_loaders, outputs):';
+beat.contributions.editor.PROCESS_METHOD_ANALYZER_LEGACY      = 'def process(self, inputs, output):';
+beat.contributions.editor.PROCESS_METHOD_ANALYZER_SEQUENTIAL  = 'def process(self, inputs, data_loaders, output):';
+beat.contributions.editor.PROCESS_METHOD_ANALYZER_AUTONOMOUS  = 'def process(self, data_loaders, output):';
+
+beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES = [
+    beat.contributions.editor.PROCESS_METHOD_ALGORITHM_LEGACY,
+    beat.contributions.editor.PROCESS_METHOD_ALGORITHM_SEQUENTIAL,
+    beat.contributions.editor.PROCESS_METHOD_ALGORITHM_AUTONOMOUS,
+    beat.contributions.editor.PROCESS_METHOD_ANALYZER_LEGACY,
+    beat.contributions.editor.PROCESS_METHOD_ANALYZER_SEQUENTIAL,
+    beat.contributions.editor.PROCESS_METHOD_ANALYZER_AUTONOMOUS,
+];
+
+beat.contributions.editor.SETUP_METHOD_SIGNATURE = 'def setup(self, parameters):';
+beat.contributions.editor.SETUP_METHOD_CONTENT   = [ '# TODO: Use the parameters',
+                                                     'return True'
+                                                   ]
+
+beat.contributions.editor.PREPARE_METHOD_SIGNATURE = 'def prepare(self, data_loaders):';
+beat.contributions.editor.PREPARE_METHOD_CONTENT   = [ '# TODO: One-time only computations',
+                                                       'return True'
+                                                     ]
 
 
 function name_check()
@@ -91,10 +113,14 @@ beat.contributions.editor.SourceCodeEditor.prototype.addSetupMethod = function()
         return;
 
     // Determine where to insert the new method
-    var process_method_index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ALGORITHM);
+    var process_method_index = null;
 
-    if (process_method_index === null)
-        process_method_index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ANALYZER);
+    for (var i = 0; i < beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES.length; ++i)
+    {
+        process_method_index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES[i]);
+        if (process_method_index !== null)
+            break;
+    }
 
     if (process_method_index === null)
         process_method_index = lines.length;
@@ -181,10 +207,118 @@ beat.contributions.editor.SourceCodeEditor.prototype.removeSetupMethod = functio
 }
 
 
+//----------------------------------------------------------------------------------------
+// Add the prepare() method in the source code (if necessary)
+//----------------------------------------------------------------------------------------
+beat.contributions.editor.SourceCodeEditor.prototype.addPrepareMethod = function()
+{
+    // Retrieve the source code
+    var lines          = this._getSourceCodeLines();
+    var stripped_lines = this._stripLines(lines);
+
+    // If the prepare() method is already in the source code, don't do anything
+    if (this._searchLine(stripped_lines, beat.contributions.editor.PREPARE_METHOD_SIGNATURE))
+        return;
+
+    // Determine where to insert the new method
+    var process_method_index = null;
+
+    for (var i = 0; i < beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES.length; ++i)
+    {
+        process_method_index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES[i]);
+        if (process_method_index !== null)
+            break;
+    }
+
+    if (process_method_index === null)
+        process_method_index = lines.length;
+
+    var indent = '    ';
+    if (process_method_index < lines.length)
+    {
+        var line = lines[process_method_index];
+        indent = line.substr(0, line.indexOf('def'));
+    }
+
+    // Generate the new source code
+    var source_code = '';
+    for (var i = 0; i < process_method_index; ++i)
+        source_code += lines[i] + '\n';
+
+    source_code += indent + beat.contributions.editor.PREPARE_METHOD_SIGNATURE + '\n';
+    for (var i = 0; i < beat.contributions.editor.PREPARE_METHOD_CONTENT.length; ++i)
+        source_code += indent + indent + beat.contributions.editor.PREPARE_METHOD_CONTENT[i] + '\n';
+
+    if (process_method_index < lines.length)
+        source_code += '\n';
+
+    for (var i = process_method_index; i < lines.length; ++i)
+        source_code += lines[i] + '\n';
+
+    this.editor.setValue(source_code);
+}
+
+
+//----------------------------------------------------------------------------------------
+// Remove the prepare() method in the source code (if not modified by the user)
+//----------------------------------------------------------------------------------------
+beat.contributions.editor.SourceCodeEditor.prototype.removePrepareMethod = function()
+{
+    // Retrieve the source code
+    var lines          = this._getSourceCodeLines();
+    var stripped_lines = this._stripLines(lines);
+
+    // Search the position of the prepare() method in the source code
+    var index = this._searchLine(stripped_lines, beat.contributions.editor.PREPARE_METHOD_SIGNATURE);
+    if (index === null)
+        return;
+
+    // Determine if the method was modified by the user
+    var stripped_content = this._stripLines(beat.contributions.editor.PREPARE_METHOD_CONTENT);
+
+    if (stripped_lines[index + 1] == stripped_content[1])
+    {
+        var nb = 2;
+        while (stripped_lines[index + nb] == '')
+            ++nb;
+
+        lines.splice(index, nb);
+    }
+
+    else if (stripped_lines[index + 1] == stripped_content[0])
+    {
+        if (stripped_lines[index + 2] == stripped_content[1])
+        {
+            var nb = 3;
+            while (stripped_lines[index + nb] == '')
+                ++nb;
+
+            lines.splice(index, nb);
+        }
+        else
+        {
+            return;
+        }
+    }
+
+    else
+    {
+        return;
+    }
+
+    // Generate the new source code
+    var source_code = '';
+    for (var i = 0; i < lines.length; ++i)
+        source_code += lines[i] + '\n';
+
+    this.editor.setValue(source_code);
+}
+
+
 //----------------------------------------------------------------------------------------
 // Modify the signature of the process() method in the source code
 //----------------------------------------------------------------------------------------
-beat.contributions.editor.SourceCodeEditor.prototype.changeProcessMethod = function(analyzer)
+beat.contributions.editor.SourceCodeEditor.prototype.changeProcessMethod = function(analyzer, type)
 {
     // Retrieve the source code
     var lines          = this._getSourceCodeLines();
@@ -192,20 +326,52 @@ beat.contributions.editor.SourceCodeEditor.prototype.changeProcessMethod = funct
 
     // Search the position of the process() method in the source code
     var index = null;
+
+    for (var i = 0; i < beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES.length; ++i)
+    {
+        index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ALL_SIGNATURES[i]);
+        if (index !== null)
+            break;
+    }
+
+    if (index === null)
+        return;
+
     var new_signature = null;
     if (analyzer)
     {
-        index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ALGORITHM);
-        new_signature = beat.contributions.editor.PROCESS_METHOD_ANALYZER;
+        switch (type)
+        {
+        case beat.contributions.editor.LEGACY:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ANALYZER_LEGACY;
+            break;
+
+        case beat.contributions.editor.SEQUENTIAL:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ANALYZER_SEQUENTIAL;
+            break;
+
+        case beat.contributions.editor.AUTONOMOUS:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ANALYZER_AUTONOMOUS;
+            break;
+        }
     }
     else
     {
-        index = this._searchLine(stripped_lines, beat.contributions.editor.PROCESS_METHOD_ANALYZER);
-        new_signature = beat.contributions.editor.PROCESS_METHOD_ALGORITHM;
-    }
+        switch (type)
+        {
+        case beat.contributions.editor.LEGACY:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ALGORITHM_LEGACY;
+            break;
 
-    if (index === null)
-        return;
+        case beat.contributions.editor.SEQUENTIAL:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ALGORITHM_SEQUENTIAL;
+            break;
+
+        case beat.contributions.editor.AUTONOMOUS:
+            new_signature = beat.contributions.editor.PROCESS_METHOD_ALGORITHM_AUTONOMOUS;
+            break;
+        }
+    }
 
     // Generate the new source code
     var indent = lines[index].substr(0, lines[index].indexOf('def'));
@@ -270,7 +436,8 @@ beat.contributions.editor.SourceCodeEditor.prototype._searchLine = function(stri
 //----------------------------------------------------------------------------------------
 // Constructor
 //----------------------------------------------------------------------------------------
-beat.contributions.editor.InputsEditor = function(id, data, dataformats, smart_selector)
+beat.contributions.editor.InputsEditor = function(id, data, dataformats, smart_selector,
+                                                  callbacks)
 {
     this.element        = $('#' + id);
     this.groups         = [];
@@ -278,6 +445,7 @@ beat.contributions.editor.InputsEditor = function(id, data, dataformats, smart_s
     this.dataformats    = dataformats;
     this.smart_selector = smart_selector;
     this.analyzer       = false;
+    this.callbacks      = callbacks;
 
 
     var panel = this;
@@ -751,6 +919,9 @@ beat.contributions.editor.InputsEditor.prototype._addGroup = function(data)
                     });
                 }
 
+                if ((panel.groups.length <= 1) && panel.callbacks.removeMethodFromSourceCode)
+                    panel.callbacks.removeMethodFromSourceCode();
+
                 panel._update();
                 break;
             }
@@ -811,6 +982,10 @@ beat.contributions.editor.InputsEditor.prototype._addGroup = function(data)
     }
 
 
+    if ((this.groups.length > 1) && this.callbacks.addMethodToSourceCode)
+        this.callbacks.addMethodToSourceCode();
+
+
     var input_names = Object.keys(data.inputs);
     for (var i = 0; i < input_names.length; ++i)
     {
diff --git a/beat/web/algorithms/templates/algorithms/edition.html b/beat/web/algorithms/templates/algorithms/edition.html
index 4b6528cd1..27ce06790 100644
--- a/beat/web/algorithms/templates/algorithms/edition.html
+++ b/beat/web/algorithms/templates/algorithms/edition.html
@@ -107,8 +107,19 @@ function setupEditor(algorithm, dataformats, libraries)
 
   var inputs_editor = new beat.contributions.editor.InputsEditor(
       'inputs_editor', declaration.groups,
-      dataformats, smart_selector
-      );
+      dataformats, smart_selector,
+      {
+        addMethodToSourceCode: function()
+        {
+          source_code_editor.addPrepareMethod();
+        },
+
+        removeMethodFromSourceCode: function()
+        {
+          source_code_editor.removePrepareMethod();
+        },
+      }
+  );
 
   inputs_editor.setAnalyzer(checkbox_analyzer[0].checked);
 
@@ -146,7 +157,7 @@ function setupEditor(algorithm, dataformats, libraries)
 
         displayErrors: displayErrors,
       }
-      );
+  );
 
 
 {% if not binary %}
@@ -160,6 +171,39 @@ function setupEditor(algorithm, dataformats, libraries)
       );
 {% endif %}
 
+
+  // Type radio buttons handling
+  var type_sequential_selector = $('#type_sequential');
+  var type_autonomous_selector = $('#type_autonomous');
+
+  type_sequential_selector.change(function() {
+    source_code_editor.changeProcessMethod(checkbox_analyzer[0].checked,
+                                           beat.contributions.editor.SEQUENTIAL);
+  });
+
+  type_autonomous_selector.change(function() {
+      source_code_editor.changeProcessMethod(checkbox_analyzer[0].checked,
+                                             beat.contributions.editor.AUTONOMOUS);
+  });
+
+  if (declaration.type == 'autonomous')
+  {
+      type_autonomous_selector[0].checked = true;
+  }
+  else if (declaration.type === undefined)
+  {
+      alert("Legacy algorithms can't be created/edited anymore. You'll need to implement a sequential or an autonomous one.");
+      type_sequential_selector[0].checked = true;
+
+      source_code_editor.changeProcessMethod(checkbox_analyzer[0].checked,
+                                             beat.contributions.editor.SEQUENTIAL);
+  }
+  else
+  {
+      type_sequential_selector[0].checked = true;
+  }
+
+
   // Analyzer checkbox handling
   checkbox_analyzer.change(function() {
     if (checkbox_analyzer[0].checked)
@@ -178,7 +222,8 @@ function setupEditor(algorithm, dataformats, libraries)
     }
 
 {% if not binary %}
-    source_code_editor.changeProcessMethod(checkbox_analyzer[0].checked);
+    source_code_editor.changeProcessMethod(checkbox_analyzer[0].checked,
+                                           type_sequential_selector[0].checked);
 {% endif %}
     inputs_editor.setAnalyzer(checkbox_analyzer[0].checked);
   });
@@ -253,6 +298,13 @@ function setupEditor(algorithm, dataformats, libraries)
         return false;
     }
 
+    declaration.api_version = 2;
+
+    if (type_sequential_selector[0].checked)
+      declaration.type = 'sequential';
+    else
+      declaration.type = 'autonomous';
+
     declaration.groups = inputs_editor.toJSON(displayErrors);
     if (declaration.groups === null)
       return false;
@@ -403,6 +455,15 @@ function setupEditor(algorithm, dataformats, libraries)
   </div>
 </div>
 
+<div class="row">
+  <div class="col-sm-offset-1 col-sm-10 contribution_editor" style="display: none;">
+    <label for="type_sequential">Type:</label>
+    <input id="type_sequential" type="radio" name="type" checked /><span class="label">Sequential</span>
+    <input id="type_autonomous" type="radio" name="type" /><span class="label">Autonomous</span>
+    <p class="help">The API used by this algorithm</p>
+  </div>
+</div>
+
 {% if not edition and not binary %}
 <div class="row">
   <div class="col-sm-offset-1 col-sm-10 contribution_editor" style="display: none;">
@@ -523,7 +584,6 @@ function setupEditor(algorithm, dataformats, libraries)
       </div>{# panel #}
 
 
-
       {# libraries editor panel #}
     {% if not binary %}
       <div id="libraries-editor-panel" class="panel panel-default contribution_editor " style="display:none;">
diff --git a/beat/web/algorithms/templates/algorithms/panels/editor.html b/beat/web/algorithms/templates/algorithms/panels/editor.html
index c9603aaf7..b12298093 100644
--- a/beat/web/algorithms/templates/algorithms/panels/editor.html
+++ b/beat/web/algorithms/templates/algorithms/panels/editor.html
@@ -22,10 +22,27 @@
 {% load ui_tags %}
 <div class="row">
   <div class="col-sm-10">
-    {% if object.splittable or object.analysis %}
-    <div class="alert alert-{% if object.splittable %}success{% else %}info{% endif %}">
-      {% if object.splittable %}<i class="fa fa-random"></i> This algorithm is <strong><span class="text-success">splittable</span></strong>{% endif %}
-      {% if object.analysis %}<i class="fa fa-line-chart"></i> This algorithm is an <strong><span class="text-primary">analyzer</span></strong>. It can only be used on analysis blocks.{% endif %}
+    <div class="alert alert-info">
+      {% ifequal object.type "L" %}
+        <i class="fa fa-book"></i> This algorithm is a <strong><span class="text-primary">legacy</span></strong> one. The API has changed since its implementation. New versions and forks will need to be updated.
+      {% endifequal %}
+      {% ifequal object.type "S" %}
+        <i class="fa fa-tasks"></i> This algorithm is a <strong><span class="text-primary">sequential</span></strong> one. The platform will call its <i><span class="text-primary">process()</span></i> method once per data incoming on its inputs.
+      {% endifequal %}
+      {% ifequal object.type "A" %}
+        <i class="fa fa-cogs"></i> This algorithm is a <strong><span class="text-primary">autonomous</span></strong> one. Its <i><span class="text-primary">process()</span></i> method will only be called once, and is expected to iterate through the data incoming on its inputs itself.
+      {% endifequal %}
+    </div>
+
+    {% if object.splittable %}
+    <div class="alert alert-success">
+      <i class="fa fa-random"></i> This algorithm is <strong><span class="text-success">splittable</span></strong>
+    </div>
+    {% endif %}
+
+    {% if object.analysis %}
+    <div class="alert alert-info">
+      <i class="fa fa-line-chart"></i> This algorithm is an <strong><span class="text-primary">analyzer</span></strong>. It can only be used on analysis blocks.
     </div>
     {% endif %}
 
-- 
GitLab