From 4fc411e20da7ed6784ae647b3f957bec989485ac Mon Sep 17 00:00:00 2001 From: didayolo Date: Sun, 21 Jan 2024 02:56:07 +0100 Subject: [PATCH 01/17] Clickable username in competition header --- src/static/riot/competitions/detail/_header.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/riot/competitions/detail/_header.tag b/src/static/riot/competitions/detail/_header.tag index 87d4fe870..d6cda97d4 100644 --- a/src/static/riot/competitions/detail/_header.tag +++ b/src/static/riot/competitions/detail/_header.tag @@ -41,7 +41,7 @@
Organized by: - {competition.created_by} + {competition.created_by} ({competition.contact_email})
From e9c8bd3606ddcefc9d441b4d401de4a38db54dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Sun, 21 Jan 2024 03:08:48 +0100 Subject: [PATCH 02/17] Replace "date of last entry" by "date" --- src/static/riot/competitions/detail/leaderboards.tag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index e21238b23..0617a38ea 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -35,7 +35,7 @@ # Participant Entries - Date of last entry + Date {column.title} @@ -58,7 +58,7 @@ { submission.owner } { submission.organization.name } {submission.num_entries} - {submission.last_entry_date} + {submission.created_when} From 2a941ab55ae265ce7d392d74de3d9af70bc9b537 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 22 Jan 2024 11:50:30 +0500 Subject: [PATCH 03/17] leaderboard date fixed --- src/apps/api/serializers/submissions.py | 4 +++- src/apps/api/views/competitions.py | 8 +------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/apps/api/serializers/submissions.py b/src/apps/api/serializers/submissions.py index ef3cdf39b..81e8de21c 100644 --- a/src/apps/api/serializers/submissions.py +++ b/src/apps/api/serializers/submissions.py @@ -86,6 +86,7 @@ class SubmissionLeaderBoardSerializer(serializers.ModelSerializer): display_name = serializers.CharField(source='owner.display_name') slug_url = serializers.CharField(source='owner.slug_url') organization = SimpleOrganizationSerializer(allow_null=True) + created_when = serializers.DateTimeField(format="%Y-%m-%d %H:%M") class Meta: model = Submission @@ -100,7 +101,8 @@ class Meta: 'display_name', 'slug_url', 'organization', - 'detailed_result' + 'detailed_result', + 'created_when' ) extra_kwargs = { "scores": {"read_only": True}, diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 565cdee27..3dc2f0bba 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -712,12 +712,6 @@ def get_leaderboard(self, request, pk): parent__isnull=False ).count() - # get date of last submission by the owner of this submission for this phase - last_entry_date = Submission.objects.filter(owner__username=submission['owner'], phase=phase)\ - .values('created_when')\ - .order_by('-created_when')[0]['created_when']\ - .strftime('%Y-%m-%d') - submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}" # gather detailed result from submissions for each task @@ -741,7 +735,7 @@ def get_leaderboard(self, request, pk): 'slug_url': submission['slug_url'], 'organization': submission['organization'], 'num_entries': num_entries, - 'last_entry_date': last_entry_date + 'created_when': submission['created_when'] }) for score in submission['scores']: From e293f359c99d0f8be6ef693d3bd357cea041d128 Mon Sep 17 00:00:00 2001 From: Benjamin Bearce Date: Mon, 22 Jan 2024 20:56:47 -0500 Subject: [PATCH 04/17] quota message details and link to user quote --- src/static/riot/competitions/detail/submission_upload.tag | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index 1e2d89508..406a8ee81 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -488,7 +488,9 @@ } } - toastr.error("Creation failed, error occurred") + toastr.error(`Creation failed, error occurred: ${response.responseJSON.data_file[0]}`) + setTimeout(()=>{toastr.warning(`Click HERE to go to the Resources tab to see your user quote.`)}, 5000) + }) .always(function () { setTimeout(self.hide_progress_bar, 500) From e9ae970b460753a928ee407fca390fe3412b0b6b Mon Sep 17 00:00:00 2001 From: didayolo Date: Tue, 23 Jan 2024 15:03:54 +0100 Subject: [PATCH 05/17] Update quota error messages --- src/apps/api/tests/test_datasets.py | 2 +- src/apps/api/views/datasets.py | 2 +- src/static/riot/competitions/detail/submission_upload.tag | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apps/api/tests/test_datasets.py b/src/apps/api/tests/test_datasets.py index 40b4dbf5d..944981757 100644 --- a/src/apps/api/tests/test_datasets.py +++ b/src/apps/api/tests/test_datasets.py @@ -65,7 +65,7 @@ def test_dataset_api_check_quota(self): }) assert resp.status_code == 400 - assert resp.data["data_file"][0] == f"Insufficient space. Your available space is {pretty_bytes(available_space)}. The file size is {pretty_bytes(file_size)}. Please free up some space and try again." + assert resp.data["data_file"][0] == f'Insufficient space. Your available space is {pretty_bytes(available_space)}. The file size is {pretty_bytes(file_size)}. Please free up some space and try again. You can manage your files in the Resources page.' # Fake upload a small file file_size = available_space - 1024 diff --git a/src/apps/api/views/datasets.py b/src/apps/api/views/datasets.py index 1f4fffa65..2175b9dc0 100644 --- a/src/apps/api/views/datasets.py +++ b/src/apps/api/views/datasets.py @@ -86,7 +86,7 @@ def create(self, request, *args, **kwargs): if storage_used + file_size > quota: available_space = pretty_bytes(quota - storage_used) file_size = pretty_bytes(file_size) - message = f"Insufficient space. Your available space is {available_space}. The file size is {file_size}. Please free up some space and try again." + message = f'Insufficient space. Your available space is {available_space}. The file size is {file_size}. Please free up some space and try again. You can manage your files in the Resources page.' return Response({'data_file': [message]}, status=status.HTTP_400_BAD_REQUEST) # All good, let's proceed diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index 406a8ee81..e463e899a 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -489,7 +489,7 @@ } } toastr.error(`Creation failed, error occurred: ${response.responseJSON.data_file[0]}`) - setTimeout(()=>{toastr.warning(`Click HERE to go to the Resources tab to see your user quote.`)}, 5000) + setTimeout(()=>{toastr.warning(`Click HERE to go to the Resources tab to see your storage quota and manage your files.`)}, 5000) }) .always(function () { From ec29a31b14b3561c07da2f4f22b9883b36bda1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Tue, 23 Jan 2024 17:43:53 +0100 Subject: [PATCH 06/17] Remove warning from submission_upload.tag --- src/static/riot/competitions/detail/submission_upload.tag | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index e463e899a..75ecc8b2f 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -489,8 +489,6 @@ } } toastr.error(`Creation failed, error occurred: ${response.responseJSON.data_file[0]}`) - setTimeout(()=>{toastr.warning(`Click HERE to go to the Resources tab to see your storage quota and manage your files.`)}, 5000) - }) .always(function () { setTimeout(self.hide_progress_bar, 500) From 9e48021a7010f194c8f01dfbabcfb1f5232b2582 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sun, 28 Jan 2024 16:10:36 +0500 Subject: [PATCH 07/17] Rerun submission, display error when a task is deleted and a submission using it is rerun --- src/apps/api/views/submissions.py | 9 ++++- src/apps/competitions/models.py | 37 ++++++++++++++++--- .../detail/submission_manager.tag | 6 ++- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/apps/api/views/submissions.py b/src/apps/api/views/submissions.py index b61963f9b..f9582488d 100644 --- a/src/apps/api/views/submissions.py +++ b/src/apps/api/views/submissions.py @@ -270,7 +270,14 @@ def re_run_submission(self, request, pk): rerun_kwargs = {} new_sub = submission.re_run(**rerun_kwargs) - return Response({'id': new_sub.id}) + if new_sub is None: + # return error + return Response({ + "error_msg": "You cannot rerun this submission because one or more tasks this submission was running are deleted, resubmit the submission or contact the competition organizer!"}, + status=status.HTTP_404_NOT_FOUND + ) + else: + return Response({'id': new_sub.id}) @action(detail=False, methods=('POST',)) def re_run_many_submissions(self, request): diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 4b87ec35a..a35141e54 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -539,24 +539,51 @@ def start(self, tasks=None): run_submission(self.pk, tasks=tasks) def re_run(self, task=None): + + # task to use in the new submission + new_submission_task = task or self.task + + # set is_specific_task_re_run + is_specific_task_re_run = bool(task) + + flag_rerun_specific_task_or_has_no_children = False + # Check if this submission needs to rerun on specific children or has no children + if not self.has_children or is_specific_task_re_run: + flag_rerun_specific_task_or_has_no_children = True + + # Check if task exists in case of specific task rerun or no children + if flag_rerun_specific_task_or_has_no_children and new_submission_task is None: + logger.error(f"Cannot rerun `{self}` because the task is None (deleted)") + return None + else: + children_tasks = self.children.values_list('task', flat=True) + if None in children_tasks: + logger.error(f"Cannot rerun `{self}` because one or more children submission tasks are None (deleted)") + return None + + # Create a new submission submission_arg_dict = { 'owner': self.owner, - 'task': task or self.task, + 'task': new_submission_task, 'phase': self.phase, 'data': self.data, 'has_children': self.has_children, - 'is_specific_task_re_run': bool(task), + 'is_specific_task_re_run': is_specific_task_re_run, 'fact_sheet_answers': self.fact_sheet_answers, } sub = Submission(**submission_arg_dict) sub.save(ignore_submission_limit=True) - # No need to rerun on children if this is running on a specific task - if not self.has_children or sub.is_specific_task_re_run: - self.refresh_from_db() + # set tasks for rerunning + if flag_rerun_specific_task_or_has_no_children: + # in case of a submission with no children or specific task rerun + # submission with no children is same as submission with one task tasks = [sub.task] else: + # in case submission has multiple children or multiple task rerun + # tasks are gathered from the children submissions tasks = Task.objects.filter(pk__in=self.children.values_list('task', flat=True)) + sub.start(tasks=tasks) return sub diff --git a/src/static/riot/competitions/detail/submission_manager.tag b/src/static/riot/competitions/detail/submission_manager.tag index 7b9aa43d7..e7a6f17ed 100644 --- a/src/static/riot/competitions/detail/submission_manager.tag +++ b/src/static/riot/competitions/detail/submission_manager.tag @@ -323,7 +323,11 @@ .fail(function (response) { if(response.responseJSON.detail){ toastr.error(response.responseJSON.detail) - } else { + } + else if(response.responseJSON.error_msg){ + toastr.error(response.responseJSON.error_msg) + } + else { toastr.error(response.responseText) } }) From cb7713756eba2ebd174f68c4b1bf3433162b0288 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sun, 28 Jan 2024 16:14:00 +0500 Subject: [PATCH 08/17] warning message displayed on task deletion --- src/static/riot/tasks/management.tag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/riot/tasks/management.tag b/src/static/riot/tasks/management.tag index 06d24bb2f..6ae6e34ca 100644 --- a/src/static/riot/tasks/management.tag +++ b/src/static/riot/tasks/management.tag @@ -639,7 +639,7 @@ } self.delete_task = function (task) { - if (confirm("Are you sure you want to delete '" + task.name + "'?")) { + if (confirm("Are you sure you want to delete '" + task.name + "'?\nSubmissions using this task cannot rerun!")) { CODALAB.api.delete_task(task.id) .done(function () { self.update_tasks() @@ -654,7 +654,7 @@ } self.delete_tasks = function () { - if (confirm(`Are you sure you want to delete multiple tasks?`)) { + if (confirm(`Are you sure you want to delete multiple tasks?\nSubmissions using these tasks cannot rerun!`)) { CODALAB.api.delete_tasks(self.marked_tasks) .done(function () { self.update_tasks() From 7a3f8ad454a3bb86fd56a7ba8faf62825221e01b Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 7 Feb 2024 15:14:18 +0500 Subject: [PATCH 09/17] files tab: show login if not loggedin + hide available column from non-organizers --- src/static/riot/competitions/detail/_tabs.tag | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/static/riot/competitions/detail/_tabs.tag b/src/static/riot/competitions/detail/_tabs.tag index 75bceec2a..1a9769790 100644 --- a/src/static/riot/competitions/detail/_tabs.tag +++ b/src/static/riot/competitions/detail/_tabs.tag @@ -50,17 +50,25 @@
- + +
- + +
+ Log In or + Sign Up to view availbale files. +
+ + +
- - From dd3f738ebeb46bf6c09385785d8b1bba2aaa8ed9 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 7 Feb 2024 20:49:40 +0500 Subject: [PATCH 10/17] do not delete original submission when are reun is deleted, similarly do not delete rerun submission when original is deleted --- src/apps/competitions/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 4b87ec35a..f3bc3823d 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -500,10 +500,17 @@ def __str__(self): return f"{self.phase.competition.title} submission PK={self.pk} by {self.owner.username}" def delete(self, **kwargs): + + # Check if any other submissions are using the same data + other_submissions_using_data = Submission.objects.filter(data=self.data).exclude(pk=self.pk).exists() + + if not other_submissions_using_data: + # If no other submissions are using the same data, delete it + self.data.delete() + # Also clean up details on delete self.details.all().delete() - # Call this here so that the data_file for the submission also gets deleted from storage - self.data.delete() + super().delete(**kwargs) def save(self, ignore_submission_limit=False, **kwargs): From a40e4e414cb0890b08d43a9d77314ba1ad95e469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Pav=C3=A3o?= Date: Thu, 8 Feb 2024 16:40:53 +0100 Subject: [PATCH 11/17] Add an example of competition docker image --- src/static/riot/competitions/editor/_competition_details.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/riot/competitions/editor/_competition_details.tag b/src/static/riot/competitions/editor/_competition_details.tag index 50f301342..c1121e555 100644 --- a/src/static/riot/competitions/editor/_competition_details.tag +++ b/src/static/riot/competitions/editor/_competition_details.tag @@ -109,7 +109,7 @@
- +
From dbe8f8817735b6348e673b6aca013ed58431b2ad Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 9 Feb 2024 15:22:08 +0500 Subject: [PATCH 12/17] warning message added --- src/static/riot/tasks/management.tag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/riot/tasks/management.tag b/src/static/riot/tasks/management.tag index 6ae6e34ca..92e2eb506 100644 --- a/src/static/riot/tasks/management.tag +++ b/src/static/riot/tasks/management.tag @@ -639,7 +639,7 @@ } self.delete_task = function (task) { - if (confirm("Are you sure you want to delete '" + task.name + "'?\nSubmissions using this task cannot rerun!")) { + if (confirm("Are you sure you want to delete '" + task.name + "'?\nSubmissions using this task cannot rerun! Results displayed on leaderboard can also be affected!")) { CODALAB.api.delete_task(task.id) .done(function () { self.update_tasks() @@ -654,7 +654,7 @@ } self.delete_tasks = function () { - if (confirm(`Are you sure you want to delete multiple tasks?\nSubmissions using these tasks cannot rerun!`)) { + if (confirm(`Are you sure you want to delete multiple tasks?\nSubmissions using these tasks cannot rerun! Results displayed on leaderboard can also be affected!`)) { CODALAB.api.delete_tasks(self.marked_tasks) .done(function () { self.update_tasks() From ac48c17c300aa5dcce4ddc4a5c4bb1c4f3449bc8 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Fri, 9 Feb 2024 16:43:38 +0500 Subject: [PATCH 13/17] show competiitons using the queue --- src/apps/api/serializers/queues.py | 25 ++++++++++++++++++++++++- src/static/riot/queues/management.tag | 19 ++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/apps/api/serializers/queues.py b/src/apps/api/serializers/queues.py index deea441d0..3614b61a2 100644 --- a/src/apps/api/serializers/queues.py +++ b/src/apps/api/serializers/queues.py @@ -3,8 +3,8 @@ from api.mixins import DefaultUserCreateMixin from queues.models import Queue - from profiles.models import User +from django.db.models import Q class OrganizerSerializer(serializers.ModelSerializer): @@ -61,6 +61,7 @@ class QueueSerializer(QueueOwnerMixin, serializers.ModelSerializer): is_owner = serializers.SerializerMethodField() owner = serializers.CharField(source='owner.username', read_only=True) organizers = OrganizerSerializer(many=True, read_only=True) + competitions = serializers.SerializerMethodField() class Meta: model = Queue @@ -74,6 +75,7 @@ class Meta: 'created_when', 'is_owner', 'id', + 'competitions', ) # This serializer is read only, basically.. read_only_fields = ( @@ -85,4 +87,25 @@ class Meta: 'broker_url', 'created_when', 'is_owner', + 'competitions', ) + + def get_competitions(self, obj): + # get user from the context request + user = self.context['request'].user + + # for super user return all competiitons using this queue + # for admin return competitions where this user is organizer using this queue + # for non-admin return public competitions using this queue + if user.is_superuser: + # Fetch all competitions + competitions = obj.competitions.all().values('id', 'title') + else: + # Fetch all competitions where user is organizer or competition is published + competitions = obj.competitions.filter( + Q(published=True) | + Q(created_by=user) | + Q(collaborators=user) + ).values('id', 'title') + + return competitions diff --git a/src/static/riot/queues/management.tag b/src/static/riot/queues/management.tag index be74a5132..ba138d6d9 100644 --- a/src/static/riot/queues/management.tag +++ b/src/static/riot/queues/management.tag @@ -133,11 +133,20 @@

Broker URL:

-
- -
-

Vhost

- {selected_queue.vhost} + {selected_queue.broker_url} + +

Vhost:

+ {selected_queue.vhost} + + +

Competitions using this queue:

+ +
Close
From 4f441b6f80c15aee0aa914670d1f611fd5907048 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 10 Feb 2024 00:01:46 +0500 Subject: [PATCH 14/17] Create Codabench Statistics (#1307) * generate codabench statistics * added comments, removed extra code, handled None values * flake * reward cleaned --- src/apps/competitions/statistics.py | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/apps/competitions/statistics.py diff --git a/src/apps/competitions/statistics.py b/src/apps/competitions/statistics.py new file mode 100644 index 000000000..d77cc624a --- /dev/null +++ b/src/apps/competitions/statistics.py @@ -0,0 +1,111 @@ +# -------------------------------------------------- +# Imports +# -------------------------------------------------- +import os +from competitions.models import Competition + +# -------------------------------------------------- +# Setting constants +# -------------------------------------------------- +BASE_URL = "https://www.codabench.org/competitions/" +STATISTICS_DIR = "/app/statistics/" +CSV_FILE_NAME = "codabench_competition_statistics.csv" +CSV_PATH = STATISTICS_DIR + CSV_FILE_NAME + + +def create_codabench_statistics(): + """ + This function prepares a CSV file with all published competitions + """ + + # Create statistics directory if not already createad + if not os.path.exists(STATISTICS_DIR): + os.makedirs(STATISTICS_DIR) + + # Write header of the CSV file + with open(CSV_PATH, 'w', newline='') as output_file: + # Header for the csv + header = 'title; description; participants; submissions; year; phases; reward; duration (days); url;\n' + output_file.write(header) + + # loop over published competitions + for comp in Competition.objects.filter(published=True): + + # get title + title = comp.title + title = clean_string(title) + + # get description + desc = comp.description + desc = clean_string(desc) + + # get participants + num_participants = comp.participants.count() + + # get phases + phases = comp.phases.all() + num_phases = len(phases) + + # get submissions + num_submissions = 0 + for phase in phases: + num_submissions += phase.submissions.count() + + # get competition first phase year + year = phases[0].start.year + + # get competition start and end date + start_date = phases[0].start + end_date = phases[num_phases - 1].end + # if last phase has no end date, set end date to last phase start date + if end_date is None: + end_date = phases[num_phases - 1].start + + # compute duration of the competition + duration = (end_date - start_date).days + + # get reward + reward = comp.reward + # set reward to empty string if none + if reward is None: + reward = "" + else: + reward = clean_string(reward) + + # prepare competition url + url = f"{BASE_URL}{comp.id}" + + # prepare a row with all the computed information for one competition + row = '{}; {}; {}; {}; {}; {}; {}; {}; {}; \n'.format( + title, + desc, + num_participants, + num_submissions, + year, + num_phases, + reward, + duration, + url + ) + + # write row in the CSV file + with open(CSV_PATH, 'a') as output_file: + output_file.write(row) + + +def clean_string(text): + """ + This function cleans an input text + """ + if ";" in text: + text = text.replace(";", ",") + + if '\n' in text: + text = text.replace(r'\n', ' ') + + if '\r' in text: + text = text.replace(r'\r', ' ') + + text = ''.join(text.splitlines()) + + return text From 61b0149066006d5b27db4fd182dfea4fe7899199 Mon Sep 17 00:00:00 2001 From: Benjamin Bearce Date: Fri, 9 Feb 2024 14:51:43 -0500 Subject: [PATCH 15/17] Logo Icons too big fix (Issue: 1290) (#1306) Logo Icons too big fix (Issue: 1290) (#1306) --- src/apps/api/serializers/competitions.py | 2 ++ .../migrations/0045_auto_20240129_2314.py | 19 +++++++++++ src/apps/competitions/models.py | 34 +++++++++++++++++++ src/static/riot/competitions/public-list.tag | 2 +- src/utils/data.py | 30 ++++++++++------ 5 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 src/apps/competitions/migrations/0045_auto_20240129_2314.py diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 2d1e1639c..e3bc20fb7 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -327,6 +327,7 @@ class CompetitionCreateSerializer(CompetitionSerializer): class CompetitionDetailSerializer(serializers.ModelSerializer): created_by = serializers.CharField(source='created_by.username', read_only=True) + logo_icon = NamedBase64ImageField(allow_null=True) pages = PageSerializer(many=True) phases = PhaseDetailSerializer(many=True) leaderboards = serializers.SerializerMethodField() @@ -347,6 +348,7 @@ class Meta: 'created_by', 'created_when', 'logo', + 'logo_icon', 'terms', 'pages', 'phases', diff --git a/src/apps/competitions/migrations/0045_auto_20240129_2314.py b/src/apps/competitions/migrations/0045_auto_20240129_2314.py new file mode 100644 index 000000000..2cb752724 --- /dev/null +++ b/src/apps/competitions/migrations/0045_auto_20240129_2314.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.17 on 2024-01-29 23:14 + +from django.db import migrations, models +import utils.data + + +class Migration(migrations.Migration): + + dependencies = [ + ('competitions', '0044_merge_20231221_1416'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='logo_icon', + field=models.ImageField(blank=True, null=True, upload_to=utils.data.PathWrapper('logos', manual_override=True)), + ), + ] diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index dc8e79b9a..1f96cc84e 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -1,9 +1,12 @@ import logging import uuid +import os +import io from django.conf import settings from django.contrib.sites.models import Site from django.contrib.postgres.fields import JSONField +from django.core.files.base import ContentFile from django.db import models from django.db.models import Q from django.urls import reverse @@ -15,6 +18,7 @@ from profiles.models import User, Organization from utils.data import PathWrapper from utils.storage import BundleStorage +from PIL import Image from tasks.models import Task @@ -32,6 +36,7 @@ class Competition(ChaHubSaveMixin, models.Model): title = models.CharField(max_length=256) logo = models.ImageField(upload_to=PathWrapper('logos'), null=True, blank=True) + logo_icon = models.ImageField(upload_to=PathWrapper('logos', manual_override=True), null=True, blank=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="competitions") created_when = models.DateTimeField(default=now) @@ -214,8 +219,37 @@ def get_chahub_data(self): return self.clean_private_data(data) + def make_logo_icon(self): + if self.logo: + # Read the content of the logo file + self.logo.name + self.logo_icon + icon_dirname_only = os.path.dirname(self.logo.name) # Get just the path + icon_basename_only = os.path.basename(self.logo.name) # Get just the filename + file_name = os.path.splitext(icon_basename_only)[0] + ext = os.path.splitext(icon_basename_only)[1] + new_path = os.path.join(icon_dirname_only, f"{file_name}_icon{ext}") + logo_content = self.logo.read() + original_logo = Image.open(io.BytesIO(logo_content)) + # Resize the image to a smaller size for logo_icon + width, height = original_logo.size + new_width = 100 # Specify the desired width for the logo_icon + new_height = int((new_width / width) * height) + resized_logo = original_logo.resize((new_width, new_height)) + # Create a BytesIO object to save the resized image + icon_content = io.BytesIO() + resized_logo.save(icon_content, format='PNG') + # Save the resized logo as logo_icon + self.logo_icon.save(new_path, ContentFile(icon_content.getvalue()), save=False) + def save(self, *args, **kwargs): super().save(*args, **kwargs) + if not self.logo: + pass + elif not self.logo_icon: + self.make_logo_icon() + elif os.path.dirname(self.logo.name) != os.path.dirname(self.logo_icon.name): + self.make_logo_icon() to_create = User.objects.filter( Q(id=self.created_by_id) | Q(id__in=self.collaborators.all().values_list('id', flat=True)) ).exclude(id__in=self.participants.values_list('user_id', flat=True)).distinct() diff --git a/src/static/riot/competitions/public-list.tag b/src/static/riot/competitions/public-list.tag index 2eb854a08..c9c8e4680 100644 --- a/src/static/riot/competitions/public-list.tag +++ b/src/static/riot/competitions/public-list.tag @@ -13,7 +13,7 @@
- +
diff --git a/src/utils/data.py b/src/utils/data.py index 0303888d6..7d743f361 100644 --- a/src/utils/data.py +++ b/src/utils/data.py @@ -19,20 +19,28 @@ class PathWrapper(object): """Helper to generate UUID's in file names while maintaining their extension""" - def __init__(self, base_directory): + def __init__(self, base_directory, manual_override=False): self.path = base_directory + self.manual_override = manual_override def __call__(self, instance, filename): - name, extension = os.path.splitext(filename) - truncated_uuid = uuid.uuid4().hex[0:12] - truncated_name = name[0:35] - - return os.path.join( - self.path, - now().strftime('%Y-%m-%d-%s'), - truncated_uuid, - "{0}{1}".format(truncated_name, extension) - ) + if not self.manual_override: + name, extension = os.path.splitext(filename) + truncated_uuid = uuid.uuid4().hex[0:12] + truncated_name = name[0:35] + + path = os.path.join( + self.path, + now().strftime('%Y-%m-%d-%s'), + truncated_uuid, + "{0}{1}".format(truncated_name, extension) + ) + else: + path = os.path.join( + filename + ) + + return path def make_url_sassy(path, permission='r', duration=60 * 60 * 24, content_type='application/zip'): From 62358ae2e823fe03a2e24bab0a4db50be6089581 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 10 Feb 2024 12:06:51 +0500 Subject: [PATCH 16/17] public competitions page error fixed --- src/apps/api/serializers/queues.py | 31 ++++++++++++++++++++++++++++++ src/apps/api/views/queues.py | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/apps/api/serializers/queues.py b/src/apps/api/serializers/queues.py index 3614b61a2..8b7e70090 100644 --- a/src/apps/api/serializers/queues.py +++ b/src/apps/api/serializers/queues.py @@ -61,6 +61,37 @@ class QueueSerializer(QueueOwnerMixin, serializers.ModelSerializer): is_owner = serializers.SerializerMethodField() owner = serializers.CharField(source='owner.username', read_only=True) organizers = OrganizerSerializer(many=True, read_only=True) + + class Meta: + model = Queue + fields = ( + 'name', + 'vhost', + 'is_public', + 'owner', + 'organizers', + 'broker_url', + 'created_when', + 'is_owner', + 'id', + ) + # This serializer is read only, basically.. + read_only_fields = ( + 'name', + 'vhost', + 'is_public', + 'owner', + 'organizers', + 'broker_url', + 'created_when', + 'is_owner', + ) + + +class QueueListSerializer(QueueOwnerMixin, serializers.ModelSerializer): + is_owner = serializers.SerializerMethodField() + owner = serializers.CharField(source='owner.username', read_only=True) + organizers = OrganizerSerializer(many=True, read_only=True) competitions = serializers.SerializerMethodField() class Meta: diff --git a/src/apps/api/views/queues.py b/src/apps/api/views/queues.py index db96188c6..23e486e85 100644 --- a/src/apps/api/views/queues.py +++ b/src/apps/api/views/queues.py @@ -13,7 +13,7 @@ class QueueViewSet(ModelViewSet): queryset = Queue.objects.all() - serializer_class = serializers.QueueSerializer + serializer_class = serializers.QueueListSerializer filter_fields = ('owner', 'is_public', 'name') filter_backends = (DjangoFilterBackend, SearchFilter) search_fields = ('name',) @@ -29,7 +29,7 @@ def get_queryset(self): def get_serializer_class(self): if self.request.method == 'GET': - return serializers.QueueSerializer + return serializers.QueueListSerializer else: return serializers.QueueCreationSerializer From 8f060153e60dbb916faac8e8c22767bf83e2c972 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 10 Feb 2024 19:38:15 +0500 Subject: [PATCH 17/17] code simplified --- src/apps/api/serializers/queues.py | 33 +++--------------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/apps/api/serializers/queues.py b/src/apps/api/serializers/queues.py index 8b7e70090..b3d84aee6 100644 --- a/src/apps/api/serializers/queues.py +++ b/src/apps/api/serializers/queues.py @@ -88,38 +88,11 @@ class Meta: ) -class QueueListSerializer(QueueOwnerMixin, serializers.ModelSerializer): - is_owner = serializers.SerializerMethodField() - owner = serializers.CharField(source='owner.username', read_only=True) - organizers = OrganizerSerializer(many=True, read_only=True) +class QueueListSerializer(QueueSerializer): competitions = serializers.SerializerMethodField() - class Meta: - model = Queue - fields = ( - 'name', - 'vhost', - 'is_public', - 'owner', - 'organizers', - 'broker_url', - 'created_when', - 'is_owner', - 'id', - 'competitions', - ) - # This serializer is read only, basically.. - read_only_fields = ( - 'name', - 'vhost', - 'is_public', - 'owner', - 'organizers', - 'broker_url', - 'created_when', - 'is_owner', - 'competitions', - ) + class Meta(QueueSerializer.Meta): + fields = QueueSerializer.Meta.fields + ('competitions',) def get_competitions(self, obj): # get user from the context request
Download Phase Task TypeAvailable Available @@ -80,7 +88,7 @@ {file.task} {file.type} + {filesize(file.file_size * 1024)}