From b6459d4ed59fae90de78eb6c04b6e50ab69e96f4 Mon Sep 17 00:00:00 2001 From: Philip ABBET <philip.abbet@idiap.ch> Date: Thu, 10 Nov 2016 16:10:42 +0100 Subject: [PATCH] [algorithms, api] Add support for retrieving binary algorithms --- beat/web/algorithms/api.py | 44 +++ beat/web/algorithms/api_urls.py | 10 + .../algorithms/migrations/0002_cxx_backend.py | 20 + beat/web/algorithms/tests/core.py | 160 ++++++++ beat/web/algorithms/tests/tests_api.py | 366 ++++++++++++++++-- beat/web/code/api.py | 2 +- beat/web/code/models.py | 28 +- beat/web/code/serializers.py | 6 +- beat/web/common/models.py | 15 +- .../libraries/migrations/0002_cxx_backend.py | 20 + .../plotters/migrations/0002_cxx_backend.py | 20 + 11 files changed, 631 insertions(+), 60 deletions(-) mode change 100644 => 100755 beat/web/algorithms/api.py mode change 100644 => 100755 beat/web/algorithms/api_urls.py create mode 100644 beat/web/algorithms/migrations/0002_cxx_backend.py mode change 100644 => 100755 beat/web/algorithms/tests/core.py mode change 100644 => 100755 beat/web/algorithms/tests/tests_api.py mode change 100644 => 100755 beat/web/code/api.py mode change 100644 => 100755 beat/web/code/models.py mode change 100644 => 100755 beat/web/code/serializers.py mode change 100644 => 100755 beat/web/common/models.py create mode 100644 beat/web/libraries/migrations/0002_cxx_backend.py create mode 100644 beat/web/plotters/migrations/0002_cxx_backend.py diff --git a/beat/web/algorithms/api.py b/beat/web/algorithms/api.py old mode 100644 new mode 100755 index e9b7c7723..f0f4912bf --- a/beat/web/algorithms/api.py +++ b/beat/web/algorithms/api.py @@ -25,6 +25,9 @@ # # ############################################################################### +from django.http import Http404 +from django.http import HttpResponse + from .models import Algorithm from .serializers import AlgorithmSerializer from .serializers import FullAlgorithmSerializer @@ -118,3 +121,44 @@ class DiffAlgorithmView(DiffView): """ model = Algorithm serializer_class = CodeDiffSerializer + + +#---------------------------------------------------------- + + +def binary(request, author_name, object_name, version=None): + """Returns the shared library of a binary algorithm + """ + + # Retrieves the algorithm + if version: + algorithm = get_object_or_404( + Algorithm, + author__username__iexact=author_name, + name__iexact=object_name, + version=int(version), + ) + else: + algorithm = Algorithm.objects.filter(author__username__iexact=author_name, + name__iexact=object_name).order_by('-version') + if not algorithm: + raise Http404() + else: + algorithm = algorithm[0] + + (has_access, _, accessibility) = algorithm.accessibility_for(request.user, without_usable=True) + + if not has_access: + raise Http404() + + if not algorithm.is_binary(): + raise Http404() + + binary_data = algorithm.source_code + + response = HttpResponse(binary_data, content_type='application/octet-stream') + + response['Content-Length'] = len(binary_data) + response['Content-Disposition'] = 'attachment; filename=%d.so' % algorithm.version + + return response diff --git a/beat/web/algorithms/api_urls.py b/beat/web/algorithms/api_urls.py old mode 100644 new mode 100755 index d12f252f7..c0e84a2a1 --- a/beat/web/algorithms/api_urls.py +++ b/beat/web/algorithms/api_urls.py @@ -66,4 +66,14 @@ urlpatterns = [ name='object', ), + url(r'^(?P<author_name>\w+)/(?P<object_name>[-\w]+)/(?P<version>\d+)/binary/$', + api.binary, + name='binary', + ), + + url(r'^(?P<author_name>\w+)/(?P<object_name>[-\w]+)/binary/$', + api.binary, + name='binary', + ), + ] diff --git a/beat/web/algorithms/migrations/0002_cxx_backend.py b/beat/web/algorithms/migrations/0002_cxx_backend.py new file mode 100644 index 000000000..abe1969ad --- /dev/null +++ b/beat/web/algorithms/migrations/0002_cxx_backend.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-11-09 11:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('algorithms', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='algorithm', + name='language', + field=models.CharField(choices=[(b'U', b'Unknown'), (b'C', b'C++'), (b'M', b'Matlab'), (b'P', b'Python'), (b'R', b'R')], default=b'P', max_length=1), + ), + ] diff --git a/beat/web/algorithms/tests/core.py b/beat/web/algorithms/tests/core.py old mode 100644 new mode 100755 index 2f3c375bb..421f05508 --- a/beat/web/algorithms/tests/core.py +++ b/beat/web/algorithms/tests/core.py @@ -188,6 +188,7 @@ class AlgorithmsCreationFunction(AlgorithmsBaseTestCase): class AlgorithmsTestCase(AlgorithmsBaseTestCase): + def setup_algorithms(self, declaration, code): user1 = User.objects.get(username='jackdoe') user2 = User.objects.get(username='johndoe') @@ -304,6 +305,138 @@ class AlgorithmsTestCase(AlgorithmsBaseTestCase): algorithm.share(public=True) + def setup_binary_algorithms(self, declaration, binary_data): + user1 = User.objects.get(username='jackdoe') + user2 = User.objects.get(username='johndoe') + + team1 = Team.objects.get(name="teamdoe", owner=user1) + team2 = Team.objects.get(name="teamdoe2", owner=user1) + + # Personal + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_personal', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + # Usable by one user + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_usable_by_one_user', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=False, users=[user2]) + + # Usable by one team + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_usable_by_one_team', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=False, teams=[team1]) + + # Usable by teams + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_usable_by_teams', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=False, teams=[team1, team2]) + + # Usable by all + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_usable_by_all', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=False) + + # Accessible to one user + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_public_for_one_user', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=True, users=[user2]) + + # Accessible to one team + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_public_for_one_team', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=True, teams=[team1]) + + # Accessible to teams + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_public_for_teams', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=True, teams=[team1, team2]) + + # Accessible to all + (algorithm, errors) = Algorithm.objects.create_algorithm( + author=user1, + name='binary_public_for_all', + short_description='', + declaration=declaration, + ) + assert algorithm, errors + + algorithm.source_code = binary_data + algorithm.save() + + algorithm.share(public=True) + + class AlgorithmsAccessibilityFunctionsBase(AlgorithmsTestCase): def setUp(self): @@ -379,6 +512,27 @@ class AlgorithmsAPIBase(AlgorithmsTestCase): JSON_DECLARATION = json.loads(DECLARATION) + CXX_DECLARATION = """{ + "language": "cxx", + "splittable": false, + "groups": [ + { + "name": "channel1", + "inputs": { + "a": { "type": "johndoe/single_integer/1" }, + "b": { "type": "johndoe/single_integer/1" } + }, + "outputs": { + "sum": { "type": "johndoe/single_integer/1" } + } + } + ], + "parameters": { + } +}""" + + JSON_CXX_DECLARATION = json.loads(CXX_DECLARATION) + CODE = """class Algorithm: def process(self, inputs, outputs): @@ -405,6 +559,9 @@ class AlgorithmsAPIBase(AlgorithmsTestCase): } }""" + BINARY = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" \ + "\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" + def setUp(self): super(AlgorithmsAPIBase, self).setUp() @@ -446,3 +603,6 @@ class AlgorithmsAPIBase(AlgorithmsTestCase): algorithm.previous_version = None algorithm.fork_of = Algorithm.objects.get(name='public_for_all') algorithm.save() + + self.setup_binary_algorithms(AlgorithmsAPIBase.CXX_DECLARATION, + AlgorithmsAPIBase.BINARY) diff --git a/beat/web/algorithms/tests/tests_api.py b/beat/web/algorithms/tests/tests_api.py old mode 100644 new mode 100755 index 749fd77b8..7d9db6b7d --- a/beat/web/algorithms/tests/tests_api.py +++ b/beat/web/algorithms/tests/tests_api.py @@ -50,18 +50,34 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): content = self.checkResponse(response, 200, content_type='application/json') self.assertTrue(isinstance(content, list)) - self.assertEqual(len(content), 2) + self.assertEqual(len(content), 4) algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'public') + self.assertEqual(algorithm['language'], 'python') self.assertFalse(algorithm.has_key('is_owner')) algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') + self.assertFalse(algorithm.has_key('is_owner')) + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'public') + self.assertEqual(algorithm['language'], 'cxx') + self.assertFalse(algorithm.has_key('is_owner')) + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') self.assertFalse(algorithm.has_key('is_owner')) def test_all_accessible_algorithms(self): @@ -73,7 +89,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): content = self.checkResponse(response, 200, content_type='application/json') self.assertTrue(isinstance(content, list)) - self.assertEqual(len(content), 9) + self.assertEqual(len(content), 17) algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_one_user/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -82,6 +98,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_one_team/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -90,6 +107,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_teams/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -98,6 +116,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -106,6 +125,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'public') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -114,6 +134,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_one_user/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -122,6 +143,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_one_team/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -130,6 +152,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_teams/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -138,6 +161,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['is_owner'], False) self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'johndoe/forked_algo/1', content)[0] self.assertEqual(algorithm['short_description'], '') @@ -146,6 +170,79 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertEqual(algorithm['fork_of'], 'jackdoe/public_for_all/1') self.assertEqual(algorithm['is_owner'], True) self.assertEqual(algorithm['accessibility'], 'private') + self.assertEqual(algorithm['language'], 'python') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_user/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_team/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_teams/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'public') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_user/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_team/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_teams/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertEqual(algorithm['version'], 1) + self.assertEqual(algorithm['previous_version'], None) + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['is_owner'], False) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertEqual(algorithm['language'], 'cxx') def test_own_algorithms(self): @@ -157,7 +254,7 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): content = self.checkResponse(response, 200, content_type='application/json') self.assertTrue(isinstance(content, list)) - self.assertEqual(len(content), 9) + self.assertEqual(len(content), 18) self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/usable_by_one_user/1', content)), 1) self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/usable_by_one_team/1', content)), 1) @@ -169,43 +266,23 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/public_for_teams/1', content)), 1) self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/personal/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_user/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_team/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_teams/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_all/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_public_for_all/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_user/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_team/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_public_for_teams/1', content)), 1) + self.assertEqual(len(filter(lambda x: x['name'] == 'jackdoe/binary_personal/1', content)), 1) algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_one_user/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) - # self.assertEqual(len(algorithm['inputs']), 2) - # self.assertEqual(len(algorithm['outputs']), 1) - - # input = algorithm['inputs'][0] - # self.assertEqual(input['name'], 'a') - # self.assertEqual(input['dataformat'], 'johndoe/single_integer/1') - - # input = algorithm['inputs'][1] - # self.assertEqual(input['name'], 'b') - # self.assertEqual(input['dataformat'], 'johndoe/single_integer/1') - - # output = algorithm['outputs'][0] - # self.assertEqual(output['name'], 'sum') - # self.assertEqual(output['dataformat'], 'johndoe/single_integer/1') - algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) - # self.assertEqual(len(algorithm['inputs']), 2) - # self.assertEqual(len(algorithm['outputs']), 1) - - # input = algorithm['inputs'][0] - # self.assertEqual(input['name'], 'a') - # self.assertEqual(input['dataformat'], 'johndoe/single_integer/1') - - # input = algorithm['inputs'][1] - # self.assertEqual(input['name'], 'b') - # self.assertEqual(input['dataformat'], 'johndoe/single_integer/1') - - # output = algorithm['outputs'][0] - # self.assertEqual(output['name'], 'sum') - # self.assertEqual(output['dataformat'], 'johndoe/single_integer/1') def test_other_user_algorithms(self): @@ -217,55 +294,119 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): content = self.checkResponse(response, 200, content_type='application/json') self.assertTrue(isinstance(content, list)) - self.assertEqual(len(content), 8) + self.assertEqual(len(content), 16) algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_one_user/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_one_team/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_teams/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/usable_by_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_one_user/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_one_team/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_teams/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'confidential') self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'public') self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_user/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_one_team/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_teams/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_usable_by_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertFalse(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_user/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_one_team/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_teams/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'confidential') + self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'public') + self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') def test_user_algorithms_for_anonymous_user(self): @@ -275,13 +416,21 @@ class AlgorithmsListRetrieval(AlgorithmsAPIBase): content = self.checkResponse(response, 200, content_type='application/json') self.assertTrue(isinstance(content, list)) - self.assertEqual(len(content), 1) + self.assertEqual(len(content), 2) algorithm = filter(lambda x: x['name'] == 'jackdoe/public_for_all/1', content)[0] self.assertEqual(algorithm['short_description'], '') self.assertTrue(algorithm['fork_of'] is None) self.assertEqual(algorithm['accessibility'], 'public') self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'python') + + algorithm = filter(lambda x: x['name'] == 'jackdoe/binary_public_for_all/1', content)[0] + self.assertEqual(algorithm['short_description'], '') + self.assertTrue(algorithm['fork_of'] is None) + self.assertEqual(algorithm['accessibility'], 'public') + self.assertTrue(algorithm['opensource']) + self.assertEqual(algorithm['language'], 'cxx') class AlgorithmsNameCheck(AlgorithmsAPIBase): @@ -1477,6 +1626,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['accessibility'], 'public') self.assertTrue(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertFalse(data.has_key('is_owner')) @@ -1490,6 +1640,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertFalse(data.has_key('code')) self.assertFalse(data.has_key('is_owner')) @@ -1505,6 +1656,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], False) self.assertEqual(data['accessibility'], 'public') self.assertTrue(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1521,6 +1673,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], False) self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertFalse(data.has_key('code')) @@ -1535,6 +1688,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], False) self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertFalse(data.has_key('code')) @@ -1549,6 +1703,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], False) self.assertEqual(data['accessibility'], 'confidential') self.assertTrue(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1565,6 +1720,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], True) self.assertEqual(data['accessibility'], 'public') self.assertTrue(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1586,6 +1742,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], True) self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1607,6 +1764,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], True) self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1628,6 +1786,7 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertEqual(data['is_owner'], True) self.assertEqual(data['accessibility'], 'confidential') self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'python') self.assertEqual(data['description'], '') self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_DECLARATION) @@ -1639,6 +1798,145 @@ class AlgorithmRetrieval(AlgorithmsAPIBase): self.assertFalse(data['sharing'].has_key('usable_by')) + def test_successful_retrieval_of_binary_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:object', args=['jackdoe', 'binary_personal']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/json') + + self.assertEqual(data['is_owner'], True) + self.assertEqual(data['accessibility'], 'private') + self.assertFalse(data['opensource']) + self.assertEqual(data['language'], 'cxx') + self.assertEqual(data['description'], '') + + self.assertEqual(data['declaration'], AlgorithmsAPIBase.JSON_CXX_DECLARATION) + self.assertFalse(data.has_key('code')) + + + +class AlgorithmBinaryRetrieval(AlgorithmsAPIBase): + + def test_no_retrieval_of_confidential_algorithm_for_anonymous_user(self): + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_personal']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_fail_to_retrieve_with_invalid_username(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['unknown', 'binary_personal']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_fail_to_retrieve_with_invalid_algorithm_name(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['johndoe', 'unknown']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_no_retrieval_of_confidential_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:binary', args=['johndoe', 'binary_personal']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_successful_retrieval_of_public_algorithm_for_anonymous_user(self): + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_public_for_all']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + def test_no_retrieval_of_usable_algorithm_for_anonymous_user(self): + url = reverse('api_algorithms:binary', args=['jackdoe', 'usable_by_all']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_successful_retrieval_of_public_algorithm(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_public_for_all']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + + def test_no_retrieval_of_usable_algorithm(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_usable_by_one_user']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_no_retrieval_of_publicly_usable_algorithm(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_usable_by_all']) + response = self.client.get(url) + self.checkResponse(response, 404) + + + def test_successful_retrieval_of_confidential_algorithm(self): + self.client.login(username='johndoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_public_for_one_user']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + + def test_successful_retrieval_of_own_public_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_public_for_all']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + + def test_successful_retrieval_of_own_confidential_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_personal']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + + def test_successful_retrieval_of_own_usable_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_usable_by_all']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + + def test_successful_retrieval_of_own_shared_algorithm(self): + self.client.login(username='jackdoe', password='1234') + + url = reverse('api_algorithms:binary', args=['jackdoe', 'binary_public_for_one_user']) + response = self.client.get(url) + data = self.checkResponse(response, 200, content_type='application/octet-stream') + + self.assertEqual(data, AlgorithmsAPIBase.BINARY) + + class AlgorithmDeletion(AlgorithmsAPIBase): diff --git a/beat/web/code/api.py b/beat/web/code/api.py old mode 100644 new mode 100755 index 7046ce6d4..b778eb7ac --- a/beat/web/code/api.py +++ b/beat/web/code/api.py @@ -226,7 +226,7 @@ class RetrieveUpdateDestroyCodeView(RetrieveUpdateDestroyContributionView): # - needed_dataformats # - attestations fields_to_remove = [] - if (request.user != db_object.author) and not(open_source): + if ((request.user != db_object.author) and not(open_source)) or db_object.is_binary(): fields_to_remove = ['code'] fields_to_return = self.get_serializer_fields(request, allow_sharing=(request.user == db_object.author), diff --git a/beat/web/code/models.py b/beat/web/code/models.py old mode 100644 new mode 100755 index b76b514ed..8131372e8 --- a/beat/web/code/models.py +++ b/beat/web/code/models.py @@ -132,7 +132,7 @@ class CodeManager(StoredContributionManager): # Figure out the language language = declaration.get('language', 'unknown') - language = getattr(Code, language.upper()) + code_db.language = getattr(Code, language.upper()) # Check the provided source code if code is None: @@ -140,7 +140,7 @@ class CodeManager(StoredContributionManager): code_db.source_code = previous_version.source_code elif fork_of is not None: code_db.source_code = fork_of.source_code - else: + elif code_db.language != Code.CXX: code_db.source_code = default.code else: code_db.source_code = code @@ -194,14 +194,14 @@ class Code(StoredContribution): # All possible values should be in sync with beat.core.utils UNKNOWN = 'U' - BINARY = 'B' + CXX = 'C' MATLAB = 'M' PYTHON = 'P' R = 'R' CODE_LANGUAGE = ( (UNKNOWN, 'Unknown'), - (BINARY, 'Binary'), + (CXX, 'Cxx'), (MATLAB, 'Matlab'), (PYTHON, 'Python'), (R, 'R'), @@ -470,6 +470,10 @@ class Code(StoredContribution): return open_source + def is_binary(self): + return self.language in [Code.CXX] + + #_____ Overrides __________ def save(self, *args, **kwargs): @@ -477,10 +481,6 @@ class Code(StoredContribution): # Invoke the base implementation super(Code, self).save(*args, **kwargs) - # If the filename has changed, move all the files - if self.source_code_filename() != self.source_code_file.name: - self._rename_file('source_code_file', self.source_code_filename()) - # Ensures that the sharing informations are consistent if self.sharing == Code.PUBLIC: self.shared_with.clear() @@ -514,7 +514,7 @@ class Code(StoredContribution): return wrapper - def _accessibility_for_user(self, user): + def _accessibility_for_user(self, user, without_usable=False): """Returns a tuple (<has_access>, <open_source>, <accessibility>), with <accessibility> being either 'public', 'private', 'confidential' """ @@ -531,29 +531,29 @@ class Code(StoredContribution): return (False, False, None) elif self.sharing == Contribution.PUBLIC: return (True, True, 'public') - elif self.sharing == Contribution.USABLE: + elif not without_usable and (self.sharing == Contribution.USABLE): return (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 self.usable_by.filter(id=user.id).exists() or (self.usable_by_team.filter(members=user).count() > 0): + 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) - def _accessibility_for_team(self, team): + def _accessibility_for_team(self, team, without_usable=False): """Team specific accessibility check """ if self.sharing == Contribution.PRIVATE: return (False, False, None) elif self.sharing == Contribution.PUBLIC: return (True, True, 'public') - elif self.sharing == Contribution.USABLE: + elif not without_usable and (self.sharing == Contribution.USABLE): return (True, False, 'confidential') elif self.shared_with_team.filter(id=team.id).exists(): return (True, True, 'confidential') - elif self.usable_by_team.filter(id=team.id).exists(): + elif not without_usable and self.usable_by_team.filter(id=team.id).exists(): return (True, False, 'confidential') return (False, False, None) diff --git a/beat/web/code/serializers.py b/beat/web/code/serializers.py old mode 100644 new mode 100755 index 77f50e8db..b50d101f6 --- a/beat/web/code/serializers.py +++ b/beat/web/code/serializers.py @@ -65,11 +65,12 @@ class CodeSharingSerializer(SharingSerializer): class CodeSerializer(ContributionSerializer): opensource = serializers.SerializerMethodField() + language = serializers.SerializerMethodField() modifiable = serializers.BooleanField() class Meta(ContributionSerializer.Meta): model = Code - default_fields = ContributionSerializer.Meta.default_fields + ['opensource'] + default_fields = ContributionSerializer.Meta.default_fields + ['opensource', 'language'] extra_fields = ContributionSerializer.Meta.extra_fields + ['code'] exclude = ContributionSerializer.Meta.exclude + ['source_code_file'] @@ -85,6 +86,9 @@ class CodeSerializer(ContributionSerializer): (has_access, open_source, accessibility) = obj.accessibility_for(user) return open_source + def get_language(self, obj): + return filter(lambda x: x[0] == obj.language, iter(Code.CODE_LANGUAGE))[0][1].lower() + def get_accessibility(self, obj): if obj.sharing == Code.PUBLIC: return 'public' diff --git a/beat/web/common/models.py b/beat/web/common/models.py old mode 100644 new mode 100755 index 66e680a07..50b3b68e6 --- a/beat/web/common/models.py +++ b/beat/web/common/models.py @@ -151,14 +151,14 @@ class Shareable(models.Model): return (self.attestations.count() == 0) return True - def accessibility_for(self, user_or_team): + def accessibility_for(self, user_or_team, without_usable=False): """Returns a tuple (<has_access>, <accessibility>), with <accessibility> being either 'public', 'private', 'confidential' """ if isinstance(user_or_team, User) or isinstance(user_or_team, AnonymousUser): - return self._accessibility_for_user(user_or_team) + return self._accessibility_for_user(user_or_team, without_usable) elif isinstance(user_or_team, Team): - return self._accessibility_for_team(user_or_team) + return self._accessibility_for_team(user_or_team, without_usable) else: raise NotUserNorTeam @@ -295,7 +295,7 @@ class Shareable(models.Model): #_____ Protected Methods __________ - def _accessibility_for_user(self, user): + def _accessibility_for_user(self, user, without_usable=False): """User specific accessibility check """ if hasattr(self, 'author') and self.author == user: @@ -317,7 +317,7 @@ class Shareable(models.Model): return (False, None) - def _accessibility_for_team(self, team): + def _accessibility_for_team(self, team, without_usable=False): """Team specific accessibility check """ if self.sharing == Shareable.PRIVATE: @@ -726,11 +726,6 @@ class StoredContribution(Contribution): # Invoke the base implementation super(StoredContribution, self).save(*args, **kwargs) - # If the filename has changed, move all the files - if self.declaration_filename() != self.declaration_file.name: - self._rename_file('declaration_file', self.declaration_filename()) - self._rename_file('description_file', self.description_filename()) - #_____ Properties __________ diff --git a/beat/web/libraries/migrations/0002_cxx_backend.py b/beat/web/libraries/migrations/0002_cxx_backend.py new file mode 100644 index 000000000..a0d8060e6 --- /dev/null +++ b/beat/web/libraries/migrations/0002_cxx_backend.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-11-09 11:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('libraries', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='library', + name='language', + field=models.CharField(choices=[(b'U', b'Unknown'), (b'C', b'C++'), (b'M', b'Matlab'), (b'P', b'Python'), (b'R', b'R')], default=b'P', max_length=1), + ), + ] diff --git a/beat/web/plotters/migrations/0002_cxx_backend.py b/beat/web/plotters/migrations/0002_cxx_backend.py new file mode 100644 index 000000000..709a1a571 --- /dev/null +++ b/beat/web/plotters/migrations/0002_cxx_backend.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-11-09 11:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plotters', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='plotter', + name='language', + field=models.CharField(choices=[(b'U', b'Unknown'), (b'C', b'C++'), (b'M', b'Matlab'), (b'P', b'Python'), (b'R', b'R')], default=b'P', max_length=1), + ), + ] -- GitLab