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