diff --git a/beat/web/algorithms/templates/algorithms/edition.html b/beat/web/algorithms/templates/algorithms/edition.html index a64bdac14c474bedfda37ee99b0eb1c9e600ab8b..73667155ed846d514853715fddb0a533d598d75d 100644 --- a/beat/web/algorithms/templates/algorithms/edition.html +++ b/beat/web/algorithms/templates/algorithms/edition.html @@ -331,6 +331,7 @@ function setupEditor(algorithm, dataformats, libraries) {% if not edition %} {% if algorithm_version > 1 and not fork_of %} name: '{{ algorithm_name }}', + version: '{{ algorithm_version }}', previous_version:'{{ algorithm_author }}/{{ algorithm_name }}/{{ algorithm_version|add:-1 }}', {% else %} name: $('#algorithm_name')[0].value.trim(), diff --git a/beat/web/algorithms/tests/tests_api.py b/beat/web/algorithms/tests/tests_api.py index 7c7ada18325a898c6a8c6874b542ee4cff1492ce..3c68aa8f43bb226cd06b5f0385f417d77ef4d926 100755 --- a/beat/web/algorithms/tests/tests_api.py +++ b/beat/web/algorithms/tests/tests_api.py @@ -684,7 +684,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1", "description": "blah"}), + json.dumps({"name": "valid-name1", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -710,7 +710,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1"}), + json.dumps({"name": "valid-name1", "version": 1}), content_type="application/json", ) @@ -735,7 +735,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1", "description": "blah"}), + json.dumps({"name": "invalid name1", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -761,7 +761,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1"}), + json.dumps({"name": "invalid name1", "version": 1}), content_type="application/json", ) @@ -789,6 +789,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, @@ -824,6 +825,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, } @@ -857,6 +859,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "invalid name1", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, @@ -892,6 +895,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "invalid name1", + "version": 1, "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, } @@ -922,7 +926,9 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "usable_by_one_user", "description": "blah"}), + json.dumps( + {"name": "usable_by_one_user", "version": 1, "description": "blah"} + ), content_type="application/json", ) @@ -933,7 +939,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "usable_by_one_user"}), + json.dumps({"name": "usable_by_one_user", "version": 1}), content_type="application/json", ) @@ -944,7 +950,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "algorithm 4", "description": "blah"}), + json.dumps({"name": "algorithm 4", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -955,7 +961,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "algorithm 4"}), + json.dumps({"name": "algorithm 4", "version": 1}), content_type="application/json", ) @@ -969,6 +975,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "usable_by_one_user", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, @@ -987,6 +994,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "usable_by_one_user", + "version": 1, "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, } @@ -1004,6 +1012,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "algorithm 4", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, @@ -1022,6 +1031,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "algorithm 4", + "version": 1, "declaration": AlgorithmsAPIBase.DECLARATION, "code": AlgorithmsAPIBase.CODE, } @@ -1039,6 +1049,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.CXX_DECLARATION, } @@ -1085,6 +1096,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "description": "blah", "declaration": AlgorithmsAPIBase.CXX_DECLARATION, "fork_of": "jackdoe/binary_personal/1", @@ -1124,6 +1136,7 @@ class AlgorithmCreation(AlgorithmsAPIBase): json.dumps( { "name": "binary_personal", + "version": 2, "description": "blah", "declaration": AlgorithmsAPIBase.CXX_DECLARATION, "previous_version": "jackdoe/binary_personal/1", diff --git a/beat/web/code/models.py b/beat/web/code/models.py index 2ff0f51e06124ca20dd9c4390cea6098c77250c3..e42994ad9cde44127b1f593315b451ccfc453faa 100755 --- a/beat/web/code/models.py +++ b/beat/web/code/models.py @@ -25,6 +25,8 @@ # # ############################################################################### +from collections import namedtuple + from django.db import models from django.db.models import Q from django.contrib.auth.models import User @@ -33,7 +35,6 @@ from ..common.models import Contribution from ..common.models import StoredContribution from ..common.models import StoredContributionManager from ..common.exceptions import ShareError -from ..common.signals import shared from ..common import storage from ..team.models import Team @@ -45,34 +46,53 @@ import beat.core.hash import simplejson -#---------------------------------------------------------- +# ---------------------------------------------------------- class CodeManager(StoredContributionManager): - - def create_object(self, author, name, short_description='', description='', - declaration=None, code=None, version=1, previous_version=None, - fork_of=None): - - create = getattr(self, 'create_{}'.format(self.model.__name__.lower())) - return create(author=author, name=name, short_description=short_description, - description=description, declaration=declaration, code=code, - version=version, previous_version=previous_version, - fork_of=fork_of) + def create_object( + self, + author, + name, + short_description="", + description="", + declaration=None, + code=None, + version=1, + previous_version=None, + fork_of=None, + ): + + create = getattr(self, "create_{}".format(self.model.__name__.lower())) + return create( + author=author, + name=name, + short_description=short_description, + description=description, + declaration=declaration, + code=code, + version=version, + previous_version=previous_version, + fork_of=fork_of, + ) def for_user(self, user, add_public=False): if user.is_anonymous(): - query = Q(sharing=Code.PUBLIC) |\ - (Q(sharing=Code.USABLE) & (Q(shared_with=None) & Q(shared_with_team=None))) + query = Q(sharing=Code.PUBLIC) | ( + Q(sharing=Code.USABLE) + & (Q(shared_with=None) & Q(shared_with_team=None)) + ) return self.filter(query).distinct() teams = Team.objects.filter(members=user) - query = Q(author=user) |\ - Q(sharing=Code.USABLE) |\ - Q(usable_by=user) |\ - Q(usable_by_team__in=teams) |\ - Q(shared_with=user) |\ - Q(shared_with_team__in=teams) + query = ( + Q(author=user) + | Q(sharing=Code.USABLE) + | Q(usable_by=user) + | Q(usable_by_team__in=teams) + | Q(shared_with=user) + | Q(shared_with_team__in=teams) + ) if add_public: query |= Q(sharing=Code.PUBLIC) @@ -80,8 +100,7 @@ class CodeManager(StoredContributionManager): return self.filter(query).distinct() def for_team(self, team): - return self.filter(Q(usable_by_team=team) | - Q(shared_with_team=team)) + return self.filter(Q(usable_by_team=team) | Q(shared_with_team=team)) def from_author(self, user, author_name, add_public=False): if user.is_anonymous(): @@ -92,28 +111,44 @@ class CodeManager(StoredContributionManager): objects_for_user = self.for_user(asked_user, add_public) else: teams = Team.objects.filter(members=user) - objects_for_user = self.filter(Q(author__username=author_name) & - (Q(sharing=Code.PUBLIC) | - Q(sharing=Code.USABLE) | - Q(usable_by=user) | - Q(usable_by_team__in=teams) | - Q(shared_with=user) | - Q(shared_with_team__in=teams))).distinct() - - return objects_for_user.order_by('author__username', 'name', '-version').select_related() - - def create_code(self, author, name, default, short_description='', - description='', declaration=None, code=None, version=1, - previous_version=None, fork_of=None): + objects_for_user = self.filter( + Q(author__username=author_name) + & ( + Q(sharing=Code.PUBLIC) + | Q(sharing=Code.USABLE) + | Q(usable_by=user) + | Q(usable_by_team__in=teams) + | Q(shared_with=user) + | Q(shared_with_team__in=teams) + ) + ).distinct() + + return objects_for_user.order_by( + "author__username", "name", "-version" + ).select_related() + + def create_code( + self, + author, + name, + default, + short_description="", + description="", + declaration=None, + code=None, + version=1, + previous_version=None, + fork_of=None, + ): # Create the database representation code_db = self.model( - author = author, - name = self.model.sanitize_name(name), - version = version, - sharing = Code.PRIVATE, - previous_version = previous_version, - fork_of = fork_of, + author=author, + name=self.model.sanitize_name(name), + version=version, + sharing=Code.PRIVATE, + previous_version=previous_version, + fork_of=fork_of, ) # Check the provided declaration @@ -124,16 +159,16 @@ class CodeManager(StoredContributionManager): declaration = fork_of.declaration else: declaration = default.data - elif not(isinstance(declaration, dict)): + elif not (isinstance(declaration, dict)): declaration = simplejson.loads(declaration) if len(short_description) > 0: - declaration['description'] = short_description + declaration["description"] = short_description code_db.declaration = declaration # Figure out the language - language = declaration.get('language', 'unknown') + language = declaration.get("language", "unknown") code_db.language = getattr(Code, language.upper()) # Check the provided source code @@ -154,7 +189,7 @@ class CodeManager(StoredContributionManager): elif fork_of is not None: description = fork_of.description else: - description = '' + description = "" code_db.description = description @@ -162,61 +197,66 @@ class CodeManager(StoredContributionManager): code_db.save() except Exception: import traceback + return (None, traceback.format_exc()) return (code_db, None) -#---------------------------------------------------------- +# ---------------------------------------------------------- def get_contribution_source_code_filename(obj, path): return obj.source_code_filename() -#---------------------------------------------------------- +# ---------------------------------------------------------- # Use those function to add a 'source_code' property to a model, by doing: # source_code = property(beat.web.code.models.get_source_code, # beat.web.code.models.set_source_code) + def set_source_code(instance, value): - storage.set_file_content(instance, 'source_code_file', instance.source_code_filename(), value) + storage.set_file_content( + instance, "source_code_file", instance.source_code_filename(), value + ) def get_source_code(instance): - return storage.get_file_content(instance, 'source_code_file') + return storage.get_file_content(instance, "source_code_file") -#---------------------------------------------------------- +# ---------------------------------------------------------- + +AccessibilityInfo = namedtuple( + "Accessibility", ["has_access", "is_opensource", "accessibility"] +) class Code(StoredContribution): - #_____ Constants __________ + # _____ Constants __________ # All possible values should be in sync with beat.core.utils - UNKNOWN = 'U' - CXX = 'C' - MATLAB = 'M' - PYTHON = 'P' - R = 'R' + UNKNOWN = "U" + CXX = "C" + MATLAB = "M" + PYTHON = "P" + R = "R" CODE_LANGUAGE = ( - (UNKNOWN, 'Unknown'), - (CXX, 'Cxx'), - (MATLAB, 'Matlab'), - (PYTHON, 'Python'), - (R, 'R'), + (UNKNOWN, "Unknown"), + (CXX, "Cxx"), + (MATLAB, "Matlab"), + (PYTHON, "Python"), + (R, "R"), ) - CODE_NAMES = { - CXX: 'C++', - } - + CODE_NAMES = {CXX: "C++"} - #_____ Fields __________ + # _____ Fields __________ # For technical reason, it is not possible to declare the required field here. It # must be declared in each subclass of Code, like this: @@ -229,35 +269,34 @@ class Code(StoredContribution): # db_column='source_code' # ) - usable_by = models.ManyToManyField(User, related_name='usable_%(class)ss', - blank=True) + usable_by = models.ManyToManyField( + User, related_name="usable_%(class)ss", blank=True + ) - usable_by_team = models.ManyToManyField(Team, - related_name='usable_%(class)ss', blank=True) + usable_by_team = models.ManyToManyField( + Team, related_name="usable_%(class)ss", blank=True + ) - language = models.CharField(max_length=1, choices=CODE_LANGUAGE, - default=PYTHON) + language = models.CharField(max_length=1, choices=CODE_LANGUAGE, default=PYTHON) objects = CodeManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta(StoredContribution.Meta): abstract = True - - #_____ Methods to implement __________ + # _____ Methods to implement __________ def validate(self, declaration): raise NotImplementedError() - - #_____ Methods __________ + # _____ Methods __________ def source_code_filename(self): - return self.hashed_path(extension_for_language(self.get_language_display().lower())) - + return self.hashed_path( + extension_for_language(self.get_language_display().lower()) + ) def share(self, public, users=None, teams=None): self._share_libraries(public=public, users=users, teams=teams) @@ -271,9 +310,13 @@ class Code(StoredContribution): if users is not None: # Filter out the users that already have this sharing access or better users_with_better_access = [self.author.username] - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) - users = [user for user in users if user not in users_with_better_access] + users = [ + user for user in users if user not in users_with_better_access + ] # Remove the sharing preferences of users that already have a lower sharing access db_users = self.usable_by.filter(username__in=users) @@ -295,7 +338,11 @@ class Code(StoredContribution): # Filter out the teams that already have this sharing access or better teams_with_better_access = self.shared_with_team.all() - db_teams = [team for team in teams if team not in teams_with_better_access] + db_teams = [ + team + for team in teams + if team not in teams_with_better_access + ] # Update the code entry in the database for db_team in db_teams: @@ -303,18 +350,26 @@ class Code(StoredContribution): self.shared_with_team.add(db_team) else: # Filter out the teams that already have this sharing access or better - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] - teams = [team for team in teams if team not in teams_with_better_access] + teams = [ + team + for team in teams + if team not in teams_with_better_access + ] # Update the algorithm entry in the database for team_name in teams: - parts = team_name.split('/') + parts = team_name.split("/") if len(parts) == 1: parts = [self.author.username, team_name] - db_team = Team.objects.filter(owner__username=parts[0], name=parts[1]) + db_team = Team.objects.filter( + owner__username=parts[0], name=parts[1] + ) if len(db_team) == 1: self.usable_by_team.remove(db_team[0]) self.shared_with_team.add(db_team[0]) @@ -329,16 +384,22 @@ class Code(StoredContribution): else: # Check that the request make sense - if not self.sharing in [Contribution.PUBLIC, Contribution.USABLE]: + if self.sharing not in [Contribution.PUBLIC, Contribution.USABLE]: # Usable by some users sharing = None if users is not None: # Filter out the users that already have this sharing access or better users_with_better_access = [self.author.username] - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) - users_with_better_access.extend(self.usable_by.all().values_list('username', flat=True)) + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) + users_with_better_access.extend( + self.usable_by.all().values_list("username", flat=True) + ) - users = [user for user in users if user not in users_with_better_access] + users = [ + user for user in users if user not in users_with_better_access + ] # Update the algorithm entry in the database db_users = User.objects.filter(username__in=users) @@ -354,26 +415,40 @@ class Code(StoredContribution): teams_with_better_access = list(self.shared_with_team.all()) teams_with_better_access.extend(list(self.usable_by_team.all())) - db_teams = [team for team in teams if team not in teams_with_better_access] + db_teams = [ + team + for team in teams + if team not in teams_with_better_access + ] # Update the code entry in the database for db_team in db_teams: self.usable_by_team.add(db_team) else: # Filter out the teams that already have this sharing access or better - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] - teams_with_better_access.extend([team.fullname for team in self.usable_by_team.all()]) - - new_teams = [team for team in teams if team not in teams_with_better_access] + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] + teams_with_better_access.extend( + [team.fullname for team in self.usable_by_team.all()] + ) + + new_teams = [ + team + for team in teams + if team not in teams_with_better_access + ] # Update the algorithm entry in the database for team_name in new_teams: - parts = team_name.split('/') + parts = team_name.split("/") if len(parts) == 1: parts = [self.author.username, team_name] - db_team = Team.objects.filter(owner__username=parts[0], name=parts[1]) + db_team = Team.objects.filter( + owner__username=parts[0], name=parts[1] + ) if len(db_team) == 1: self.usable_by_team.add(db_team[0]) @@ -385,7 +460,6 @@ class Code(StoredContribution): self._update_sharing(sharing, users, teams) - def is_accessible(self, public, users=None, teams=None): errors = [] @@ -393,116 +467,168 @@ class Code(StoredContribution): if self.sharing == Contribution.PUBLIC: return errors - if (users is None and teams is None): - errors.append("The {} '{}' isn't accessible to all".format(self.model_name(), self.fullname())) + if users is None and teams is None: + errors.append( + "The {} '{}' isn't accessible to all".format( + self.model_name(), self.fullname() + ) + ) # Public some teams if users is not None: # Retrieve the users that don't have enough access to the algorithm users_with_better_access = [self.author.username] - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) - missing_users = [user for user in users if user not in users_with_better_access] + missing_users = [ + user for user in users if user not in users_with_better_access + ] if len(missing_users) > 0: - errors.append("The {} '{}' isn't accessible to the following users: {}".format(self.model_name(), self.fullname(), ', '.join(missing_users))) + errors.append( + "The {} '{}' isn't accessible to the following users: {}".format( + self.model_name(), self.fullname(), ", ".join(missing_users) + ) + ) # Accessible to some teams if teams is not None: # Retrieve the teams that don't have enough access to the algorithm - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] - missing_teams = [team for team in teams if team not in teams_with_better_access] + missing_teams = [ + team for team in teams if team not in teams_with_better_access + ] if len(missing_teams) > 0: - errors.append("The {} '{}' isn't accessible to the following teams: {}".format(self.model_name(), self.fullname(), ', '.join(missing_teams))) + errors.append( + "The {} '{}' isn't accessible to the following teams: {}".format( + self.model_name(), self.fullname(), ", ".join(missing_teams) + ) + ) else: if self.sharing in [Contribution.PUBLIC, Contribution.USABLE]: return errors - if (users is None and teams is None): - errors.append("The {} '{}' isn't usable by all".format(self.model_name(), self.fullname())) + if users is None and teams is None: + errors.append( + "The {} '{}' isn't usable by all".format( + self.model_name(), self.fullname() + ) + ) # Usable by some users if users is not None: # Retrieve the users that don't have enough access to the algorithm users_with_better_access = [self.author.username] - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) - users_with_better_access.extend(self.usable_by.all().values_list('username', flat=True)) + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) + users_with_better_access.extend( + self.usable_by.all().values_list("username", flat=True) + ) - missing_users = [user for user in users if user not in users_with_better_access] + missing_users = [ + user for user in users if user not in users_with_better_access + ] if len(missing_users) > 0: - errors.append("The {} '{}' isn't usable by the following users: {}".format(self.model_name(), self.fullname(), ', '.join(missing_users))) + errors.append( + "The {} '{}' isn't usable by the following users: {}".format( + self.model_name(), self.fullname(), ", ".join(missing_users) + ) + ) # Usable by some teams if teams is not None: # Retrieve the teams that don't have enough access to the algorithm - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] - teams_with_better_access.extend([team.fullname() for team in self.usable_by_team.all()]) + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] + teams_with_better_access.extend( + [team.fullname() for team in self.usable_by_team.all()] + ) - missing_teams = [team for team in teams if team not in teams_with_better_access] + missing_teams = [ + team for team in teams if team not in teams_with_better_access + ] if len(missing_teams) > 0: - errors.append("The {} '{}' isn't usable by the following teams: {}".format(self.model_name(), self.fullname(), ', '.join(missing_teams))) + errors.append( + "The {} '{}' isn't usable by the following teams: {}".format( + self.model_name(), self.fullname(), ", ".join(missing_teams) + ) + ) return errors - def sharing_preferences(self): - result = { - 'status': self.get_sharing_display().lower() - } + result = {"status": self.get_sharing_display().lower()} if self.shared_with.count() > 0: - result['shared_with'] = self.shared_with.all().values_list('username', flat=True) + result["shared_with"] = self.shared_with.all().values_list( + "username", flat=True + ) if self.shared_with_team.count() > 0: - result['shared_with_team'] = [team.fullname() for team in self.shared_with_team.all()] + result["shared_with_team"] = [ + team.fullname() for team in self.shared_with_team.all() + ] if self.usable_by.count() > 0: - result['usable_by'] = self.usable_by.all().values_list('username', flat=True) + result["usable_by"] = self.usable_by.all().values_list( + "username", flat=True + ) if self.usable_by_team.count() > 0: - result['usable_by_team'] = [team.fullname() for team in self.usable_by_team.all()] + result["usable_by_team"] = [ + team.fullname() for team in self.usable_by_team.all() + ] return result - def open_source(self, user): - '''Tells if a given user can view the code of this contribution''' + """Tells if a given user can view the code of this contribution""" - if self.author == user: return True + if self.author == user: + return True accessible, open_source, level = self.accessibility_for(user) return open_source - def is_binary(self): return self.language in [Code.CXX] - def language_fullname(self): if self.language in Code.CODE_NAMES: return Code.CODE_NAMES[self.language] - return [language for language in Code.CODE_LANGUAGE if language[0] == self.language][0][1] - + return [ + language for language in Code.CODE_LANGUAGE if language[0] == self.language + ][0][1] def json_language(self): return Code.language_identifier(self.language) - @staticmethod def language_identifier(db_language): - return [language for language in Code.CODE_LANGUAGE if language[0] == db_language][0][1].lower() + return [ + language for language in Code.CODE_LANGUAGE if language[0] == db_language + ][0][1].lower() @staticmethod def language_db(language_identifier): - return [language for language in Code.CODE_LANGUAGE if language[1].lower() == language_identifier][0][0] + return [ + language + for language in Code.CODE_LANGUAGE + if language[1].lower() == language_identifier + ][0][0] - - #_____ Overrides __________ + # _____ Overrides __________ def save(self, *args, **kwargs): @@ -516,77 +642,78 @@ class Code(StoredContribution): elif self.sharing == Code.USABLE: self.usable_by.clear() - - #_____ Properties __________ + # _____ Properties __________ source_code = property(get_source_code, set_source_code) - - #_____ Protected methods __________ + # _____ Protected methods __________ def _save_preprocessing(self): # Make sure the declaration is valid, raises SyntaxError if a problem occurs wrapper = self.validate(self.declaration) # Update the DB entry using the validated declaration - declaration_hash = beat.core.hash.hashJSON(self.declaration, 'description') + declaration_hash = beat.core.hash.hashJSON(self.declaration, "description") code_hash = beat.core.hash.hash(self.source_code) - self.hash = beat.core.hash.hash(dict( - declaration=declaration_hash, - code=code_hash, - )) + self.hash = beat.core.hash.hash( + dict(declaration=declaration_hash, code=code_hash) + ) - self.short_description = wrapper.description if wrapper.description is not None else '' + self.short_description = ( + wrapper.description if wrapper.description is not None else "" + ) return wrapper - def _accessibility_for_user(self, user, without_usable=False): """Returns a tuple (<has_access>, <open_source>, <accessibility>), with <accessibility> being either 'public', 'private', 'confidential' """ if self.author == user: if self.sharing == Contribution.PRIVATE: - return (True, False, 'private') + return AccessibilityInfo(True, False, "private") elif self.sharing == Contribution.PUBLIC: - return (True, True, 'public') + return AccessibilityInfo(True, True, "public") else: - return (True, False, 'confidential') - + return AccessibilityInfo(True, False, "confidential") else: if self.sharing == Contribution.PRIVATE: - return (False, False, None) + return AccessibilityInfo(False, False, None) elif self.sharing == Contribution.PUBLIC: - return (True, True, 'public') + return AccessibilityInfo(True, True, "public") elif not without_usable and (self.sharing == Contribution.USABLE): - return (True, False, 'confidential') + return AccessibilityInfo(True, False, "confidential") elif not user.is_anonymous(): - if self.shared_with.filter(id=user.id).exists() or (self.shared_with_team.filter(members=user).count() > 0): - return (True, True, 'confidential') - elif not without_usable and (self.usable_by.filter(id=user.id).exists() or (self.usable_by_team.filter(members=user).count() > 0)): - return (True, False, 'confidential') - - return (False, False, None) - + if self.shared_with.filter(id=user.id).exists() or ( + self.shared_with_team.filter(members=user).count() > 0 + ): + return AccessibilityInfo(True, True, "confidential") + elif not without_usable and ( + self.usable_by.filter(id=user.id).exists() + or (self.usable_by_team.filter(members=user).count() > 0) + ): + return AccessibilityInfo(True, False, "confidential") + + return AccessibilityInfo(False, False, None) def _accessibility_for_team(self, team, without_usable=False): """Team specific accessibility check """ if self.sharing == Contribution.PRIVATE: - return (False, False, None) + return AccessibilityInfo(False, False, None) elif self.sharing == Contribution.PUBLIC: - return (True, True, 'public') + return AccessibilityInfo(True, True, "public") elif not without_usable and (self.sharing == Contribution.USABLE): - return (True, False, 'confidential') + return AccessibilityInfo(True, False, "confidential") elif self.shared_with_team.filter(id=team.id).exists(): - return (True, True, 'confidential') + return AccessibilityInfo(True, True, "confidential") elif not without_usable and self.usable_by_team.filter(id=team.id).exists(): - return (True, False, 'confidential') + return AccessibilityInfo(True, False, "confidential") - return (False, False, None) + return AccessibilityInfo(False, False, None) - #_____ Protected methods __________ + # _____ Protected methods __________ def _share_libraries(self, public, users, teams): # Retrieve and process the list of referenced libraries @@ -598,7 +725,9 @@ class Code(StoredContribution): # preferences errors = [] for needed_library in other_needed_libraries: - errors.extend(needed_library.is_accessible(public=public, users=users, teams=teams)) + errors.extend( + needed_library.is_accessible(public=public, users=users, teams=teams) + ) if len(errors) > 0: raise ShareError(errors) diff --git a/beat/web/common/models.py b/beat/web/common/models.py index 324a2f72409ca943bd60ff2cdc60d6cd5b27b1d4..590670c6031c65e76a40bc571df577818938eb79 100755 --- a/beat/web/common/models.py +++ b/beat/web/common/models.py @@ -25,6 +25,8 @@ # # ############################################################################### +from collections import namedtuple + from django.db import models from django.conf import settings from django.db.models import Q @@ -49,7 +51,7 @@ import simplejson from collections import OrderedDict -#---------------------------------------------------------- +# ---------------------------------------------------------- class ShareableManager(models.Manager): @@ -57,11 +59,12 @@ class ShareableManager(models.Manager): if user.is_anonymous(): return self.public() - query = Q(sharing=Shareable.SHARED) &\ - (Q(shared_with=user) |\ - Q(shared_with_team__in=Team.objects.filter(members=user))) + query = Q(sharing=Shareable.SHARED) & ( + Q(shared_with=user) + | Q(shared_with_team__in=Team.objects.filter(members=user)) + ) - if hasattr(self.model, 'author'): + if hasattr(self.model, "author"): query |= Q(author=user) if add_public: @@ -73,57 +76,57 @@ class ShareableManager(models.Manager): return self.filter(sharing=Shareable.PUBLIC) -#---------------------------------------------------------- +# ---------------------------------------------------------- + + +AccessibilityInfo = namedtuple("Accessibility", ["has_access", "accessibility"]) class Shareable(models.Model): - #_____ Constants __________ + # _____ Constants __________ - PRIVATE = 'P' - SHARED = 'S' - PUBLIC = 'A' - USABLE = 'U' + PRIVATE = "P" + SHARED = "S" + PUBLIC = "A" + USABLE = "U" SHARING_STATUS = ( - (PRIVATE, 'Private'), - (SHARED, 'Shared'), - (PUBLIC, 'Public'), - (USABLE, 'Usable'), # Not applicable for all contribution types, must be - # checked for each case! + (PRIVATE, "Private"), + (SHARED, "Shared"), + (PUBLIC, "Public"), + (USABLE, "Usable"), # Not applicable for all contribution types, must be + # checked for each case! ) + # _____ Fields __________ - #_____ Fields __________ - - sharing = models.CharField(max_length=1, choices=SHARING_STATUS, default=PRIVATE) - shared_with = models.ManyToManyField(User, related_name='shared_%(class)ss', - blank=True) + sharing = models.CharField(max_length=1, choices=SHARING_STATUS, default=PRIVATE) + shared_with = models.ManyToManyField( + User, related_name="shared_%(class)ss", blank=True + ) - shared_with_team = models.ManyToManyField(Team, - related_name='shared_%(class)ss', blank=True) + shared_with_team = models.ManyToManyField( + Team, related_name="shared_%(class)ss", blank=True + ) objects = ShareableManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta: abstract = True - - #_____ Utilities __________ + # _____ Utilities __________ def model_name(self): return type(self).__name__.lower() - - #_____ Overrides __________ + # _____ Overrides __________ def __str__(self): return self.fullname() - def save(self, *args, **kwargs): super(Shareable, self).save(*args, **kwargs) @@ -132,9 +135,7 @@ class Shareable(models.Model): if self.sharing == Shareable.PUBLIC: self.shared_with.clear() - - #_____ Methods __________ - + # _____ Methods __________ def get_verbose_name(self): return self._meta.verbose_name @@ -143,13 +144,13 @@ class Shareable(models.Model): return self._meta.verbose_name_plural def modifiable(self): - if hasattr(self, 'attestations'): - return (self.attestations.count() == 0) + if hasattr(self, "attestations"): + return self.attestations.count() == 0 return True def deletable(self): - if hasattr(self, 'attestations'): - return (self.attestations.count() == 0) + if hasattr(self, "attestations"): + return self.attestations.count() == 0 return True def accessibility_for(self, user_or_team, without_usable=False): @@ -173,30 +174,55 @@ class Shareable(models.Model): if users is not None: # Retrieve the users that don't have enough access to the algorithm users_with_better_access = [] - if hasattr(self, 'author'): + if hasattr(self, "author"): users_with_better_access.append(self.author.username) - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) - users_with_better_access.extend([user for user in users if self.shared_with_team.filter(members__username=user).exists()]) - - missing_users = [user for user in users if user not in users_with_better_access] + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) + users_with_better_access.extend( + [ + user + for user in users + if self.shared_with_team.filter(members__username=user).exists() + ] + ) + + missing_users = [ + user for user in users if user not in users_with_better_access + ] if len(missing_users) > 0: - errors.append("The {0} '{1}' isn't accessible to the following users: {2}".format(self.model_name(), self.fullname(), ', '.join(missing_users))) - + errors.append( + "The {0} '{1}' isn't accessible to the following users: {2}".format( + self.model_name(), self.fullname(), ", ".join(missing_users) + ) + ) if teams is not None: # Retrieve the teams that don't have enough access to the algorithm - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] - missing_teams = [team for team in teams if team not in teams_with_better_access] + missing_teams = [ + team for team in teams if team not in teams_with_better_access + ] if len(missing_teams) > 0: - errors.append("The {0} '{1}' isn't accessible to the following teams: {2}".format(self.model_name(), self.fullname(), ', '.join(missing_teams))) + errors.append( + "The {0} '{1}' isn't accessible to the following teams: {2}".format( + self.model_name(), self.fullname(), ", ".join(missing_teams) + ) + ) # Accessible to everybody if users is None and teams is None: if self.sharing != Shareable.PUBLIC: - errors.append("The {0} '{1}' isn't Accessible to all".format(self.model_name(), self.fullname())) + errors.append( + "The {0} '{1}' isn't Accessible to all".format( + self.model_name(), self.fullname() + ) + ) return errors @@ -210,9 +236,11 @@ class Shareable(models.Model): if users is not None: # Filter out the users that already have this sharing access or better users_with_better_access = [] - if hasattr(self, 'author'): + if hasattr(self, "author"): users_with_better_access.append(self.author.username) - users_with_better_access.extend(self.shared_with.all().values_list('username', flat=True)) + users_with_better_access.extend( + self.shared_with.all().values_list("username", flat=True) + ) users = [user for user in users if user not in users_with_better_access] @@ -230,25 +258,31 @@ class Shareable(models.Model): # Filter out the teams that already have this sharing access or better teams_with_better_access = self.shared_with_team.all() - db_teams = [team for team in teams if team not in teams_with_better_access] + db_teams = [ + team for team in teams if team not in teams_with_better_access + ] # Update the database entry for db_team in db_teams: self.shared_with_team.add(db_team) else: # Filter out the teams that already have this sharing access or better - teams_with_better_access = [team.fullname() for team in self.shared_with_team.all()] + teams_with_better_access = [ + team.fullname() for team in self.shared_with_team.all() + ] teams = [team for team in teams if team not in teams_with_better_access] # Update the database entry for team_name in teams: - parts = team_name.split('/') + parts = team_name.split("/") if len(parts) == 1: parts = [self.author.username, team_name] - db_team = Team.objects.filter(owner__username=parts[0], name=parts[1]) + db_team = Team.objects.filter( + owner__username=parts[0], name=parts[1] + ) if len(db_team) == 1: self.shared_with_team.add(db_team[0]) @@ -263,73 +297,86 @@ class Shareable(models.Model): self._update_sharing(sharing, users, teams) def sharing_preferences(self): - result = { - 'status': self.get_sharing_display().lower() - } + result = {"status": self.get_sharing_display().lower()} if self.shared_with.count() > 0: - result['shared_with'] = self.shared_with.all().values_list('username', flat=True) + result["shared_with"] = self.shared_with.all().values_list( + "username", flat=True + ) if self.shared_with_team.count() > 0: - result['shared_with_team'] = [team.fullname() for team in self.shared_with_team.all()] + result["shared_with_team"] = [ + team.fullname() for team in self.shared_with_team.all() + ] return result def all_shared_with_users(self): - '''Returns a list of users this object is shared with''' + """Returns a list of users this object is shared with""" - users = set(self.shared_with.exclude(username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS).distinct()) - teams = set(User.objects.filter(teams__in=self.shared_with_team.all()).exclude(username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS)) + users = set( + self.shared_with.exclude( + username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS + ).distinct() + ) + teams = set( + User.objects.filter(teams__in=self.shared_with_team.all()).exclude( + username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS + ) + ) return users | teams def users_with_access(self): - '''Returns a set of users that have access to this environment''' + """Returns a set of users that have access to this environment""" if self.sharing == Shareable.PUBLIC: - return set(User.objects.filter(is_active=True).exclude(username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS).distinct()) + return set( + User.objects.filter(is_active=True) + .exclude(username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS) + .distinct() + ) elif self.sharing == Shareable.SHARED: return self.all_shared_with_users() return set() - - #_____ Protected Methods __________ + # _____ Protected Methods __________ def _accessibility_for_user(self, user, without_usable=False): """User specific accessibility check """ - if hasattr(self, 'author') and self.author == user: + if hasattr(self, "author") and self.author == user: if self.sharing == Shareable.PRIVATE: - return (True, 'private') + return AccessibilityInfo(True, "private") elif self.sharing == Shareable.PUBLIC: - return (True, 'public') + return AccessibilityInfo(True, "public") else: - return (True, 'confidential') - + return AccessibilityInfo(True, "confidential") else: if self.sharing == Shareable.PRIVATE: - return (False, None) + return AccessibilityInfo(False, None) elif self.sharing == Shareable.PUBLIC: - return (True, 'public') + return AccessibilityInfo(True, "public") elif not user.is_anonymous(): - if self.shared_with.filter(id=user.id).exists() or (self.shared_with_team.filter(members=user).count() > 0): - return (True, 'confidential') + if self.shared_with.filter(id=user.id).exists() or ( + self.shared_with_team.filter(members=user).count() > 0 + ): + return AccessibilityInfo(True, "confidential") - return (False, None) + return AccessibilityInfo(False, None) def _accessibility_for_team(self, team, without_usable=False): """Team specific accessibility check """ if self.sharing == Shareable.PRIVATE: - return (False, None) + return AccessibilityInfo(False, None) elif self.sharing == Shareable.PUBLIC: - return (True, 'public') + return AccessibilityInfo(True, "public") elif self.shared_with_team.filter(id=team.id).exists(): - return (True, 'confidential') - - return (False, None) + return AccessibilityInfo(True, "confidential") + return AccessibilityInfo(False, None) def _update_sharing(self, sharing, users, teams): if sharing != self.sharing: @@ -339,81 +386,73 @@ class Shareable(models.Model): shared.send(sender=self, users=users, teams=teams) -#---------------------------------------------------------- +# ---------------------------------------------------------- class VersionableManager(ShareableManager): - def is_last_version(self, object): - return not self.filter(name=object.name, - version__gt=object.version).exists() + return not self.filter(name=object.name, version__gt=object.version).exists() -#---------------------------------------------------------- +# ---------------------------------------------------------- class Versionable(Shareable): - #_____ Fields __________ + # _____ Fields __________ - name = models.CharField(max_length=200, help_text=Messages['name'], blank=False) - version = models.PositiveIntegerField(default=1, help_text=Messages['version']) - short_description = models.CharField(max_length=100, default='', blank=True, help_text=Messages['short_description']) - creation_date = models.DateTimeField('Creation date', auto_now_add=True) - hash = models.CharField(max_length=64, editable=False, help_text=Messages['hash']) - - previous_version = models.ForeignKey('self', - related_name='next_versions', - null=True, - blank=True, - ) + name = models.CharField(max_length=200, help_text=Messages["name"], blank=False) + version = models.PositiveIntegerField(default=1, help_text=Messages["version"]) + short_description = models.CharField( + max_length=100, default="", blank=True, help_text=Messages["short_description"] + ) + creation_date = models.DateTimeField("Creation date", auto_now_add=True) + hash = models.CharField(max_length=64, editable=False, help_text=Messages["hash"]) - fork_of = models.ForeignKey('self', - related_name='forks', - null=True, - blank=True, - ) + previous_version = models.ForeignKey( + "self", related_name="next_versions", null=True, blank=True + ) + fork_of = models.ForeignKey("self", related_name="forks", null=True, blank=True) objects = VersionableManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta(Shareable.Meta): abstract = True - ordering = ['name', '-version'] - + ordering = ["name", "-version"] - #_____ Static Methods __________ + # _____ Static Methods __________ @staticmethod def sanitize_name(name): """Makes sure that the name is valid""" - return re.sub(r'[^\x00-\x7f]|\W', r'-', name) - - + return re.sub(r"[^\x00-\x7f]|\W", r"-", name) @staticmethod def filter_latest_versions(versionables): result = [] for versionable in versionables: try: - entry = [item for item in result if item.fullname().startswith('%s' % versionable.name)][0] + entry = [ + item + for item in result + if item.fullname().startswith("%s" % versionable.name) + ][0] if entry.version >= versionable.version: continue else: result.remove(entry) - except: + except IndexError: pass result.append(versionable) return result - - #_____ Overrides __________ + # _____ Overrides __________ def delete(self, *args, **kwargs): for next_version in self.next_versions.iterator(): @@ -427,7 +466,9 @@ class Versionable(Shareable): super(Versionable, self).delete(*args, **kwargs) def modifiable(self): - return super(Versionable, self).modifiable() and (self.next_versions.count() == 0) + return super(Versionable, self).modifiable() and ( + self.next_versions.count() == 0 + ) def history(self, for_user): """Calculates its own history and returns it in a dictionary. @@ -441,81 +482,88 @@ class Versionable(Shareable): """Recursive function to build the history starting from a leaf""" # First retrieve all accessible versions of the versionable - if hasattr(obj, 'author'): - versions = self.__class__.objects.for_user(for_user, True).filter(author=obj.author, name=obj.name).order_by('version') + if hasattr(obj, "author"): + versions = ( + self.__class__.objects.for_user(for_user, True) + .filter(author=obj.author, name=obj.name) + .order_by("version") + ) else: - versions = self.__class__.objects.for_user(for_user, True).filter(name=obj.name).order_by('version') + versions = ( + self.__class__.objects.for_user(for_user, True) + .filter(name=obj.name) + .order_by("version") + ) if versions.count() == 0: return None # Next search for the very first version of the history first_version = versions[0] - if not(forward_only) and (first_version.fork_of is not None): + if not (forward_only) and (first_version.fork_of is not None): history = _process(first_version.fork_of, forward_only=False) if history is not None: return history # Construct a tree from the first version - history = {'object': first_version, 'next': []} + history = {"object": first_version, "next": []} for fork in first_version.forks.iterator(): fork_history = _process(fork) if fork_history is not None: - history['next'].append(fork_history) + history["next"].append(fork_history) previous_version = history for version in versions[1:]: - version_dict = {'object': version, 'next': []} + version_dict = {"object": version, "next": []} for fork in version.forks.iterator(): fork_history = _process(fork) if fork_history is not None: - version_dict['next'].append(fork_history) + version_dict["next"].append(fork_history) - previous_version['next'].append(version_dict) + previous_version["next"].append(version_dict) previous_version = version_dict return history return _process(self, forward_only=False) - def api_history(self, for_user): """The same as history(), but providing an implementation compatible with our V1 API""" def _recurse(d): - o = d['object'] - d['name'] = o.fullname() - d['creation_date'] = o.creation_date.isoformat(' ') - d['author_gravatar'] = gravatar_url(o.author.email) if hasattr(o, 'author') else None - del d['object'] - d['next'] = [_recurse(k) for k in d['next']] + o = d["object"] + d["name"] = o.fullname() + d["creation_date"] = o.creation_date.isoformat(" ") + d["author_gravatar"] = ( + gravatar_url(o.author.email) if hasattr(o, "author") else None + ) + del d["object"] + d["next"] = [_recurse(k) for k in d["next"]] return d return _recurse(self.history(for_user)) - def json_history(self, for_user): """The same as API history, but serializes the result into a JSON""" return simplejson.dumps(self.api_history(for_user)) -#---------------------------------------------------------- +# ---------------------------------------------------------- class ContributionManager(VersionableManager): - def get_by_natural_key(self, username, name, version): return self.get(author__username=username, name=name, version=version) def is_last_version(self, object): - return not self.filter(author=object.author, - name=object.name, - version__gt=object.version).exists() + return not self.filter( + author=object.author, name=object.name, version__gt=object.version + ).exists() def from_author(self, user, author_name, add_public=False): if user.is_anonymous(): @@ -525,65 +573,74 @@ class ContributionManager(VersionableManager): objects_for_user = self.for_user(user, add_public) else: teams = Team.objects.filter(members=user) - objects_for_user = self.filter(Q(author__username=author_name) & - (Q(sharing=Contribution.PUBLIC)| - Q(shared_with=user) | - Q(shared_with_team__in=teams))).distinct() - - return objects_for_user.order_by('author__username', 'name', '-version').select_related() + objects_for_user = self.filter( + Q(author__username=author_name) + & ( + Q(sharing=Contribution.PUBLIC) + | Q(shared_with=user) + | Q(shared_with_team__in=teams) + ) + ).distinct() + + return objects_for_user.order_by( + "author__username", "name", "-version" + ).select_related() def from_author_and_public(self, user, author_name): return self.from_author(user, author_name, True) -#---------------------------------------------------------- +# ---------------------------------------------------------- class Contribution(Versionable): - #_____ Fields __________ + # _____ Fields __________ - author = models.ForeignKey(User, related_name='%(class)ss', - on_delete=models.CASCADE) + author = models.ForeignKey( + User, related_name="%(class)ss", on_delete=models.CASCADE + ) objects = ContributionManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta(Versionable.Meta): abstract = True - ordering = ['author__username', 'name', 'version'] - unique_together = ('author', 'name', 'version') + ordering = ["author__username", "name", "version"] + unique_together = ("author", "name", "version") - - #_____ Utilities __________ + # _____ Utilities __________ def natural_key(self): return (self.author.username, self.name, self.version) - - #_____ Methods __________ + # _____ Methods __________ def fullname(self): - return '%s/%s/%d' % (self.author.username, self.name, self.version) + return "%s/%s/%d" % (self.author.username, self.name, self.version) - - #_____ Static Methods __________ + # _____ Static Methods __________ @staticmethod def filter_latest_versions(contributions): result = [] for contribution in contributions: try: - entry = [item for item in result if item.fullname().startswith('%s/%s/' % (contribution.author.username, contribution.name))][0] + entry = [ + item + for item in result + if item.fullname().startswith( + "%s/%s/" % (contribution.author.username, contribution.name) + ) + ][0] if entry.version >= contribution.version: continue else: result.remove(entry) - except: + except IndexError: pass result.append(contribution) @@ -591,23 +648,37 @@ class Contribution(Versionable): return result -#---------------------------------------------------------- +# ---------------------------------------------------------- class StoredContributionManager(ContributionManager): - - def create_object(self, author, name, short_description='', description='', - declaration=None, version=1, previous_version=None, - fork_of=None): - - create = getattr(self, 'create_{}'.format(self.model.__name__.lower())) - - return create(author=author, name=name, short_description=short_description, - description=description, declaration=declaration, version=version, - previous_version=previous_version, fork_of=fork_of) + def create_object( + self, + author, + name, + short_description="", + description="", + declaration=None, + version=1, + previous_version=None, + fork_of=None, + ): + + create = getattr(self, "create_{}".format(self.model.__name__.lower())) + + return create( + author=author, + name=name, + short_description=short_description, + description=description, + declaration=declaration, + version=version, + previous_version=previous_version, + fork_of=fork_of, + ) -#---------------------------------------------------------- +# ---------------------------------------------------------- def get_contribution_declaration_filename(obj, path): @@ -618,7 +689,7 @@ def get_contribution_description_filename(obj, path): return obj.description_filename() -#---------------------------------------------------------- +# ---------------------------------------------------------- # Use those function to add a 'declaration' and a 'declaration_string' property to a @@ -627,45 +698,52 @@ def get_contribution_description_filename(obj, path): # beat.web.common.models.set_declaration) # declaration_string = property(beat.web.common.models.get_declaration_string) + def set_declaration(instance, value): if isinstance(value, dict): - value = simplejson.dumps(value, - indent=4, - cls=NumpyJSONEncoder) + value = simplejson.dumps(value, indent=4, cls=NumpyJSONEncoder) - storage.set_file_content(instance, 'declaration_file', instance.declaration_filename(), value) + storage.set_file_content( + instance, "declaration_file", instance.declaration_filename(), value + ) def get_declaration(instance): - return simplejson.loads(storage.get_file_content(instance, 'declaration_file'), object_pairs_hook=OrderedDict) + return simplejson.loads( + storage.get_file_content(instance, "declaration_file"), + object_pairs_hook=OrderedDict, + ) def get_declaration_string(instance): - data = storage.get_file_content(instance, 'declaration_file') + data = storage.get_file_content(instance, "declaration_file") return ensure_string(data) -#---------------------------------------------------------- +# ---------------------------------------------------------- # Use those function to add a 'description' property to a model, by doing: # description = property(beat.web.common.models.get_description, # beat.web.common.models.set_description) + def set_description(instance, value): - storage.set_file_content(instance, 'description_file', instance.description_filename(), value) + storage.set_file_content( + instance, "description_file", instance.description_filename(), value + ) def get_description(instance): - return storage.get_file_content(instance, 'description_file') + return storage.get_file_content(instance, "description_file") -#---------------------------------------------------------- +# ---------------------------------------------------------- class StoredContribution(Contribution): - #_____ Fields __________ + # _____ Fields __________ # For technical reason, it is not possible to declare the required fields here. They # must be declared in each subclass of StoredContribution, like this: @@ -688,16 +766,14 @@ class StoredContribution(Contribution): objects = StoredContributionManager() - - #_____ Meta parameters __________ + # _____ Meta parameters __________ class Meta(Contribution.Meta): abstract = True + # _____ Methods __________ - #_____ Methods __________ - - def hashed_path(self, extension=''): + def hashed_path(self, extension=""): """Relative path of a file belonging to the object on the respective storage""" @@ -707,18 +783,15 @@ class StoredContribution(Contribution): str(self.version) + extension, ) - def declaration_filename(self): """Relative path of the declaration file on the storage""" - return self.hashed_path('.json') - + return self.hashed_path(".json") def description_filename(self): """Relative path of the description file on the storage""" - return self.hashed_path('.rst') - + return self.hashed_path(".rst") - #_____ Overrides __________ + # _____ Overrides __________ def save(self, *args, **kwargs): @@ -728,8 +801,7 @@ class StoredContribution(Contribution): # Invoke the base implementation super(StoredContribution, self).save(*args, **kwargs) - - #_____ Properties __________ + # _____ Properties __________ description = property(get_description, set_description) declaration = property(get_declaration, set_declaration) diff --git a/beat/web/common/serializers.py b/beat/web/common/serializers.py index d3e6b3bcaf6b0e6b5c79b910e884bd6aee2db0c9..05344e4daf42551e3d9abad94887b866db62d9bb 100644 --- a/beat/web/common/serializers.py +++ b/beat/web/common/serializers.py @@ -25,17 +25,17 @@ # # ############################################################################### -from django.conf import settings -from django.core.exceptions import ValidationError from django.contrib.auth.models import User from django.utils import six +from django.db.models import CharField, Value as V +from django.db.models.functions import Concat + from rest_framework import serializers from ..team.models import Team -from ..ui.templatetags.gravatar import gravatar_url -from ..common.utils import validate_restructuredtext, ensure_html +from ..common.utils import ensure_html from .models import Shareable, Versionable, Contribution from .exceptions import ContributionCreationError @@ -44,24 +44,22 @@ from .fields import JSONSerializerField, StringListField import simplejson as json import difflib -import ast - -#---------------------------------------------------------- +# ---------------------------------------------------------- class DiffSerializer(serializers.Serializer): diff = serializers.SerializerMethodField() def get_diff(self, obj): - source1 = json.dumps(obj['object1'].declaration, indent=4) - source2 = json.dumps(obj['object2'].declaration, indent=4) + source1 = json.dumps(obj["object1"].declaration, indent=4) + source2 = json.dumps(obj["object2"].declaration, indent=4) diff = difflib.ndiff(source1.splitlines(), source2.splitlines()) - return '\n'.join(filter(lambda x: x[0] != '?', list(diff))) + return "\n".join(filter(lambda x: x[0] != "?", list(diff))) -#---------------------------------------------------------- +# ---------------------------------------------------------- class CheckNameSerializer(serializers.Serializer): @@ -72,16 +70,16 @@ class CheckNameSerializer(serializers.Serializer): return Contribution.sanitize_name(name) def get_used(self, obj): - name = obj.get('name') - model = self.context.get('model') - user = self.context.get('user') + name = obj.get("name") + model = self.context.get("model") + user = self.context.get("user") return model.objects.filter(author=user, name=name).exists() def create(self, validated_data): return validated_data -#---------------------------------------------------------- +# ---------------------------------------------------------- class SharingSerializer(serializers.Serializer): @@ -89,22 +87,28 @@ class SharingSerializer(serializers.Serializer): teams = StringListField(required=False) def validate_users(self, users): - user_accounts = User.objects.filter(username__in=users).values_list('username', flat=True) + user_accounts = User.objects.filter(username__in=users).values_list( + "username", flat=True + ) if len(users) != user_accounts.count(): unknown_users = [user for user in users if user not in user_accounts] if len(unknown_users) > 1: - raise serializers.ValidationError(['Unknown usernames: ' + ', '.join(unknown_users)]) + raise serializers.ValidationError( + ["Unknown usernames: " + ", ".join(unknown_users)] + ) else: - raise serializers.ValidationError(['Unknown username: ' + unknown_users[0]]) + raise serializers.ValidationError( + ["Unknown username: " + unknown_users[0]] + ) return users def validate_teams(self, teams): unknown_teams = [] - user = self.context.get('user') + user = self.context.get("user") for team_name in teams: - parts = team_name.split('/') + parts = team_name.split("/") if len(parts) > 2: unknown_teams.append(team_name) @@ -118,14 +122,16 @@ class SharingSerializer(serializers.Serializer): unknown_teams.append(team_name) if len(unknown_teams) > 1: - raise serializers.ValidationError('Unknown teams: ' + ', '.join(unknown_teams)) + raise serializers.ValidationError( + "Unknown teams: " + ", ".join(unknown_teams) + ) elif len(unknown_teams) == 1: - raise serializers.ValidationError('Unknown team: ' + unknown_teams[0]) + raise serializers.ValidationError("Unknown team: " + unknown_teams[0]) return teams -#---------------------------------------------------------- +# ---------------------------------------------------------- class DynamicFieldsSerializer(serializers.ModelSerializer): @@ -134,7 +140,7 @@ class DynamicFieldsSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): # Don't pass the 'fields' arg up to the superclass - fields = kwargs.pop('fields', self.Meta.default_fields) + fields = kwargs.pop("fields", self.Meta.default_fields) # Instantiate the superclass normally super(DynamicFieldsSerializer, self).__init__(*args, **kwargs) @@ -146,7 +152,7 @@ class DynamicFieldsSerializer(serializers.ModelSerializer): self.fields.pop(field_name) -#---------------------------------------------------------- +# ---------------------------------------------------------- class ShareableSerializer(DynamicFieldsSerializer): @@ -158,43 +164,50 @@ class ShareableSerializer(DynamicFieldsSerializer): class Meta(DynamicFieldsSerializer.Meta): model = Shareable - default_fields = DynamicFieldsSerializer.Meta.default_fields + \ - ['is_owner', 'modifiable', 'deletable', 'sharing'] + default_fields = DynamicFieldsSerializer.Meta.default_fields + [ + "is_owner", + "modifiable", + "deletable", + "sharing", + ] def get_accessibility(self, obj): if obj.sharing == Versionable.PUBLIC: - return 'public' + return "public" elif obj.sharing == Versionable.SHARED or obj.sharing == Versionable.USABLE: - return 'confidential' + return "confidential" else: - return 'private' - + return "private" def get_sharing(self, obj): - user = self.context.get('user') + user = self.context.get("user") sharing = None - if hasattr(obj, 'author') and user == obj.author: - sharing = {'status': obj.get_sharing_display().lower()} + if hasattr(obj, "author") and user == obj.author: + sharing = {"status": obj.get_sharing_display().lower()} if obj.shared_with.count() > 0: - sharing['shared_with'] = [user.username for user in obj.shared_with.all()] + sharing["shared_with"] = [ + user.username for user in obj.shared_with.all() + ] if obj.shared_with_team.count() > 0: - sharing['shared_with_team'] = [team.fullname() for team in obj.shared_with_team.all()] + sharing["shared_with_team"] = [ + team.fullname() for team in obj.shared_with_team.all() + ] return sharing def get_is_owner(self, obj): - if hasattr(obj, 'author'): - return obj.author == self.context.get('user') + if hasattr(obj, "author"): + return obj.author == self.context.get("user") return False -#---------------------------------------------------------- +# ---------------------------------------------------------- class VersionableSerializer(ShareableSerializer): - name = serializers.CharField(source='fullname') + name = serializers.CharField(source="fullname") fork_of = serializers.SerializerMethodField() last_version = serializers.SerializerMethodField() previous_version = serializers.SerializerMethodField() @@ -205,28 +218,33 @@ class VersionableSerializer(ShareableSerializer): class Meta(ShareableSerializer.Meta): model = Versionable - default_fields = ShareableSerializer.Meta.default_fields + ['name', 'version', 'last_version', - 'short_description', - 'fork_of', 'previous_version', - 'accessibility', - 'hash', - 'creation_date'] + default_fields = ShareableSerializer.Meta.default_fields + [ + "name", + "version", + "last_version", + "short_description", + "fork_of", + "previous_version", + "accessibility", + "hash", + "creation_date", + ] def get_fork_of(self, obj): - if not(obj.fork_of): + if not (obj.fork_of): return None - accessibility_infos = obj.fork_of.accessibility_for(self.context.get('user')) + accessibility_infos = obj.fork_of.accessibility_for(self.context.get("user")) return obj.fork_of.fullname() if accessibility_infos[0] else None def get_last_version(self, obj): return self.Meta.model.objects.is_last_version(obj) def get_previous_version(self, obj): - if not(obj.previous_version): + if not (obj.previous_version): return None - user = self.context.get('user') + user = self.context.get("user") previous_version = obj.previous_version while previous_version is not None: @@ -240,10 +258,10 @@ class VersionableSerializer(ShareableSerializer): def get_history(self, obj): - return obj.api_history(self.context.get('user')) + return obj.api_history(self.context.get("user")) -#---------------------------------------------------------- +# ---------------------------------------------------------- class ContributionSerializer(VersionableSerializer): @@ -253,18 +271,18 @@ class ContributionSerializer(VersionableSerializer): class Meta(VersionableSerializer.Meta): model = Contribution - extra_fields = ['description', 'declaration'] - exclude = ['description_file', 'declaration_file'] + extra_fields = ["description", "declaration"] + exclude = ["description_file", "declaration_file"] def get_description(self, obj): result = obj.description if six.PY2: - result = result.decode('utf-8') + result = result.decode("utf-8") return result def get_declaration(self, obj): - object_format = self.context.get('object_format') - if object_format == 'string': + object_format = self.context.get("object_format") + if object_format == "string": return json.dumps(obj.declaration, indent=4) else: return obj.declaration @@ -273,39 +291,10 @@ class ContributionSerializer(VersionableSerializer): description = obj.description if len(description) > 0: return ensure_html(description) - return '' - - -#---------------------------------------------------------- - -class MapDot(dict): - def __init__(self, *args, **kwargs): - super(MapDot, self).__init__(*args, **kwargs) - for arg in args: - if isinstance(arg, dict): - for k, v in arg.items(): - self[k] = v - - if kwargs: - for k, v in kwargs.items(): - self[k] = v - - def __getattr__(self, attr): - return self.get(attr) - - def __setattr__(self, key, value): - self.__setitem__(key, value) - - def __setitem__(self, key, value): - super(MapDot, self).__setitem__(key, value) - self.__dict__.update({key: value}) + return "" - def __delattr__(self, item): - self.__delitem__(item) - def __delitem__(self, key): - super(MapDot, self).__delitem__(key) - del self.__dict__[key] +# ---------------------------------------------------------- class ContributionCreationSerializer(serializers.ModelSerializer): @@ -313,101 +302,122 @@ class ContributionCreationSerializer(serializers.ModelSerializer): description = serializers.CharField(required=False, allow_blank=True) fork_of = serializers.JSONField(required=False) previous_version = serializers.CharField(required=False) + version = serializers.IntegerField(min_value=1) class Meta: - fields = ['name', 'short_description', 'description', - 'declaration', 'previous_version', - 'fork_of'] + fields = [ + "name", + "short_description", + "description", + "declaration", + "previous_version", + "fork_of", + "version", + ] beat_core_class = None + def validate_fork_of(self, fork_of): + if "previous_version" in self.initial_data: + raise serializers.ValidationError( + "fork_of and previous_version cannot appear together" + ) + return fork_of + + def validate_previous_version(self, previous_version): + if "fork_of" in self.initial_data: + raise serializers.ValidationError( + "previous_version and fork_of cannot appear together" + ) + return previous_version + def validate_description(self, description): - if description.find('\\') >= 0: #was escaped, unescape - description = description.decode('string_escape') + if description.find("\\") >= 0: # was escaped, unescape + description = description.decode("string_escape") return description def validate(self, data): - user = self.context.get('user') - name = self.Meta.model.sanitize_name(data['name']) - data['name'] = name - - if 'previous_version' in data: - if self.Meta.beat_core_class is not None: - previous_version_id = self.Meta.beat_core_class.Storage(settings.PREFIX, - data['previous_version']) - if previous_version_id.username is None: - previous_version_id.username = user.username - else: - previous_version_id = MapDot() - previous_version_id["username"] = user.username - previous_version_id["name"] = name - previous_version_id["version"] = data['previous_version'] - data['data'] = json.dumps(data['data']) - - else: - previous_version_id = None - - if 'fork_of' in data: - if self.Meta.beat_core_class is not None: - fork_of_id = self.Meta.beat_core_class.Storage(settings.PREFIX, - data['fork_of']) - if fork_of_id.username is None: - fork_of_id.username = user.username - else: - fork_of_id = MapDot() - fork_elem = data['fork_of'] - fork_of_id["username"] = fork_elem['username'] - fork_of_id["name"] = fork_elem['name'] - fork_of_id["version"] = fork_elem['version'] - data['data'] = json.dumps(data['data']) - - else: - fork_of_id = None - - # Retrieve the previous version (if applicable) - if previous_version_id is not None: + user = self.context.get("user") + name = self.Meta.model.sanitize_name(data["name"]) + data["name"] = name + version = data.get("version") + + # If version is not one then it's necessarily a new version + # forks start at one + if version > 1 and "previous_version" not in data: + raise serializers.ValidationError( + "{} {} version {} incomplete history data posted".format( + self.Meta.model.__name__.lower(), name, version + ) + ) + + if self.Meta.model.objects.filter( + author__username__iexact=user, name=name, version=version + ).exists(): + raise serializers.ValidationError( + "{} {} version {} already exists on this account".format( + self.Meta.model.__name__.lower(), name, version + ) + ) + + previous_version = data.get("previous_version") + fork_of = data.get("fork_of") + + if previous_version is not None: try: - previous_version = self.Meta.model.objects.get( - author__username__iexact=previous_version_id.username, - name=previous_version_id.name, - version=previous_version_id.version) - except: - raise serializers.ValidationError("{} '{}' not found".format( - self.Meta.model.__name__, - previous_version_id.fullname) - ) - - is_accessible = previous_version.accessibility_for(user) - if not is_accessible[0]: - raise serializers.ValidationError('No access allowed') - data['previous_version'] = previous_version - - # Retrieve the forked algorithm (if applicable) - if fork_of_id is not None: + previous_object = self.Meta.model.objects.annotate( + fullname=Concat( + "author__username", + V("/"), + "name", + V("/"), + "version", + output_field=CharField(), + ) + ).get(fullname=previous_version) + except self.Meta.model.DoesNotExist: + raise serializers.ValidationError( + "{} '{}' not found".format( + self.Meta.model.__name__, previous_version + ) + ) + accessibility_infos = previous_object.accessibility_for(user) + if not accessibility_infos.has_access: + raise serializers.ValidationError("No access allowed") + + if version - previous_object.version != 1: + raise serializers.ValidationError( + "The requested version ({}) for this {} does not match" + "the standard increment with {}".format( + version, self.Meta.model.__name__, previous_object.version + ) + ) + data["previous_version"] = previous_object + + elif fork_of is not None: + if version > 1: + raise serializers.ValidationError("A fork starts at 1") + try: - fork_of = self.Meta.model.objects.get(author__username__iexact=fork_of_id.username, - name=fork_of_id.name, - version=fork_of_id.version) - except: - raise serializers.ValidationError("{} '{}' not found".format(self.Meta.model.__name__, fork_of_id.fullname)) - - is_accessible = fork_of.accessibility_for(user) - if not is_accessible[0]: - raise serializers.ValidationError('No access allowed') - data['fork_of'] = fork_of - - # Determine the version number - last_version = None - - if previous_version_id is not None: - if (previous_version_id.username == user.username) and \ - (previous_version_id.name == name): - last_version = self.Meta.model.objects.filter(author=user, name=name).order_by('-version')[0] - - if last_version is None: - if self.Meta.model.objects.filter(author=user, name=name).count() > 0: - raise serializers.ValidationError('This {} name already exists on this account'.format(self.Meta.model.__name__.lower())) - - data['version'] = (last_version.version + 1 if last_version is not None else 1) + forked_of_object = self.Meta.model.objects.annotate( + fullname=Concat( + "author__username", + V("/"), + "name", + V("/"), + "version", + output_field=CharField(), + ) + ).get(fullname=fork_of) + except self.Meta.model.DoesNotExist: + raise serializers.ValidationError( + "{} '{}' fork origin not found".format( + self.Meta.model.__name__, fork_of + ) + ) + accessibility_infos = forked_of_object.accessibility_for(user) + if not accessibility_infos.has_access: + raise serializers.ValidationError("No access allowed") + data["fork_of"] = forked_of_object return data diff --git a/beat/web/dataformats/tests/tests_api.py b/beat/web/dataformats/tests/tests_api.py index b0b5201a64fadc36bf7be3c323cc5e2c2251593a..79f93fd7bd9657e7397e0a752c1e013927e1b34f 100644 --- a/beat/web/dataformats/tests/tests_api.py +++ b/beat/web/dataformats/tests/tests_api.py @@ -293,7 +293,9 @@ class DataFormatCreation(DataFormatsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1", "short_description": "blah"}), + json.dumps( + {"name": "valid-name1", "version": 1, "short_description": "blah"} + ), content_type="application/json", ) @@ -319,7 +321,7 @@ class DataFormatCreation(DataFormatsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1"}), + json.dumps({"name": "valid-name1", "version": 1}), content_type="application/json", ) @@ -345,7 +347,9 @@ class DataFormatCreation(DataFormatsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1", "short_description": "blah"}), + json.dumps( + {"name": "invalid name1", "version": 1, "short_description": "blah"} + ), content_type="application/json", ) @@ -371,7 +375,7 @@ class DataFormatCreation(DataFormatsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1"}), + json.dumps({"name": "invalid name1", "version": 1}), content_type="application/json", ) @@ -397,7 +401,7 @@ class DataFormatCreation(DataFormatsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "format2", "description": "blah"}), + json.dumps({"name": "format2", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -440,6 +444,7 @@ class DataFormatCreation(DataFormatsAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "short_description": "blah", "declaration": {"value": "int32"}, } diff --git a/beat/web/libraries/templates/libraries/edition.html b/beat/web/libraries/templates/libraries/edition.html index ab44d2d58af716cce99d308bc832bfc62387216a..3f51064712eab3c4e1b33b344ff2347686aa645e 100644 --- a/beat/web/libraries/templates/libraries/edition.html +++ b/beat/web/libraries/templates/libraries/edition.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 %} @@ -261,6 +261,7 @@ jQuery(document).ready(function() { data: JSON.stringify({ name: name, {% if op == 'new-version' %} + version: '{{ library.version | add:+1 }}', previous_version: '{{ library.fullname }}', {% endif %} {% if op == 'fork' %} diff --git a/beat/web/libraries/tests/tests_api.py b/beat/web/libraries/tests/tests_api.py index db99e68bb837e733065483dadfbed8538ab696f4..7b6e62b4de4ee501e50285169c9505d9e51ac6df 100644 --- a/beat/web/libraries/tests/tests_api.py +++ b/beat/web/libraries/tests/tests_api.py @@ -421,7 +421,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1", "description": "blah"}), + json.dumps({"name": "valid-name1", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -445,7 +445,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1"}), + json.dumps({"name": "valid-name1", "version": 1}), content_type="application/json", ) @@ -468,7 +468,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1", "description": "blah"}), + json.dumps({"name": "invalid name1", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -492,7 +492,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1"}), + json.dumps({"name": "invalid name1", "version": 1}), content_type="application/json", ) @@ -518,6 +518,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "description": "blah", "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, @@ -549,6 +550,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "valid-name1", + "version": 1, "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, } @@ -578,6 +580,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "invalid name1", + "version": 1, "description": "blah", "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, @@ -610,6 +613,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "invalid name1", + "version": 1, "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, } @@ -636,7 +640,9 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "usable_by_one_user", "description": "blah"}), + json.dumps( + {"name": "usable_by_one_user", "version": 1, "description": "blah"} + ), content_type="application/json", ) @@ -647,7 +653,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "usable_by_one_user"}), + json.dumps({"name": "usable_by_one_user", "version": 1}), content_type="application/json", ) @@ -658,7 +664,7 @@ class LibraryCreation(LibrariesAPIBase): response = self.client.post( self.url, - json.dumps({"name": "library 4", "description": "blah"}), + json.dumps({"name": "library 4", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -668,7 +674,9 @@ class LibraryCreation(LibrariesAPIBase): self.login_jackdoe() response = self.client.post( - self.url, json.dumps({"name": "library 4"}), content_type="application/json" + self.url, + json.dumps({"name": "library 4", "version": 1}), + content_type="application/json", ) self.checkResponse(response, 400, content_type="application/json") @@ -681,6 +689,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "usable_by_one_user", + "version": 1, "description": "blah", "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, @@ -699,6 +708,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "usable_by_one_user", + "version": 1, "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, } @@ -716,6 +726,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "library 4", + "version": 1, "description": "blah", "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, @@ -734,6 +745,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "", + "version": 1, "description": "blah", "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, @@ -753,6 +765,7 @@ class LibraryCreation(LibrariesAPIBase): json.dumps( { "name": "library 4", + "version": 1, "declaration": LibrariesAPIBase.DECLARATION, "code": LibrariesAPIBase.CODE, } diff --git a/beat/web/plotters/tests.py b/beat/web/plotters/tests.py index 0ddc21ae5f2cb62db602185dba021559188878aa..72c84c893425e3f15aa4a7104ac5ad5021b54594 100644 --- a/beat/web/plotters/tests.py +++ b/beat/web/plotters/tests.py @@ -81,6 +81,7 @@ class PlotterParameterTestCase(APITestCase): self.data_plotter = { "author": self.plot.username, "name": "plotter_test", + "version": 1, "short_description": "some description plotter", "description": "some longer description plotter", "declaration": { @@ -100,6 +101,7 @@ class PlotterParameterTestCase(APITestCase): self.data = { "name": "plotterparameter1", + "version": 1, "short_description": "some description", "description": "some longer description", "plotter": self.plotter.id, @@ -107,6 +109,7 @@ class PlotterParameterTestCase(APITestCase): self.data2 = { "name": "plotterparameter2", + "version": 1, "short_description": "some description2", "description": "some longer description2", "plotter": self.plotter.id, @@ -154,7 +157,7 @@ class PlotterParameterCreationTestCase(PlotterParameterTestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( json.loads(response.content)["non_field_errors"][0], - "This plotterparameter name already exists on this account", + "plotterparameter plotterparameter1 version 1 already exists on this account", ) diff --git a/beat/web/settings/test.py b/beat/web/settings/test.py index 8ac050df73c4f69d56ea652e9c42265e631fd114..1abd6fa8af1599f2824731bf8fd3f70b65837faf 100755 --- a/beat/web/settings/test.py +++ b/beat/web/settings/test.py @@ -48,7 +48,9 @@ if platform.system() == "Linux": database_name = os.path.join(shm_path, "test.sqlite3") else: - database_name = "test.sqlite3" + here = os.path.dirname(os.path.realpath(__file__)) + database_name = os.path.join(here, "test.sqlite3") + DATABASES["default"]["NAME"] = database_name # noqa DATABASES["default"]["TEST"] = {"NAME": DATABASES["default"]["NAME"]} # noqa DATABASES["default"]["OPTIONS"]["timeout"] = 30 # noqa diff --git a/beat/web/toolchains/templates/toolchains/edition.html b/beat/web/toolchains/templates/toolchains/edition.html index 25f6db4b3a7fe53348e56895f106ad57983f0936..295c539e7f9e96a78f9180f92074d44355b700cf 100644 --- a/beat/web/toolchains/templates/toolchains/edition.html +++ b/beat/web/toolchains/templates/toolchains/edition.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 %} @@ -159,6 +159,7 @@ function setupEditor(databases, toolchains) {% if not edition %} {% if toolchain_version > 1 %} name: '{{ toolchain_name }}', + version: '{{ toolchain_version }}', previous_version: '{{ toolchain_author }}/{{ toolchain_name }}/{{ toolchain_version|add:-1 }}', {% else %} name: $('#toolchain_name')[0].value.trim(), diff --git a/beat/web/toolchains/tests.py b/beat/web/toolchains/tests.py index da09521e90bcc431f783765e9414de60ac8a1055..3de38a63412b3d69a93de77ea8db61edfdd7f218 100644 --- a/beat/web/toolchains/tests.py +++ b/beat/web/toolchains/tests.py @@ -44,7 +44,7 @@ from ..common.testutils import BaseTestCase from ..common.testutils import tearDownModule # noqa test runner will call it from ..common.testutils import get_algorithms_from_data -TEST_PWD = "1234" +TEST_PWD = "1234" # nosec class ToolchainsCreationFunction(TestCase): @@ -710,7 +710,7 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "valid-name1"}), + json.dumps({"name": "valid-name1", "version": 1}), content_type="application/json", ) @@ -736,7 +736,7 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "invalid name1"}), + json.dumps({"name": "invalid name1", "version": 1}), content_type="application/json", ) @@ -761,7 +761,9 @@ class ToolchainCreation(ToolchainsAPIBase): self.login_jackdoe() response = self.client.post( - self.url, json.dumps({"name": "personal"}), content_type="application/json" + self.url, + json.dumps({"name": "personal", "version": 1}), + content_type="application/json", ) self.checkResponse(response, 400) @@ -771,7 +773,7 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "toolchain 4"}), + json.dumps({"name": "toolchain 4", "version": 1}), content_type="application/json", ) @@ -782,7 +784,7 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( self.url, - json.dumps({"name": "new_toolchain", "description": "blah"}), + json.dumps({"name": "new_toolchain", "version": 1, "description": "blah"}), content_type="application/json", ) @@ -811,7 +813,11 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( self.url, json.dumps( - {"name": "new_toolchain", "declaration": ToolchainsAPIBase.DECLARATION} + { + "name": "new_toolchain", + "version": 1, + "declaration": ToolchainsAPIBase.DECLARATION, + } ), content_type="application/json", ) @@ -839,6 +845,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "new_toolchain", + "version": 1, "declaration": ToolchainCreation.DECLARATION_INVALID, } ), @@ -856,6 +863,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "johndoe/toolchain1/1", } @@ -874,6 +882,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "johndoe/toolchain1/1", } @@ -890,7 +899,11 @@ class ToolchainCreation(ToolchainsAPIBase): response = self.client.post( url, json.dumps( - {"description": "blah", "fork_of": "jackdoe/public_for_one_user/1"} + { + "version": 1, + "description": "blah", + "fork_of": "jackdoe/public_for_one_user/1", + } ), content_type="application/json", ) @@ -909,6 +922,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "jackdoe/public_for_one_user/1", } @@ -928,6 +942,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "jackdoe/personal/1", } @@ -947,6 +962,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "jackdoe/public_for_all/1", } @@ -998,6 +1014,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "fork_of": "jackdoe/public_for_one_user/1", } @@ -1047,6 +1064,7 @@ class ToolchainCreation(ToolchainsAPIBase): json.dumps( { "name": "name1", + "version": 1, "description": "blah", "declaration": ToolchainsAPIBase.DECLARATION, "fork_of": "jackdoe/public_for_one_user/1", diff --git a/dev.yml b/dev.yml index 5c4105699e0b64e91134e9e2be2694e13f5f3eb3..bf47412b63c3e98f7d2ddfd37dd909dd81680a8b 100644 --- a/dev.yml +++ b/dev.yml @@ -11,9 +11,9 @@ dependencies: - beat-devel=2020.01.09 # beat dependencies matching release.cfg - - beat.core=1.9.2 - - beat.backend.python=1.7.3 - - beat.cmdline=1.6.1 + - beat.core=1.10.3 + - beat.backend.python=1.7.6 + - beat.cmdline=1.7.1 # requirements.txt, they are indirectly pinned through the above - docopt diff --git a/release.cfg b/release.cfg index 8b2e6f7faca67d3dac60da29300334836f8b00f8..914cafa1bb69e204fe651a5cad448603d9458918 100644 --- a/release.cfg +++ b/release.cfg @@ -25,9 +25,9 @@ eggs = ${buildout:eggs} interpreter = python [sources] -beat.core = git https://gitlab.idiap.ch/beat/beat.core.git rev=v1.9.2 -beat.cmdline = git https://gitlab.idiap.ch/beat/beat.cmdline.git rev=v1.6.1 -beat.backend.python = git https://gitlab.idiap.ch/beat/beat.backend.python.git rev=v1.7.3 +beat.core = git https://gitlab.idiap.ch/beat/beat.core.git rev=v1.10.3 +beat.cmdline = git https://gitlab.idiap.ch/beat/beat.cmdline.git rev=v1.7.1 +beat.backend.python = git https://gitlab.idiap.ch/beat/beat.backend.python.git rev=v1.7.6 [uwsgi] recipe = buildout.recipe.uwsgi