diff --git a/bin/pg_dump.py b/bin/pg_dump.py index f8b29c254..88f93d548 100755 --- a/bin/pg_dump.py +++ b/bin/pg_dump.py @@ -27,5 +27,3 @@ call([ 'docker', 'exec', 'codabench-django-1', 'python', 'manage.py', 'upload_backup', f'{dump_name}' ]) - - diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 5057e3778..1171bb590 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -41,7 +41,7 @@ # Setup base directories used by all submissions # note: we need to pass this directory to docker-compose so it knows where to store things! HOST_DIRECTORY = os.environ.get("HOST_DIRECTORY", "/tmp/codabench/") -BASE_DIR = "/codabench/" # base directory inside the container +BASE_DIR = "/codabench/" # base directory inside the container CACHE_DIR = os.path.join(BASE_DIR, "cache") MAX_CACHE_DIR_SIZE_GB = float(os.environ.get('MAX_CACHE_DIR_SIZE_GB', 10)) @@ -74,6 +74,7 @@ else: CONTAINER_ENGINE_EXECUTABLE = "docker" + class SubmissionException(Exception): pass @@ -182,7 +183,7 @@ def __init__(self, run_args): self.bundle_dir = os.path.join(self.root_dir, "bundles") self.input_dir = os.path.join(self.root_dir, "input") self.output_dir = os.path.join(self.root_dir, "output") - self.data_dir = os.path.join(HOST_DIRECTORY, "data") # absolute path to data in the host + self.data_dir = os.path.join(HOST_DIRECTORY, "data") # absolute path to data in the host self.logs = {} # Details for submission @@ -497,10 +498,10 @@ async def _run_program_directory(self, program_dir, kind, can_be_output=False): logger.info(f"Metadata path is {os.path.join(program_dir, metadata_path)}") with open(os.path.join(program_dir, metadata_path), 'r') as metadata_file: - try: # try to find a command in the metadata, in other cases set metadata to None + try: # try to find a command in the metadata, in other cases set metadata to None metadata = yaml.load(metadata_file.read(), Loader=yaml.FullLoader) logger.info(f"Metadata contains:\n {metadata}") - if isinstance(metadata, dict): # command found + if isinstance(metadata, dict): # command found command = metadata.get("command") else: command = None diff --git a/fabfile.py b/fabfile.py index 580a715ab..de5e6a891 100644 --- a/fabfile.py +++ b/fabfile.py @@ -17,6 +17,7 @@ # $ fab -R role_name env.roledefs = yaml.load(open('server_config.yaml').read()) + # ---------------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------------- @@ -24,6 +25,7 @@ def _reconnect_current_host(): network.disconnect_all() connections.connect(env.host + ':%s' % env.port) + # ---------------------------------------------------------------------------- # Tasks # ---------------------------------------------------------------------------- diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 4d79c9a61..9b27ca08d 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -4,6 +4,7 @@ from api.fields import NamedBase64ImageField from api.mixins import DefaultUserCreateMixin +from api.serializers.datasets import DataDetailSerializer from api.serializers.leaderboards import LeaderboardSerializer, ColumnSerializer from api.serializers.profiles import CollaboratorSerializer from api.serializers.submissions import SubmissionScoreSerializer @@ -41,6 +42,8 @@ class Meta: 'auto_migrate_to_this_phase', 'hide_output', 'leaderboard', + 'public_data', + 'starting_kit', 'is_final_phase', ) @@ -90,6 +93,9 @@ class PhaseDetailSerializer(serializers.ModelSerializer): tasks = PhaseTaskInstanceSerializer(source='task_instances', many=True) status = serializers.SerializerMethodField() + public_data = DataDetailSerializer(read_only=True) + starting_kit = DataDetailSerializer(read_only=True) + class Meta: model = Phase fields = ( @@ -100,13 +106,16 @@ class Meta: 'name', 'description', 'status', + 'execution_time_limit', 'tasks', - 'auto_migrate_to_this_phase', 'has_max_submissions', 'max_submissions_per_day', 'max_submissions_per_person', - 'execution_time_limit', + 'auto_migrate_to_this_phase', 'hide_output', + # no leaderboard + 'public_data', + 'starting_kit', 'is_final_phase', ) diff --git a/src/apps/api/serializers/datasets.py b/src/apps/api/serializers/datasets.py index 0b44b7ec1..25e069afc 100644 --- a/src/apps/api/serializers/datasets.py +++ b/src/apps/api/serializers/datasets.py @@ -26,6 +26,7 @@ class Meta: 'was_created_by_competition', 'competition', 'file_name', + ) read_only_fields = ( 'key', @@ -61,6 +62,7 @@ def create(self, validated_data): class DataSimpleSerializer(serializers.ModelSerializer): + class Meta: model = Data fields = ( @@ -74,6 +76,7 @@ class Meta: class DataDetailSerializer(serializers.ModelSerializer): created_by = serializers.CharField(source='created_by.username') competition = serializers.SerializerMethodField() + value = serializers.CharField(source='key', required=False) class Meta: model = Data @@ -86,6 +89,8 @@ class Meta: 'description', 'is_public', 'key', + # Value is used for Semantic Multiselect dropdown api calls + 'value', 'was_created_by_competition', 'in_use', 'file_size', diff --git a/src/apps/api/serializers/tasks.py b/src/apps/api/serializers/tasks.py index deac682cc..b98ce36ea 100644 --- a/src/apps/api/serializers/tasks.py +++ b/src/apps/api/serializers/tasks.py @@ -13,6 +13,7 @@ class SolutionSerializer(WritableNestedModelSerializer): tasks = serializers.SlugRelatedField(queryset=Task.objects.all(), required=False, allow_null=True, slug_field='key', many=True) data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') + size = serializers.SerializerMethodField() class Meta: model = Solution @@ -23,8 +24,16 @@ class Meta: 'tasks', 'data', 'md5', + 'size', ] + def get_size(self, instance): + try: + return instance.data.file_size + except AttributeError: + print("This solution has no data associated with it...might be a test") + return None + class SolutionListSerializer(serializers.ModelSerializer): data = DataDetailSerializer() @@ -38,6 +47,7 @@ class Meta: class TaskSerializer(DefaultUserCreateMixin, WritableNestedModelSerializer): + input_data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') ingestion_program = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') reference_data = serializers.SlugRelatedField(queryset=Data.objects.all(), required=False, allow_null=True, slug_field='key') @@ -159,6 +169,8 @@ class PhaseTaskInstanceSerializer(serializers.HyperlinkedModelSerializer): key = serializers.CharField(source='task.key', required=False) created_when = serializers.DateTimeField(source='task.created_when', required=False) name = serializers.CharField(source='task.name', required=False) + solutions = serializers.SerializerMethodField() + public_datasets = serializers.SerializerMethodField() class Meta: model = PhaseTaskInstance @@ -172,4 +184,23 @@ class Meta: 'key', 'created_when', 'name', + 'solutions', + 'public_datasets' ) + + def get_solutions(self, instance): + qs = instance.task.solutions.all() + return SolutionSerializer(qs, many=True).data + + def get_public_datasets(self, instance): + input_data = instance.task.input_data + reference_data = instance.task.reference_data + ingestion_program = instance.task.ingestion_program + scoring_program = instance.task.scoring_program + try: + dataset_list_ids = [input_data.id, reference_data.id, ingestion_program.id, scoring_program.id] + qs = Data.objects.filter(id__in=dataset_list_ids) + return DataDetailSerializer(qs, many=True).data + except AttributeError: + print("This phase task has no datasets") + return None diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 2db303a45..7d2a5570d 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -31,6 +31,7 @@ from competitions.emails import send_participation_requested_emails, send_participation_accepted_emails, \ send_participation_denied_emails, send_direct_participant_email from competitions.models import Competition, Phase, CompetitionCreationTaskStatus, CompetitionParticipant, Submission +from datasets.models import Data from competitions.tasks import batch_send_email, manual_migration, create_competition_dump from competitions.utils import get_popular_competitions, get_featured_competitions from leaderboards.models import Leaderboard @@ -229,7 +230,21 @@ def update(self, request, *args, **kwargs): phase['leaderboard'] = leaderboard_id + # Get public_data and starting_kit + for phase in data['phases']: + # We just need to know what public_data and starting_kit go with this phase + # We don't need to serialize the whole object + try: + phase['public_data'] = Data.objects.filter(key=phase['public_data']['value'])[0].id + except TypeError: + phase['public_data'] = None + try: + phase['starting_kit'] = Data.objects.filter(key=phase['starting_kit']['value'])[0].id + except TypeError: + phase['starting_kit'] = None + serializer = self.get_serializer(instance, data=data, partial=partial) + type(serializer) serializer.is_valid(raise_exception=True) self.perform_update(serializer) diff --git a/src/apps/api/views/datasets.py b/src/apps/api/views/datasets.py index d1a817c24..fd2ae17cb 100644 --- a/src/apps/api/views/datasets.py +++ b/src/apps/api/views/datasets.py @@ -79,7 +79,6 @@ def get_serializer_class(self): return serializers.DataSerializer def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) new_dataset = serializer.save() # request_sassy_file_name is temporarily set via this serializer diff --git a/src/apps/competitions/migrations/0033_auto_20230617_1753.py b/src/apps/competitions/migrations/0033_auto_20230617_1753.py new file mode 100644 index 000000000..3603af13e --- /dev/null +++ b/src/apps/competitions/migrations/0033_auto_20230617_1753.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.17 on 2023-06-17 17:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('datasets', '0007_auto_20230609_1738'), + ('competitions', '0032_submission_worker_hostname'), + ] + + operations = [ + migrations.AddField( + model_name='phase', + name='public_data', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phase_public_data', to='datasets.Data'), + ), + migrations.AddField( + model_name='phase', + name='starting_kit', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phase_starting_kit', to='datasets.Data'), + ), + ] diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index f355802d1..6d89b4ad5 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -283,6 +283,9 @@ class Phase(ChaHubSaveMixin, models.Model): leaderboard = models.ForeignKey('leaderboards.Leaderboard', on_delete=models.DO_NOTHING, null=True, blank=True, related_name="phases") + public_data = models.ForeignKey('datasets.Data', on_delete=models.SET_NULL, null=True, blank=True, related_name="phase_public_data") + starting_kit = models.ForeignKey('datasets.Data', on_delete=models.SET_NULL, null=True, blank=True, related_name="phase_starting_kit") + class Meta: ordering = ('index',) diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index 96a1a1d16..0a6c38ac8 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -381,7 +381,6 @@ def mark_status_as_failed_and_delete_dataset(competition_creation_status, detail ) unpacker.unpack() - try: competition = unpacker.save() except ValidationError as e: diff --git a/src/apps/competitions/tests/unpacker_test_data.py b/src/apps/competitions/tests/unpacker_test_data.py index eb38945e9..f9e38b1a6 100644 --- a/src/apps/competitions/tests/unpacker_test_data.py +++ b/src/apps/competitions/tests/unpacker_test_data.py @@ -203,6 +203,8 @@ 'auto_migrate_to_this_phase': False, 'has_max_submissions': True, 'end': datetime.datetime(2019, 9, 30, 0, 0, tzinfo=timezone.now().tzinfo), + 'public_data': None, + 'starting_kit': None, 'tasks': [0], 'status': 'Previous', }, @@ -216,9 +218,11 @@ 'max_submissions_per_person': None, 'auto_migrate_to_this_phase': True, 'has_max_submissions': True, + 'end': None, + 'public_data': None, + 'starting_kit': None, 'tasks': [1], 'status': 'Current', - 'end': None, 'is_final_phase': True, } ] diff --git a/src/apps/competitions/unpackers/base_unpacker.py b/src/apps/competitions/unpackers/base_unpacker.py index 281069ca1..a769dc2eb 100644 --- a/src/apps/competitions/unpackers/base_unpacker.py +++ b/src/apps/competitions/unpackers/base_unpacker.py @@ -231,6 +231,7 @@ def _unpack_phases(self): "description": phase_description, "start": phase_start (datetime.datetime), "end": phase_end (datetime.datetime), + # BB public_data and starting_kit # ... See serializer for complete fields list "tasks": [list of indices that should match self.competition['tasks']] } @@ -315,6 +316,14 @@ def _save_competition(self): for phase in self.competition['phases']: phase['tasks'] = [self.competition['tasks'][index].key for index in phase['tasks']] phase['leaderboard'] = self.competition['leaderboards'][0].id + phase_public_data_file_data = phase['public_data'] + phase_starting_kit_file_data = phase['starting_kit'] + if phase_public_data_file_data is not None: + public_data_key, public_data_temp_data_path = self._get_data_key(**phase_public_data_file_data) + phase['public_data'] = Data.objects.filter(key=public_data_key)[0].id + if phase_starting_kit_file_data is not None: + starting_kit_key, starting_kit_temp_data_path = self._get_data_key(**phase_starting_kit_file_data) + phase['starting_kit'] = Data.objects.filter(key=starting_kit_key)[0].id self.competition.pop('leaderboards') diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index 449569667..1e4dee3cc 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -98,6 +98,27 @@ def _unpack_phases(self): else: new_phase['end'] = None + # Public Data and Starting Kit + try: + new_phase['public_data'] = { + 'file_name': phase['public_data'], + 'file_path': os.path.join(self.temp_directory, phase['public_data']), + 'file_type': 'public_data', + 'creator': self.creator.id, + } + except KeyError: + new_phase['public_data'] = None + + try: + new_phase['starting_kit'] = { + 'file_name': phase['starting_kit'], + 'file_path': os.path.join(self.temp_directory, phase['starting_kit']), + 'file_type': 'starting_kit', + 'creator': self.creator.id, + } + except KeyError: + new_phase['starting_kit'] = None + task_index = len(self.competition['tasks']) new_phase['tasks'] = [task_index] self.competition['phases'].append(new_phase) diff --git a/src/apps/competitions/unpackers/v2.py b/src/apps/competitions/unpackers/v2.py index f6bd941fb..4f5488f22 100644 --- a/src/apps/competitions/unpackers/v2.py +++ b/src/apps/competitions/unpackers/v2.py @@ -203,6 +203,27 @@ def _unpack_phases(self): if new_phase['max_submissions_per_day'] or new_phase['max_submissions_per_person']: new_phase['has_max_submissions'] = True + # Public Data and Starting Kit + try: + new_phase['public_data'] = { + 'file_name': phase_data['public_data'], + 'file_path': os.path.join(self.temp_directory, phase_data['public_data']), + 'file_type': 'public_data', + 'creator': self.creator.id, + } + except KeyError: + new_phase['public_data'] = None + + try: + new_phase['starting_kit'] = { + 'file_name': phase_data['starting_kit'], + 'file_path': os.path.join(self.temp_directory, phase_data['starting_kit']), + 'file_type': 'starting_kit', + 'creator': self.creator.id, + } + except KeyError: + new_phase['starting_kit'] = None + self.competition['phases'].append(new_phase) self._validate_phase_ordering() self._set_phase_statuses() diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 9a19090dd..cfa4f2e10 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -68,7 +68,6 @@ def get_context_data(self, **kwargs): def activate(request, uidb64, token): try: - # import pdb; pdb.set_trace(); uid = force_str(urlsafe_base64_decode(uidb64)) user = User.objects.get(pk=uid) except User.DoesNotExist: diff --git a/src/static/js/ours/utils.js b/src/static/js/ours/utils.js index 026be61d5..7407c5aa2 100644 --- a/src/static/js/ours/utils.js +++ b/src/static/js/ours/utils.js @@ -96,7 +96,30 @@ function get_form_fields(base_element) { //return $(':input', self.root).not('button').not('[readonly]').each(function (i, field) { // console.log(field) //}) - return $(':input', base_element).not('button').not('[readonly]') + form_fields = $(':input', base_element).not('button').not('[readonly]') + // Calendars come through as read-only and jQuery leaves them out + calendars = $('.two.fields .ui.calendar.field input[type="text"]') + readonly_calendars = $('.two.fields .ui.calendar.field [readonly]') + // If calendars is readonly_calendars, then append them to form_fields + if (calendars.length === readonly_calendars.length) { + var isIdentical = true; + calendars.each(function(index) { + if (!$(this).is(readonly_calendars.eq(index))) { + isIdentical = false; + return false; // exit the loop + } + }); + + if (isIdentical) { + form_fields = form_fields.add(calendars) + } else { + // console.log("The two sets are not identical."); + } + } else { + // console.log("The two sets have different lengths and are not identical."); + } + + return form_fields } function get_form_data(base_element) { @@ -170,134 +193,134 @@ function getBase64(file) { } /* - A simple, lightweight jQuery plugin for creating sortable tables. - https://github.com/kylefox/jquery-tablesort - Version 0.0.11 + A simple, lightweight jQuery plugin for creating sortable tables. + https://github.com/kylefox/jquery-tablesort + Version 0.0.11 */ (function($) { - $.tablesort = function ($table, settings) { - var self = this; - this.$table = $table; - this.$thead = this.$table.find('thead'); - this.settings = $.extend({}, $.tablesort.defaults, settings); - this.$sortCells = this.$thead.length > 0 ? this.$thead.find('th:not(.no-sort)') : this.$table.find('th:not(.no-sort)'); - this.$sortCells.on('click.tablesort', function() { - self.sort($(this)); - }); - this.index = null; - this.$th = null; - this.direction = null; - }; - - $.tablesort.prototype = { - - sort: function(th, direction) { - var start = new Date(), - self = this, - table = this.$table, - rowsContainer = table.find('tbody').length > 0 ? table.find('tbody') : table, - rows = rowsContainer.find('tr').has('td, th'), - cells = rows.find(':nth-child(' + (th.index() + 1) + ')').filter('td, th'), - sortBy = th.data().sortBy, - sortedMap = []; - - var unsortedValues = cells.map(function(idx, cell) { - if (sortBy) - return (typeof sortBy === 'function') ? sortBy($(th), $(cell), self) : sortBy; - return ($(this).data().sortValue != null ? $(this).data().sortValue : $(this).text()); - }); - if (unsortedValues.length === 0) return; - - //click on a different column - if (this.index !== th.index()) { - this.direction = 'asc'; - this.index = th.index(); - } - else if (direction !== 'asc' && direction !== 'desc') - this.direction = this.direction === 'asc' ? 'desc' : 'asc'; - else - this.direction = direction; - - direction = this.direction == 'asc' ? 1 : -1; - - self.$table.trigger('tablesort:start', [self]); - self.log("Sorting by " + this.index + ' ' + this.direction); - - // Try to force a browser redraw - self.$table.css("display"); - // Run sorting asynchronously on a timeout to force browser redraw after - // `tablesort:start` callback. Also avoids locking up the browser too much. - setTimeout(function() { - self.$sortCells.removeClass(self.settings.asc + ' ' + self.settings.desc); - for (var i = 0, length = unsortedValues.length; i < length; i++) - { - sortedMap.push({ - index: i, - cell: cells[i], - row: rows[i], - value: unsortedValues[i] - }); - } - - sortedMap.sort(function(a, b) { - return self.settings.compare(a.value, b.value) * direction; - }); - - $.each(sortedMap, function(i, entry) { - rowsContainer.append(entry.row); - }); - - th.addClass(self.settings[self.direction]); - - self.log('Sort finished in ' + ((new Date()).getTime() - start.getTime()) + 'ms'); - self.$table.trigger('tablesort:complete', [self]); - //Try to force a browser redraw - self.$table.css("display"); - }, unsortedValues.length > 2000 ? 200 : 10); - }, - - log: function(msg) { - if(($.tablesort.DEBUG || this.settings.debug) && console && console.log) { - console.log('[tablesort] ' + msg); - } - }, - - destroy: function() { - this.$sortCells.off('click.tablesort'); - this.$table.data('tablesort', null); - return null; - } - - }; - - $.tablesort.DEBUG = false; - - $.tablesort.defaults = { - debug: $.tablesort.DEBUG, - asc: 'sorted ascending', - desc: 'sorted descending', - compare: function(a, b) { - if (a > b) { - return 1; - } else if (a < b) { - return -1; - } else { - return 0; - } - } - }; - - $.fn.tablesort = function(settings) { - var table, sortable, previous; - return this.each(function() { - table = $(this); - previous = table.data('tablesort'); - if(previous) { - previous.destroy(); - } - table.data('tablesort', new $.tablesort(table, settings)); - }); - }; + $.tablesort = function ($table, settings) { + var self = this; + this.$table = $table; + this.$thead = this.$table.find('thead'); + this.settings = $.extend({}, $.tablesort.defaults, settings); + this.$sortCells = this.$thead.length > 0 ? this.$thead.find('th:not(.no-sort)') : this.$table.find('th:not(.no-sort)'); + this.$sortCells.on('click.tablesort', function() { + self.sort($(this)); + }); + this.index = null; + this.$th = null; + this.direction = null; + }; + + $.tablesort.prototype = { + + sort: function(th, direction) { + var start = new Date(), + self = this, + table = this.$table, + rowsContainer = table.find('tbody').length > 0 ? table.find('tbody') : table, + rows = rowsContainer.find('tr').has('td, th'), + cells = rows.find(':nth-child(' + (th.index() + 1) + ')').filter('td, th'), + sortBy = th.data().sortBy, + sortedMap = []; + + var unsortedValues = cells.map(function(idx, cell) { + if (sortBy) + return (typeof sortBy === 'function') ? sortBy($(th), $(cell), self) : sortBy; + return ($(this).data().sortValue != null ? $(this).data().sortValue : $(this).text()); + }); + if (unsortedValues.length === 0) return; + + //click on a different column + if (this.index !== th.index()) { + this.direction = 'asc'; + this.index = th.index(); + } + else if (direction !== 'asc' && direction !== 'desc') + this.direction = this.direction === 'asc' ? 'desc' : 'asc'; + else + this.direction = direction; + + direction = this.direction == 'asc' ? 1 : -1; + + self.$table.trigger('tablesort:start', [self]); + self.log("Sorting by " + this.index + ' ' + this.direction); + + // Try to force a browser redraw + self.$table.css("display"); + // Run sorting asynchronously on a timeout to force browser redraw after + // `tablesort:start` callback. Also avoids locking up the browser too much. + setTimeout(function() { + self.$sortCells.removeClass(self.settings.asc + ' ' + self.settings.desc); + for (var i = 0, length = unsortedValues.length; i < length; i++) + { + sortedMap.push({ + index: i, + cell: cells[i], + row: rows[i], + value: unsortedValues[i] + }); + } + + sortedMap.sort(function(a, b) { + return self.settings.compare(a.value, b.value) * direction; + }); + + $.each(sortedMap, function(i, entry) { + rowsContainer.append(entry.row); + }); + + th.addClass(self.settings[self.direction]); + + self.log('Sort finished in ' + ((new Date()).getTime() - start.getTime()) + 'ms'); + self.$table.trigger('tablesort:complete', [self]); + //Try to force a browser redraw + self.$table.css("display"); + }, unsortedValues.length > 2000 ? 200 : 10); + }, + + log: function(msg) { + if(($.tablesort.DEBUG || this.settings.debug) && console && console.log) { + console.log('[tablesort] ' + msg); + } + }, + + destroy: function() { + this.$sortCells.off('click.tablesort'); + this.$table.data('tablesort', null); + return null; + } + + }; + + $.tablesort.DEBUG = false; + + $.tablesort.defaults = { + debug: $.tablesort.DEBUG, + asc: 'sorted ascending', + desc: 'sorted descending', + compare: function(a, b) { + if (a > b) { + return 1; + } else if (a < b) { + return -1; + } else { + return 0; + } + } + }; + + $.fn.tablesort = function(settings) { + var table, sortable, previous; + return this.each(function() { + table = $(this); + previous = table.data('tablesort'); + if(previous) { + previous.destroy(); + } + table.data('tablesort', new $.tablesort(table, settings)); + }); + }; })(window.Zepto || window.jQuery); diff --git a/src/static/riot/competitions/detail/_tabs.tag b/src/static/riot/competitions/detail/_tabs.tag index 380ba377f..57ceacb71 100644 --- a/src/static/riot/competitions/detail/_tabs.tag +++ b/src/static/riot/competitions/detail/_tabs.tag @@ -29,9 +29,9 @@ data-tab="_tab_page{page.index}"> { page.title } - +
@@ -40,7 +40,7 @@
- + yes {filesize(file.file_size * 1024)} + + + + No Files Available Yet + + - --> + @@ -193,17 +204,103 @@ self.competition = competition self.competition.files = [] _.forEach(competition.phases, phase => { + _.forEach(phase.tasks, task => { + // Over complicated data org but it is so we can order exactly how we want... + let input_data = {} + let reference_data = {} + let ingestion_program = {} + let scoring_program = {} + _.forEach(task.public_datasets, dataset => { + let type = 'input_data' + if(dataset.type === "input_data"){ + type = 'Input Data' + input_data = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "reference_data"){ + type = 'Reference Data' + reference_data = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "ingestion_program"){ + type = 'Ingestion Program' + ingestion_program = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + }else if(dataset.type === "scoring_program"){ + type = 'Scoring Program' + scoring_program = {key: dataset.key, name: dataset.name, file_size: dataset.file_size, phase: phase.name, task: task.name, type: type} + } + }) + if(self.competition.admin){ + self.competition.files.push(input_data) + self.competition.files.push(reference_data) + self.competition.files.push(ingestion_program) + self.competition.files.push(scoring_program) + } + }) _.forEach(phase.tasks, task => { _.forEach(task.solutions, solution => { self.competition.files.push({ - key: solution.data.key, + key: solution.data, name: solution.name, - file_size: solution.data.file_size, + file_size: solution.size, phase: phase.name, task: task.name, + type: 'Solution' }) }) }) + // Need code for public_data and starting_kit at phase level + if(self.competition.participant_status === 'approved'){ + if (phase.starting_kit != null){ + self.competition.files.push({ + key: phase.starting_kit.key, + name: phase.starting_kit.name, + file_size: phase.starting_kit.file_size, + phase: phase.name, + task: '-', + type: 'Starting Kit' + }) + } + if (phase.public_data != null){ + self.competition.files.push({ + key: phase.public_data.key, + name: phase.public_data.name, + file_size: phase.public_data.file_size, + phase: phase.name, + task: '-', + type: 'Public Data' + }) + } + } + }) + // loop over competition phases to mark if phase has started or ended + self.competition.phases.forEach(function (phase, index) { + + phase_ended = false + phase_started = false + + // check if phase has started + if((Date.parse(phase["start"]) - Date.parse(new Date())) > 0){ + // start date is in the future, phase started = NO + phase_started = false + }else{ + // start date is not in the future, phase started = YES + phase_started = true + } + + if(phase_started){ + // check if end data exists for this phase + if(phase["end"]){ + if((Date.parse(phase["end"]) - Date.parse(new Date())) < 0){ + // Phase cannote accept submissions if end date is in the past + phase_ended = true + }else{ + // Phase can accept submissions if end date is in the future + phase_ended = false + } + }else{ + // Phase can accept submissions if end date is not given + phase_ended = false + } + } + self.competition.phases[index]["phase_ended"] = phase_ended + self.competition.phases[index]["phase_started"] = phase_started }) self.competition.is_admin = CODALAB.state.user.has_competition_admin_privileges(competition) diff --git a/src/static/riot/competitions/editor/_phases.tag b/src/static/riot/competitions/editor/_phases.tag index e2dc437c1..62305a354 100644 --- a/src/static/riot/competitions/editor/_phases.tag +++ b/src/static/riot/competitions/editor/_phases.tag @@ -96,6 +96,27 @@ multiple="multiple"> + +
+ + +
+
+ + +
@@ -176,6 +197,8 @@ self.form_is_valid = false self.phases = [] self.phase_tasks = [] + self.phase_public_data = [] + self.phase_starting_kit = [] self.selected_phase_index = undefined self.warnings = [] @@ -197,6 +220,29 @@ onRemove: self.task_removed, }) + $(self.refs.public_data_multiselect).dropdown({ + apiSettings: { + url: `${URLS.API}datasets/?search={query}&type=public_data`, + onResponse: (data) => { + return {success: true, results: _.values(data.results)} + }, + }, + onAdd: self.public_data_added, + onRemove: self.public_data_removed, + }) + + $(self.refs.starting_kit_multiselect).dropdown({ + apiSettings: { + url: `${URLS.API}datasets/?search={query}&type=starting_kit`, + onResponse: (data) => { + return {success: true, results: _.values(data.results)} + }, + }, + onAdd: self.starting_kit_added, + onRemove: self.starting_kit_removed, + }) + // When adding \ removing phase we need to code it like above + // Form change events $(':input', self.root).not('[type="file"]').not('button').not('[readonly]').each(function (i, field) { this.addEventListener('keyup', self.form_updated) @@ -219,13 +265,14 @@ /*--------------------------------------------------------------------- Methods ---------------------------------------------------------------------*/ + // Tasks self.task_added = (key, text, item) => { let index = _.findIndex(self.phase_tasks, (task) => { return task.value === key }) if (index === -1) { let task = {name: text, value: key, selected: true} - self.phase_tasks.push(task) + self.phase_tasks.push(task) } self.form_updated() } @@ -238,6 +285,68 @@ self.form_updated() } + // Public Data + self.public_data_added = (key, text, item) => { + let index = _.findIndex(self.phase_public_data, (public_data) => { + if (public_data === null){ + return false + }else{ + if (public_data.value != key){ + // Remove if not first selected. We can have only one. + alert("Only one Public Data set allowed per phase.") + setTimeout(()=>{$('a[data-value="'+ key +'"] .delete.icon').click()},100) + } + return public_data.value === key + } + }) + if (index === -1 && (self.phase_public_data.length === 0 || self.phase_public_data[0] === null)) { + let public_data = {name: text, value: key, selected: true} + self.phase_public_data[0] = public_data + } + self.form_updated() + } + + self.public_data_removed = (key, text, item) => { + let index = _.findIndex(self.phase_public_data, (public_data) => { + return public_data.value === key + }) + if (index != -1){ + self.phase_public_data.splice(index, 1) + } + self.form_updated() + } + + // Starting Kit + self.starting_kit_added = (key, text, item) => { + let index = _.findIndex(self.phase_starting_kit, (starting_kit) => { + if (starting_kit === null){ + return false + }else{ + if (starting_kit.value != key){ + // Remove if not first selected. We can have only one. + alert("Only one Starting Kit set allowed per phase.") + setTimeout(()=>{$('a[data-value="'+ key +'"] .delete.icon').click()},100) + } + return starting_kit.value === key + } + }) + if (index === -1 && (self.phase_starting_kit.length === 0 || self.phase_starting_kit[0] === null)) { + let starting_kit = {name: text, value: key, selected: true} + self.phase_starting_kit[0] = starting_kit + } + self.form_updated() + } + + self.starting_kit_removed = (key, text, item) => { + let index = _.findIndex(self.phase_starting_kit, (starting_kit) => { + return starting_kit.value === key + }) + self.phase_starting_kit.splice(index, 1) + self.form_updated() + } + + + self.show_modal = function () { $(self.refs.modal).modal('show') @@ -286,6 +395,7 @@ is_valid = false } else { // Make sure each phase has the proper details + // BB - check for public_data and starting_kit - NOT DONE self.phases.forEach(function (phase) { if (!phase.name || !phase.start || phase.tasks.length === 0) { is_valid = false @@ -338,6 +448,8 @@ self.selected_phase_index = undefined self.phase_tasks = [] $(self.refs.multiselect).dropdown('clear') + $(self.refs.public_data_multiselect).dropdown('clear') + $(self.refs.starting_kit_multiselect).dropdown('clear') $(':input', self.refs.form) .not('[type="file"]') @@ -369,6 +481,9 @@ self.selected_phase_index = index var phase = self.phases[index] self.phase_tasks = phase.tasks + self.phase_public_data = [phase.public_data] + self.phase_starting_kit = [phase.starting_kit] + self.update() set_form_data(phase, self.refs.form) @@ -389,11 +504,40 @@ selected: true, } })) + // Setting Public Data + if(self.phase_public_data[0] != null){ + $(self.refs.public_data_multiselect) + .dropdown('change values', _.map(self.phase_public_data, public_data => { + // renaming things to work w/ semantic UI multiselect + return { + value: public_data.value, + text: public_data.name, + name: public_data.name, + selected: true, + } + })) + } + // Setting Starting Kit + if(self.phase_starting_kit[0] != null){ + $(self.refs.starting_kit_multiselect) + .dropdown('change values', _.map(self.phase_starting_kit, starting_kit => { + // renaming things to work w/ semantic UI multiselect + return { + //value: starting_kit.value, // Maybe need to grab from serializer? + value: starting_kit.value, + text: starting_kit.name, + name: starting_kit.name, + selected: true, + } + })) + } self.show_modal() // make semantic multiselect sortable -- Sortable library imported in competitions/form.html Sortable.create($('.search.dropdown.multiple', self.refs.tasks_select_container)[0]) + Sortable.create($('.search.dropdown.multiple', self.refs.public_data_select_container)[0]) + Sortable.create($('.search.dropdown.multiple', self.refs.starting_kit_select_container)[0]) self.form_check_is_valid() self.update() @@ -432,8 +576,48 @@ }) self.phase_tasks = sorted_phase_tasks.slice() + // Get public data from DOM + let public_data_from_dom = [] + $("#public_data_select_container a").each(function () { + public_data_from_dom.push($(this).data("value")) + }) + let sorted_phase_public_data = [] + public_data_from_dom.forEach( function(key) { + let found = false; + self.phase_public_data = self.phase_public_data.filter(function (item) { + if(!found && item['value'] == key){ + sorted_phase_public_data.push(item) + found = true + return false + } else + return true + }) + }) + self.phase_public_data = sorted_phase_public_data.slice() + + // Get starting kit from DOM + let starting_kit_from_dom = [] + $("#starting_kit_select_container a").each(function () { + starting_kit_from_dom.push($(this).data("value")) + }) + let sorted_phase_starting_kit = [] + starting_kit_from_dom.forEach( function(key) { + let found = false; + self.phase_starting_kit = self.phase_starting_kit.filter(function (item) { + if(!found && item['value'] == key){ + sorted_phase_starting_kit.push(item) + found = true + return false + } else + return true + }) + }) + self.phase_starting_kit = sorted_phase_starting_kit.slice() + var data = get_form_data(self.refs.form) data.tasks = self.phase_tasks + data.public_data = self.phase_public_data.length === 0 ? null : self.phase_public_data[0] + data.starting_kit = self.phase_starting_kit.length === 0 ? null : self.phase_starting_kit[0] data.task_instances = [] for(task of self.phase_tasks){ data.task_instances.push({ diff --git a/src/static/riot/competitions/editor/form.tag b/src/static/riot/competitions/editor/form.tag index 184590ffc..6a877d383 100644 --- a/src/static/riot/competitions/editor/form.tag +++ b/src/static/riot/competitions/editor/form.tag @@ -241,7 +241,6 @@ // Send competition_id for either create or update, won't hurt anything but is // useless for creation - api_endpoint(self.competition_return, self.opts.competition_id) .done(function (response) { self.errors = {} diff --git a/src/static/riot/tasks/management.tag b/src/static/riot/tasks/management.tag index 0fdf320f2..d9013d630 100644 --- a/src/static/riot/tasks/management.tag +++ b/src/static/riot/tasks/management.tag @@ -462,7 +462,6 @@ self.search_tasks = function () { var filter = self.refs.search.value - delay(() => self.update_tasks({search: filter}), 100) }