diff --git a/beat/web/algorithms/models.py b/beat/web/algorithms/models.py index 3b74c9df06aa739e0b85b3c610da80fc81fc62c9..a4d4e5aaa8e29e45a6ceb64b46108e3ed1b0dbe0 100755 --- a/beat/web/algorithms/models.py +++ b/beat/web/algorithms/models.py @@ -28,7 +28,6 @@ from django.db import models from django.conf import settings from django.core.urlresolvers import reverse -from django.db.models import Count, Q import beat.core.algorithm import beat.core.library @@ -49,21 +48,22 @@ from ..libraries.models import Library import simplejson import collections -import itertools -#---------------------------------------------------------- +# ---------------------------------------------------------- def validate_algorithm(declaration): """Validates the declaration of an algorithm code, returns wrapper""" if not declaration: - raise SyntaxError('Algorithm declaration cannot be empty') + raise SyntaxError("Algorithm declaration cannot be empty") - if not(isinstance(declaration, dict)): + if not isinstance(declaration, dict): try: - declaration_dict = simplejson.loads(declaration, object_pairs_hook=collections.OrderedDict) + declaration_dict = simplejson.loads( + declaration, object_pairs_hook=collections.OrderedDict + ) except Exception as e: raise SyntaxError(str(e)) else: @@ -72,154 +72,149 @@ def validate_algorithm(declaration): algorithm = beat.core.algorithm.Algorithm(settings.PREFIX, declaration_dict) if not algorithm.valid: - raise SyntaxError('\n * %s' % '\n * '.join(algorithm.errors)) + raise SyntaxError("\n * %s" % "\n * ".join(algorithm.errors)) return algorithm -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmStorage(OverwriteStorage): - def __init__(self, *args, **kwargs): - kwargs['location'] = settings.ALGORITHMS_ROOT + kwargs["location"] = settings.ALGORITHMS_ROOT super(AlgorithmStorage, self).__init__(*args, **kwargs) -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmManager(CodeManager): - - def create_algorithm(self, author, name, short_description='', - description='', declaration=None, code=None, version=1, - previous_version=None, fork_of=None): + def create_algorithm( + self, + author, + name, + short_description="", + description="", + declaration=None, + code=None, + version=1, + previous_version=None, + fork_of=None, + ): default = beat.core.algorithm.Algorithm(settings.PREFIX, data=None) - return self.create_code(author, name, default, short_description, - description, declaration, code, version, - previous_version, fork_of) + return self.create_code( + author, + name, + default, + short_description, + description, + declaration, + code, + version, + previous_version, + fork_of, + ) -#---------------------------------------------------------- +# ---------------------------------------------------------- class Algorithm(Code): - LEGACY = 'L' - SEQUENTIAL = 'S' - AUTONOMOUS = 'A' + LEGACY = "L" + SEQUENTIAL = "S" + AUTONOMOUS = "A" - TYPES = ( - (LEGACY, 'Legacy'), - (SEQUENTIAL, 'Sequential'), - (AUTONOMOUS, 'Autonomous'), - ) + TYPES = ((LEGACY, "Legacy"), (SEQUENTIAL, "Sequential"), (AUTONOMOUS, "Autonomous")) - - #_____ Fields __________ + # _____ Fields __________ declaration_file = models.FileField( storage=AlgorithmStorage(), upload_to=get_contribution_declaration_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='declaration' + db_column="declaration", ) description_file = models.FileField( storage=AlgorithmStorage(), upload_to=get_contribution_description_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='description' + db_column="description", ) source_code_file = models.FileField( storage=AlgorithmStorage(), upload_to=get_contribution_source_code_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='source_code' + db_column="source_code", ) # Read-only parameters that are updated at every save(), if required - parameters = models.TextField(blank=True, null=True) + parameters = models.TextField(blank=True, null=True) result_dataformat = models.TextField(blank=True, null=True) - 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) + 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', - symmetrical=False) + referenced_libraries = models.ManyToManyField( + Library, blank=True, related_name="used_by_algorithms", symmetrical=False + ) objects = AlgorithmManager() - - #_____ Utilities __________ + # _____ Utilities __________ def get_absolute_url(self): return reverse( - 'algorithms:view', - args=(self.author.username, self.name, self.version,), + "algorithms:view", args=(self.author.username, self.name, self.version) ) - def get_api_update_url(self): - '''Returns the endpoint to update this object''' + """Returns the endpoint to update this object""" return reverse( - 'api_algorithms:object', - args=(self.author.username, self.name, self.version,), + "api_algorithms:object", + args=(self.author.username, self.name, self.version), ) - def get_api_share_url(self): - '''Returns the endpoint to share this object''' + """Returns the endpoint to share this object""" return reverse( - 'api_algorithms:share', - args=(self.author.username, self.name, self.version,), + "api_algorithms:share", args=(self.author.username, self.name, self.version) ) - def environments(self): - '''Calculates environment usage for this algorithm + """Calculates environment usage for this algorithm Returns: - list: mapping environment to usage counts, determining how many times - a given algorithm has been successfuly used on that environment + list: annotated environment with usage counts, determining how many times + a given algorithm has been successfully used on that environment - ''' + """ - from ..experiments.models import Block - from ..experiments.models import Experiment from ..backend.models import Environment - # Tries to figure through a maximum if an algorithm has been - # successfuly used inside an environment. - - # Case 1) The block is part of an experiment that was successful - # Case 2) The block is part of an experiment that is not successful - # (failed or other), but it is CACHED (if not cached, then we can't - # attest anything about the algorithm/environment relationship!) + # successfully used inside an environment. - envs = Environment.objects.filter(blocks__in=self.blocks.filter( - Q(experiment__status=Experiment.DONE) | \ - ((~Q(experiment__status=Experiment.DONE)) & Q(status=Block.DONE)) - )).annotate(itemcount=Count('id')).order_by('-creation_date').distinct() + return Environment.objects.use_count(self.blocks) - return [(k, k.itemcount) for k in envs] - - - #_____ Overrides __________ + # _____ Overrides __________ def save(self, *args, **kwargs): wrapper = self._save_preprocessing() @@ -229,44 +224,45 @@ class Algorithm(Code): else: parameters = [] for name, details in wrapper.parameters.items(): - parameters.append({ - "name": name, - "default_value": details.get('default'), - "comment": details.get('description', ''), - "type": details['type'], - } + parameters.append( + { + "name": name, + "default_value": details.get("default"), + "comment": details.get("description", ""), + "type": details["type"], + } ) - if 'choice' in details: - parameters[-1]['choices'] = details['choice'] + if "choice" in details: + parameters[-1]["choices"] = details["choice"] - if 'range' in details: - parameters[-1]['minimum'] = details['range'][0] - parameters[-1]['maximum'] = details['range'][1] + if "range" in details: + parameters[-1]["minimum"] = details["range"][0] + parameters[-1]["maximum"] = details["range"][1] - self.parameters = simplejson.dumps(parameters, - indent=4, - cls=NumpyJSONEncoder) + self.parameters = simplejson.dumps( + parameters, indent=4, cls=NumpyJSONEncoder + ) if not wrapper.results: self.result_dataformat = None else: results = {} for name, details in wrapper.results.items(): - key = name if not details.get('display') else ('+' + name) - results[key] = details['type'] + key = name if not details.get("display") else ("+" + name) + results[key] = details["type"] - self.result_dataformat = simplejson.dumps(results, - indent=4, - cls=NumpyJSONEncoder) + self.result_dataformat = simplejson.dumps( + results, indent=4, cls=NumpyJSONEncoder + ) # Set splittability self.splittable = wrapper.splittable # Set type - if wrapper.type == 'sequential': + if wrapper.type == "sequential": self.type = Algorithm.SEQUENTIAL - elif wrapper.type == 'autonomous': + elif wrapper.type == "autonomous": self.type = Algorithm.AUTONOMOUS else: self.type = Algorithm.LEGACY @@ -279,34 +275,37 @@ class Algorithm(Code): if wrapper.uses is not None: for l in set(wrapper.uses.values()): s = beat.core.library.Storage(settings.PREFIX, l) - library = Library.objects.get(author__username=s.username, - name=s.name, - version=s.version, - ) + library = Library.objects.get( + author__username=s.username, name=s.name, version=s.version + ) self.referenced_libraries.add(library) - def share(self, public, users=None, teams=None): self._share_dataformats(users=users, teams=teams) super(Algorithm, self).share(public=public, users=users, teams=teams) - - #_____ Methods __________ + # _____ Methods __________ def validate(self, declaration): return validate_algorithm(declaration) def analysis(self): - return (self.result_dataformat is not None) + return self.result_dataformat is not None def all_referenced_result_dataformats(self): retval = set() if self.result_dataformat: result_dataformat = simplejson.loads(self.result_dataformat) for value in result_dataformat.values(): - elements = value.split('/') + elements = value.split("/") if len(elements) == 3: - retval.add(DataFormat.objects.get(author__username=elements[0], name=elements[1], version=elements[2])) + retval.add( + DataFormat.objects.get( + author__username=elements[0], + name=elements[1], + version=elements[2], + ) + ) return retval def all_referenced_dataformats(self): @@ -322,33 +321,32 @@ class Algorithm(Code): return list(set(result)) def modifiable(self): - return (self.experiments.count() == 0) and super(Algorithm, self).modifiable() + return self.experiments.count() == 0 and super(Algorithm, self).modifiable() def deletable(self): - return (self.experiments.count() == 0) and super(Algorithm, self).deletable() + return self.experiments.count() == 0 and super(Algorithm, self).deletable() def valid(self): - return (self.source_code_file.name is not None) and (self.source_code_file.name != '') - + return ( + self.source_code_file.name is not None and self.source_code_file.name != "" + ) def core(self): return validate_algorithm(self.declaration) - def uses_and_groups(self): core = self.core() return core.uses, core.groups - def json_parameters(self): return simplejson.loads(self.parameters) if self.parameters else [] - def json_result(self): - return simplejson.loads(self.result_dataformat) if self.result_dataformat else {} - + return ( + simplejson.loads(self.result_dataformat) if self.result_dataformat else {} + ) - #_____ Protected methods __________ + # _____ Protected methods __________ def _share_dataformats(self, users, teams): # Retrieve and process the list of referenced dataformats @@ -369,15 +367,15 @@ class Algorithm(Code): dataformats.share(users=users, teams=teams) -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmEndpoint(models.Model): - algorithm = models.ForeignKey(Algorithm, related_name='endpoints') - input = models.BooleanField(default=False) - name = models.CharField(max_length=200) - dataformat = models.ForeignKey(DataFormat, related_name='algorithm_endpoints') - channel = models.CharField(max_length=200) + algorithm = models.ForeignKey(Algorithm, related_name="endpoints") + input = models.BooleanField(default=False) + name = models.CharField(max_length=200) + dataformat = models.ForeignKey(DataFormat, related_name="algorithm_endpoints") + channel = models.CharField(max_length=200) def __str__(self): - return self.algorithm.fullname() + ', ' + self.name + return self.algorithm.fullname() + ", " + self.name diff --git a/beat/web/algorithms/serializers.py b/beat/web/algorithms/serializers.py index fa486b256cc72ef6264736c9e57458beba52920b..3f40b5be298074e110c7e9d823fc58c9a13fb290 100755 --- a/beat/web/algorithms/serializers.py +++ b/beat/web/algorithms/serializers.py @@ -25,6 +25,10 @@ # # ############################################################################### +import simplejson as json + +import beat.core.algorithm + from rest_framework import serializers from operator import itemgetter @@ -34,15 +38,12 @@ from ..dataformats.serializers import ReferencedDataFormatSerializer from ..attestations.serializers import AttestationSerializer from ..experiments.serializers import ExperimentSerializer from ..experiments.models import Experiment +from ..backend.serializers import EnvironmentInfoSerializer from .models import Algorithm -import simplejson as json - -import beat.core.algorithm - -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmCreationSerializer(CodeCreationSerializer): @@ -51,7 +52,7 @@ class AlgorithmCreationSerializer(CodeCreationSerializer): beat_core_class = beat.core.algorithm -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmSerializer(CodeSerializer): @@ -62,13 +63,17 @@ class AlgorithmSerializer(CodeSerializer): outputs = serializers.SerializerMethodField() referencing_experiments = serializers.SerializerMethodField() referenced_libraries = LibraryReferenceSerializer(many=True) - referenced_dataformats = ReferencedDataFormatSerializer(many=True, source='all_referenced_dataformats') - needed_dataformats = ReferencedDataFormatSerializer(many=True, source='all_needed_dataformats') + referenced_dataformats = ReferencedDataFormatSerializer( + many=True, source="all_referenced_dataformats" + ) + needed_dataformats = ReferencedDataFormatSerializer( + many=True, source="all_needed_dataformats" + ) + environments = EnvironmentInfoSerializer(many=True) class Meta(CodeSerializer.Meta): model = Algorithm - def get_result_dataformat(self, obj): return json.loads(obj.result_dataformat) if obj.result_dataformat else None @@ -76,33 +81,45 @@ class AlgorithmSerializer(CodeSerializer): return json.loads(obj.parameters) if obj.parameters else None def get_inputs(self, obj): - return map(lambda x: { - 'name': x.name, - 'dataformat': x.dataformat.fullname(), - 'channel': x.channel - }, obj.endpoints.filter(input=True).order_by('name')) + return map( + lambda x: { + "name": x.name, + "dataformat": x.dataformat.fullname(), + "channel": x.channel, + }, + obj.endpoints.filter(input=True).order_by("name"), + ) def get_outputs(self, obj): - return map(lambda x: { - 'name': x.name, - 'dataformat': x.dataformat.fullname(), - 'channel': x.channel, - }, obj.endpoints.filter(input=False).order_by('name')) + return map( + lambda x: { + "name": x.name, + "dataformat": x.dataformat.fullname(), + "channel": x.channel, + }, + obj.endpoints.filter(input=False).order_by("name"), + ) def get_referencing_experiments(self, obj): - user = self.context.get('user') - experiments = Experiment.objects.for_user(user, True).filter(blocks=obj.blocks.all()).distinct() + user = self.context.get("user") + experiments = ( + Experiment.objects.for_user(user, True) + .filter(blocks=obj.blocks.all()) + .distinct() + ) serializer = ExperimentSerializer(experiments, many=True) referencing_experiments = serializer.data # Put the pending experiments first - return sorted(referencing_experiments, key=itemgetter('creation_date')) + return sorted(referencing_experiments, key=itemgetter("creation_date")) -#---------------------------------------------------------- +# ---------------------------------------------------------- class FullAlgorithmSerializer(AlgorithmSerializer): - class Meta(AlgorithmSerializer.Meta): - default_fields = AlgorithmSerializer.Meta.default_fields + AlgorithmSerializer.Meta.extra_fields + default_fields = ( + AlgorithmSerializer.Meta.default_fields + + AlgorithmSerializer.Meta.extra_fields + ) diff --git a/beat/web/algorithms/templates/algorithms/view.html b/beat/web/algorithms/templates/algorithms/view.html index 1998f2d4176760f93ebdcc141ecb992c80122f25..e36218cdc3488a2f25de076dd2934671186b77a6 100644 --- a/beat/web/algorithms/templates/algorithms/view.html +++ b/beat/web/algorithms/templates/algorithms/view.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -128,7 +128,7 @@ This algorithm was never executed. {% else %} <ul class="list-group"> - {% for key, value in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' key.name key.version %}">{{ key.fullname }}</a> <span class="badge">{{ value }}</span></li>{% endfor %} + {% for info in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' info.name info.version %}">{{ info.fullname }}</a> <span class="badge">{{ info.use_count }}</span></li>{% endfor %} </ul> <p class="help">This table shows the number of times this algorithm has been <b>successfully</b> run using the given environment. Note diff --git a/beat/web/backend/models/environment.py b/beat/web/backend/models/environment.py index 7233b79201caf2ca1f14e49ba7f1f308509d975c..3dc14b3334dbc797d0945af893e1bf2a15c6a9f9 100755 --- a/beat/web/backend/models/environment.py +++ b/beat/web/backend/models/environment.py @@ -27,6 +27,7 @@ from django.db import models from django.core.urlresolvers import reverse +from django.db.models import Count, Q from ...code.models import Code from ...common.models import Shareable @@ -34,112 +35,122 @@ from ...common.models import ShareableManager from ...common.texts import Messages -#---------------------------------------------------------- +# ---------------------------------------------------------- class EnvironmentManager(ShareableManager): - def get_by_natural_key(self, name, version): return self.get(name=name, version=version) def get_by_fullname(self, fullname): - name, version = fullname.rsplit(' ', 1) + name, version = fullname.rsplit(" ", 1) return self.get_by_natural_key(name, version[1:-1]) + def use_count(self, block_list): + """Returns a list of environments used for the blocks in that list with + how many times each has been used. + + Parameters: + block_list (Block) : list of blocks to analyze. + + Returns: + list: annotated environments with the number of time they have been + used for blocks that are done. + """ + + from ...experiments.models import Experiment + from ...experiments.models import Block + + # Tries to figure through a maximum if blocks in the list have been + # successfully used inside an environment. + + # Case 1) The block is part of an experiment that was successful + # Case 2) The block is part of an experiment that is not successful + # (failed or other), but it is CACHED (if not cached, then we can't + # attest anything about the block/environment relationship!) + + return ( + self.filter( + blocks__in=block_list.filter( + Q(experiment__status=Experiment.DONE) + | ((~Q(experiment__status=Experiment.DONE)) & Q(status=Block.DONE)) + ) + ) + .annotate(use_count=Count("id")) + .order_by("-creation_date") + .distinct() + ) + -#---------------------------------------------------------- +# ---------------------------------------------------------- class Environment(Shareable): """Defines a software environment to run algorithms""" - name = models.CharField( - max_length=200, - help_text=Messages['name'], - ) + name = models.CharField(max_length=200, help_text=Messages["name"]) version = models.CharField( max_length=20, - help_text='Free-style version for this environment (normally read from the Worker/Scheduler available environments)', + help_text="Free-style version for this environment (normally read from the Worker/Scheduler available environments)", ) short_description = models.CharField( - max_length=100, - default='', - blank=True, - help_text=Messages['short_description'], + max_length=100, default="", blank=True, help_text=Messages["short_description"] ) description = models.TextField( - default='', - blank=True, - help_text=Messages['description'], + default="", blank=True, help_text=Messages["description"] ) - creation_date = models.DateTimeField( - 'Creation date', - auto_now_add = True, - ) + creation_date = models.DateTimeField("Creation date", auto_now_add=True) active = models.BooleanField( - default=True, - help_text='If this environment can be used in experiments', + default=True, help_text="If this environment can be used in experiments" ) previous_version = models.ForeignKey( - 'self', - related_name='next_versions', + "self", + related_name="next_versions", null=True, blank=True, on_delete=models.SET_NULL, ) - objects = EnvironmentManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta: - unique_together = ('name', 'version') + unique_together = ("name", "version") - - #_____ Overrides __________ + # _____ Overrides __________ def __str__(self): return self.fullname() - def natural_key(self): return (self.name, self.version) - - #_____ Utilities __________ + # _____ Utilities __________ def fullname(self): - return '%s (%s)' % (self.name, self.version) - + return "%s (%s)" % (self.name, self.version) def get_absolute_url(self): - return reverse( - 'backend:view-environment', - args=(self.name, self.version,), - ) - + return reverse("backend:view-environment", args=(self.name, self.version)) def get_admin_change_url(self): - return reverse('admin:backend_environment_change', args=(self.id,)) - + return reverse("admin:backend_environment_change", args=(self.id,)) def queues_for(self, user): """Returns all queues associated to this environment for which the user has the 'can_access' permission""" - return [q for q in self.queues.all() if user.has_perm('backend.can_access', q)] - + return [q for q in self.queues.all() if user.has_perm("backend.can_access", q)] def as_dict(self): - '''Returns a representation as a dictionary''' + """Returns a representation as a dictionary""" return dict( name=self.name, @@ -149,12 +160,13 @@ class Environment(Shareable): ) -#---------------------------------------------------------- +# ---------------------------------------------------------- class EnvironmentLanguage(models.Model): - environment = models.ForeignKey(Environment, related_name='languages') + environment = models.ForeignKey(Environment, related_name="languages") - language = models.CharField(max_length=1, choices=Code.CODE_LANGUAGE, - default=Code.PYTHON) + language = models.CharField( + max_length=1, choices=Code.CODE_LANGUAGE, default=Code.PYTHON + ) diff --git a/beat/web/backend/serializers.py b/beat/web/backend/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..2de73c3144c1b9a0d1800fd88fc9c5cb567cf99a --- /dev/null +++ b/beat/web/backend/serializers.py @@ -0,0 +1,35 @@ +############################################################################### +# # +# Copyright (c) 2020 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from rest_framework import serializers + +from .models import Environment + + +class EnvironmentInfoSerializer(serializers.ModelSerializer): + class Meta: + model = Environment + fields = ["name", "version", "use_count"] + + use_count = serializers.IntegerField() diff --git a/beat/web/libraries/models.py b/beat/web/libraries/models.py index 6e85a6cc010f5a00529f0f2d62c7158e6ecb9973..685ec3fba5c484b73839d710c95966f551742693 100644 --- a/beat/web/libraries/models.py +++ b/beat/web/libraries/models.py @@ -43,18 +43,20 @@ import simplejson import collections -#---------------------------------------------------------- +# ---------------------------------------------------------- def validate_library(declaration): """Validates the declaration of a library code, returns wrapper""" if not declaration: - raise SyntaxError('Library declaration cannot be empty') + raise SyntaxError("Library declaration cannot be empty") - if not(isinstance(declaration, dict)): + if not (isinstance(declaration, dict)): try: - declaration_dict = simplejson.loads(declaration, object_pairs_hook=collections.OrderedDict) + declaration_dict = simplejson.loads( + declaration, object_pairs_hook=collections.OrderedDict + ) except Exception as e: raise SyntaxError(str(e)) else: @@ -63,111 +65,122 @@ def validate_library(declaration): library = beat.core.library.Library(settings.PREFIX, declaration_dict) if not library.valid: - raise SyntaxError('\n * %s' % '\n * '.join(library.errors)) + raise SyntaxError("\n * %s" % "\n * ".join(library.errors)) return library -#---------------------------------------------------------- +# ---------------------------------------------------------- class LibraryStorage(OverwriteStorage): - def __init__(self, *args, **kwargs): - kwargs['location'] = settings.LIBRARIES_ROOT + kwargs["location"] = settings.LIBRARIES_ROOT super(LibraryStorage, self).__init__(*args, **kwargs) -#---------------------------------------------------------- +# ---------------------------------------------------------- class LibraryManager(CodeManager): - - def create_library(self, author, name, short_description='', - description='', declaration=None, code=None, version=1, - previous_version=None, fork_of=None): + def create_library( + self, + author, + name, + short_description="", + description="", + declaration=None, + code=None, + version=1, + previous_version=None, + fork_of=None, + ): default = beat.core.library.Library(settings.PREFIX, data=None) - return self.create_code(author, name, default, short_description, - description, declaration, code, version, - previous_version, fork_of) + return self.create_code( + author, + name, + default, + short_description, + description, + declaration, + code, + version, + previous_version, + fork_of, + ) -#---------------------------------------------------------- +# ---------------------------------------------------------- class Library(Code): - #_____ Fields __________ + # _____ Fields __________ declaration_file = models.FileField( storage=LibraryStorage(), upload_to=get_contribution_declaration_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='declaration' + db_column="declaration", ) description_file = models.FileField( storage=LibraryStorage(), upload_to=get_contribution_description_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='description' + db_column="description", ) source_code_file = models.FileField( storage=LibraryStorage(), upload_to=get_contribution_source_code_filename, - blank=True, null=True, + blank=True, + null=True, max_length=200, - db_column='source_code' + db_column="source_code", ) # Read-only parameters that are updated at every save(), if required - referenced_libraries = models.ManyToManyField('self', - blank=True, related_name='referencing', - symmetrical=False) + referenced_libraries = models.ManyToManyField( + "self", blank=True, related_name="referencing", symmetrical=False + ) objects = LibraryManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta(Code.Meta): - verbose_name_plural = 'libraries' + verbose_name_plural = "libraries" - - #_____ Utilities __________ + # _____ Utilities __________ def get_absolute_url(self): return reverse( - 'libraries:view', - args=(self.author.username, self.name, self.version,), + "libraries:view", args=(self.author.username, self.name, self.version) ) - def get_api_update_url(self): - '''Returns the endpoint to update this object''' + """Returns the endpoint to update this object""" return reverse( - 'api_libraries:object', - args=(self.author.username, self.name, self.version,), + "api_libraries:object", args=(self.author.username, self.name, self.version) ) - def get_api_share_url(self): - '''Returns the endpoint to share this object''' + """Returns the endpoint to share this object""" return reverse( - 'api_libraries:share', - args=(self.author.username, self.name, self.version,), + "api_libraries:share", args=(self.author.username, self.name, self.version) ) - - #_____ Overrides __________ + # _____ Overrides __________ def save(self, *args, **kwargs): wrapper = self._save_preprocessing() @@ -179,69 +192,54 @@ class Library(Code): if wrapper.uses is not None: for l in set(wrapper.uses.values()): s = beat.core.library.Storage(settings.PREFIX, l) - library = Library.objects.get(author__username=s.username, - name=s.name, - version=s.version, - ) + library = Library.objects.get( + author__username=s.username, name=s.name, version=s.version + ) self.referenced_libraries.add(library) - - #_____ Methods __________ + # _____ Methods __________ def validate(self, declaration): return validate_library(declaration) - def modifiable(self): """Can modify if nobody points at me""" - return super(Library, self).modifiable() and ((self.referencing.count() + self.used_by_algorithms.count()) == 0) - + return super(Library, self).modifiable() and ( + (self.referencing.count() + self.used_by_algorithms.count()) == 0 + ) def deletable(self): """Can delete if nobody points at me""" - return super(Library, self).deletable() and ((self.referencing.count() + self.used_by_algorithms.count()) == 0) - + return super(Library, self).deletable() and ( + (self.referencing.count() + self.used_by_algorithms.count()) == 0 + ) def valid(self): - return True # A library (at least for now) is always implemented in Python, - # thus always valid + return True # A library (at least for now) is always implemented in Python, + # thus always valid def core(self): return validate_library(self.declaration) - def uses(self): return self.core().uses - def environments(self): - '''Calculates environment usage for this library + """Calculates environment usage for this library Returns: - list: mapping environment to usage counts, determining how many times - a given algorithm has been successfuly used on that environment + list: annotated environment with usage counts, determining how many times + a given library used in algorithms has been successfully used on that environment - ''' + """ - from django.db.models import Count, Q from ..experiments.models import Block - from ..experiments.models import Experiment from ..backend.models import Environment # Tries to figure through a maximum if using algorithms have been - # successfuly used inside an environment. - - # Case 1) The block is part of an experiment that was successful - # Case 2) The block is part of an experiment that is not successful - # (failed or other), but it is CACHED (if not cached, then we can't - # attest anything about the algorithm/environment relationship!) + # successfully used inside an environment. - envs = Environment.objects.filter(blocks__in=Block.objects.filter( \ - algorithm__in=self.used_by_algorithms.all()).filter( \ - Q(experiment__status=Experiment.DONE) | \ - ((~Q(experiment__status=Experiment.DONE)) & Q(status=Block.DONE)) - )).annotate(itemcount=Count('id')).order_by('-creation_date' \ - ).distinct() - - return [(k, k.itemcount) for k in envs] + return Environment.objects.use_count( + Block.objects.filter(algorithm__in=self.used_by_algorithms.all()) + ) diff --git a/beat/web/libraries/serializers.py b/beat/web/libraries/serializers.py index 3e2feb817950666e4c1116c984e60d39bd224243..4fac5d1c88da2d6abc049b3ddbde4ddd3401a4d9 100644 --- a/beat/web/libraries/serializers.py +++ b/beat/web/libraries/serializers.py @@ -25,17 +25,19 @@ # # ############################################################################### +import beat.core.library + from rest_framework import serializers from ..code.serializers import CodeSerializer, CodeCreationSerializer -from .models import Library from ..algorithms.models import Algorithm +from ..backend.serializers import EnvironmentInfoSerializer -import beat.core.library +from .models import Library -#---------------------------------------------------------- +# ---------------------------------------------------------- class LibraryCreationSerializer(CodeCreationSerializer): @@ -44,7 +46,7 @@ class LibraryCreationSerializer(CodeCreationSerializer): beat_core_class = beat.core.library -#---------------------------------------------------------- +# ---------------------------------------------------------- class ReferenceSerializer(serializers.ModelSerializer): @@ -53,49 +55,51 @@ class ReferenceSerializer(serializers.ModelSerializer): is_owner = serializers.SerializerMethodField() class Meta: - fields = ['name', 'short_description', 'accessibility', 'is_owner'] + fields = ["name", "short_description", "accessibility", "is_owner"] def get_accessibility(self, obj): return obj.get_sharing_display().lower() def get_is_owner(self, obj): - return (obj.author == self.context.get('user')) + return obj.author == self.context.get("user") -#---------------------------------------------------------- +# ---------------------------------------------------------- class LibraryReferenceSerializer(ReferenceSerializer): - class Meta(ReferenceSerializer.Meta): model = Library -#---------------------------------------------------------- +# ---------------------------------------------------------- class AlgorithmReferenceSerializer(ReferenceSerializer): - class Meta(ReferenceSerializer.Meta): model = Algorithm -#---------------------------------------------------------- +# ---------------------------------------------------------- class LibrarySerializer(CodeSerializer): referenced_libraries = LibraryReferenceSerializer(many=True) - referencing_libraries = LibraryReferenceSerializer(many=True, source='referencing') - referencing_algorithms = AlgorithmReferenceSerializer(many=True, source='used_by_algorithms') + referencing_libraries = LibraryReferenceSerializer(many=True, source="referencing") + referencing_algorithms = AlgorithmReferenceSerializer( + many=True, source="used_by_algorithms" + ) + environments = EnvironmentInfoSerializer(many=True) class Meta(CodeSerializer.Meta): model = Library -#---------------------------------------------------------- +# ---------------------------------------------------------- class FullLibrarySerializer(LibrarySerializer): - class Meta(LibrarySerializer.Meta): - default_fields = LibrarySerializer.Meta.default_fields + LibrarySerializer.Meta.extra_fields + default_fields = ( + LibrarySerializer.Meta.default_fields + LibrarySerializer.Meta.extra_fields + ) diff --git a/beat/web/libraries/templates/libraries/view.html b/beat/web/libraries/templates/libraries/view.html index dc37dfa75f0cc1cf1160a0ddff6b8e25818e15b5..b2fbe2da50fd8a9a4192e7e87a50bc5db6655f29 100644 --- a/beat/web/libraries/templates/libraries/view.html +++ b/beat/web/libraries/templates/libraries/view.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -174,7 +174,7 @@ </div> {% else %} <ul class="list-group"> - {% for key, value in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' key.name key.version %}">{{ key.fullname }}</a> <span class="badge">{{ value }}</span></li>{% endfor %} + {% for info in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' info.name info.version %}">{{ item.fullname }}</a> <span class="badge">{{ item.use_count }}</span></li>{% endfor %} </ul> <p class="help">This table shows the number of times this library has been <b>successfuly</b> used at a given environment. Note diff --git a/beat/web/plotters/templates/plotters/view.html b/beat/web/plotters/templates/plotters/view.html index 260400e042a0ada3e7317cf4e72aff0b86ad07ed..3d080a11c3a4a80bb73f22602f2126e472ab176a 100644 --- a/beat/web/plotters/templates/plotters/view.html +++ b/beat/web/plotters/templates/plotters/view.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -121,23 +121,6 @@ <div role="tabpanel" class="tab-pane" id="history"> {% history 'plotters' plotter 'history' 400 %} </div> - <div role="tabpanel" class="tab-pane" id="compatibility"> - <div class="col-sm-5"> - {% with plotter.environments as execinfo %} - {% if not execinfo %} - This plotter was never executed. - {% else %} - <ul class="list-group"> - {% for key, value in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' key.name key.version %}">{{ key.fullname }}</a> <span class="badge">{{ value }}</span></li>{% endfor %} - </ul> - <p class="help">This table shows the number of times this plotter - has been <b>successfuly</b> run using the given environment. Note - this does not provide sufficient information to evaluate if the - plotter will run when submitted to different conditions.</p> - {% endif %} - {% endwith %} - </div> - </div> </div> </div> diff --git a/common.cfg b/common.cfg index faf2ab9c50a7aa47b1d1d9e2b7087819cdb5e0dd..cc651cee6056e85b79a2352e47ce6999d4167f0e 100644 --- a/common.cfg +++ b/common.cfg @@ -15,7 +15,8 @@ django-rest-swagger = >2.1 django-guardian = >=1.4,<2.0 djangorestframework = >3.7 django-activity-stream = >= 0.6.5 -django-jsonfield = >= 1.0.1 +django-jsonfield = >=1.0.1 +django-post-office = <3.3 jsonfield = <3.0 [scripts]