From 733e2956d13b0352be4ce4f11706d380ce84fa3f Mon Sep 17 00:00:00 2001 From: MinhThieu145 Date: Thu, 4 Jul 2024 00:32:35 -0600 Subject: [PATCH 1/3] Add Interactive Buttons for Slack message to approve and disapprove message from Slack --- apps/base/utils.py | 19 ++++++--- apps/challenges/models.py | 33 +++++++++++++++ apps/challenges/urls.py | 5 +++ apps/challenges/views.py | 84 ++++++++++++++++++++++++++++++++++----- 4 files changed, 124 insertions(+), 17 deletions(-) diff --git a/apps/base/utils.py b/apps/base/utils.py index 406ecbd4da..18c872bbcf 100644 --- a/apps/base/utils.py +++ b/apps/base/utils.py @@ -251,12 +251,19 @@ def send_slack_notification(webhook=settings.SLACK_WEB_HOOK_URL, message=""): message {str} -- JSON/Text message to be sent to slack (default: {""}) """ try: - data = { - "attachments": [{"color": "ffaf4b", "fields": message["fields"]}], - "icon_url": "https://eval.ai/dist/images/evalai-logo-single.png", - "text": message["text"], - "username": "EvalAI", - } + # check if the message is a string or a dictionary + if isinstance(message, str): + data = { + "attachments": [ + {"color": "ffaf4b", "fields": message["fields"]} + ], + "icon_url": "https://eval.ai/dist/images/evalai-logo-single.png", + "text": message["text"], + "username": "EvalAI", + } + else: + data = message + return requests.post( webhook, data=json.dumps(data), diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 7539627878..b311436c8d 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -260,6 +260,39 @@ def is_active(self): return False +def slack_challenge_approval_callback(challenge_id): + field_name = "approved_by_admin" + from challenges.models import Challenge + import challenges.aws_utils as aws + + instance = Challenge.objects.get(id=challenge_id) + + if challenge_id: + challenge = Challenge.objects.filter(id=challenge_id).first() + if challenge: + created = False + else: + created = True + + if not created and is_model_field_changed(instance, field_name): + if ( + instance.approved_by_admin is True + and instance.is_docker_based is True + and instance.remote_evaluation is False + ): + serialized_obj = serializers.serialize("json", [instance]) + aws.setup_eks_cluster.delay(serialized_obj) + elif ( + instance.approved_by_admin is True + and instance.uses_ec2_worker is True + ): + serialized_obj = serializers.serialize("json", [instance]) + aws.setup_ec2.delay(serialized_obj) + aws.challenge_approval_callback( + instance=instance, field_name=field_name, sender="challenges.Challenge" + ) + + @receiver(signals.post_save, sender="challenges.Challenge") def create_eks_cluster_or_ec2_for_challenge(sender, instance, created, **kwargs): field_name = "approved_by_admin" diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py index 00c24e688a..d2b878d2ce 100644 --- a/apps/challenges/urls.py +++ b/apps/challenges/urls.py @@ -130,6 +130,11 @@ views.get_or_update_challenge_phase_split, name="get_or_update_challenge_phase_split", ), + url( + r"^challenge/slack_actions/$", + views.slack_actions, + name="challenge_slack_actions", + ), url( r"^(?P[0-9]+)/$", views.star_challenge, diff --git a/apps/challenges/views.py b/apps/challenges/views.py index b79e2fae41..37db96aae7 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -15,7 +15,7 @@ from os.path import basename, isfile, join from datetime import datetime - +from django.http import JsonResponse from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User @@ -24,7 +24,9 @@ from django.db import transaction from django.http import HttpResponse from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt +from rest_framework.permissions import AllowAny from rest_framework import permissions, status from rest_framework.decorators import ( api_view, @@ -4638,6 +4640,42 @@ def update_allowed_email_ids(request, challenge_pk, phase_pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +@api_view(["POST"]) +@authentication_classes([]) +@permission_classes([AllowAny]) +def slack_actions(request): + from . import models + + payload = json.loads(request.POST.get("payload", "{}")) + + action = payload["actions"][0] + action_type, challenge_id_str = action["value"].split("_") + + challenge_id = int(challenge_id_str) + + challenge = get_challenge_model(challenge_id) + + if action_type == "approve": + challenge.approved_by_admin = True + challenge.save() + + models.slack_challenge_approval_callback(challenge_id) + + return JsonResponse( + {"text": f"Challenge {challenge_id} has been approved"} + ) + + else: + challenge.approved_by_admin = False + challenge.save() + + models.slack_challenge_approval_callback(challenge_id) + + return JsonResponse( + {"text": f"Challenge {challenge_id} has been disapproved"} + ) + + @api_view(["GET"]) @throttle_classes([UserRateThrottle]) @permission_classes((permissions.IsAuthenticated, HasVerifiedEmail)) @@ -4679,17 +4717,41 @@ def request_challenge_approval_by_pk(request, challenge_pk): message = { "text": f"Challenge {challenge_pk} has finished submissions and has requested for approval!", - "fields": [ + "attachments": [ { - "title": "Admin URL", - "value": f"{evalai_api_server}/api/admin/challenges/challenge/{challenge_pk}", - "short": False, - }, - { - "title": "Challenge title", - "value": challenge.title, - "short": False, - }, + "fallback": "You are unable to make a decision.", + "callback_id": "challenge_approval", # Callback ID used to identify this particular interaction + "color": "#3AA3E3", + "attachment_type": "default", + "fields": [ + { + "title": "Admin URL", + "value": f"{evalai_api_server}/api/admin/challenges/challenge/{challenge_pk}", + "short": False, + }, + { + "title": "Challenge title", + "value": challenge.title, + "short": False, + }, + ], + "actions": [ + { + "name": "approval", + "text": "Yes", + "type": "button", + "value": f"approve_{challenge_pk}", + "style": "primary", + }, + { + "name": "approval", + "text": "No", + "type": "button", + "value": f"disapprove_{challenge_pk}", + "style": "danger", + }, + ], + } ], } From 499fc0efc588c497b4643e64bc3463e03e98a03d Mon Sep 17 00:00:00 2001 From: MinhThieu145 Date: Thu, 4 Jul 2024 00:37:16 -0600 Subject: [PATCH 2/3] chore: Remove unused import statement in challenges/views.py --- apps/challenges/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 37db96aae7..7116a21509 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -24,7 +24,6 @@ from django.db import transaction from django.http import HttpResponse from django.utils import timezone -from django.views.decorators.csrf import csrf_exempt from rest_framework.permissions import AllowAny from rest_framework import permissions, status From b2815fdae4dcf37d876e96525d1f6426590be4f7 Mon Sep 17 00:00:00 2001 From: MinhThieu145 Date: Thu, 4 Jul 2024 01:25:34 -0600 Subject: [PATCH 3/3] chore: Verify Slack token in slack_actions view --- apps/challenges/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 7116a21509..8b0adae067 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -4654,6 +4654,13 @@ def slack_actions(request): challenge = get_challenge_model(challenge_id) + # Verify the token + slack_token = payload.get('token') + expected_token = os.getenv('SLACK_VERIFICATION_TOKEN') + + if slack_token != expected_token: + return JsonResponse({"error": "Invalid token"}, status=403) + if action_type == "approve": challenge.approved_by_admin = True challenge.save()