Skip to content

Commit 2a52e28

Browse files
authored
Merge branch 'master' into slugg
2 parents bc9ccd7 + 76e39f8 commit 2a52e28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+4275
-63
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ notifications:
8181
email:
8282
on_success: change
8383
on_failure: always
84-
slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea
84+
slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea

apps/challenges/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin):
5858
"workers",
5959
"task_def_arn",
6060
"github_repository",
61+
"github_branch",
6162
)
6263
list_filter = (
6364
ChallengeFilter,
@@ -75,6 +76,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin):
7576
"creator__team_name",
7677
"slug",
7778
"github_repository",
79+
"github_branch",
7880
)
7981
actions = [
8082
"start_selected_workers",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 2.2.20 on 2025-07-09 15:00
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("challenges", "0112_challenge_sqs_retention_period"),
10+
]
11+
12+
operations = [
13+
migrations.RunSQL(
14+
sql=(
15+
"DO $$\n"
16+
"BEGIN\n"
17+
" IF NOT EXISTS (\n"
18+
" SELECT 1 FROM information_schema.columns\n"
19+
" WHERE table_name='challenge'\n"
20+
" AND column_name='github_branch'\n"
21+
" ) THEN\n"
22+
" ALTER TABLE challenge ADD COLUMN github_branch "
23+
"varchar(200) NULL DEFAULT '';\n"
24+
" END IF;\n"
25+
"END$$;"
26+
),
27+
reverse_sql=(
28+
"ALTER TABLE challenge DROP COLUMN IF EXISTS github_branch;"
29+
),
30+
),
31+
# Add a partial unique constraint
32+
# Only applies when both fields are not empty
33+
migrations.RunSQL(
34+
(
35+
"CREATE UNIQUE INDEX challenge_github_repo_branch_partial_idx "
36+
"ON challenge (github_repository, github_branch) "
37+
"WHERE github_repository != '' "
38+
"AND github_branch != '';"
39+
),
40+
reverse_sql=(
41+
"DROP INDEX IF EXISTS "
42+
"challenge_github_repo_branch_partial_idx;"
43+
),
44+
),
45+
]

apps/challenges/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ def __init__(self, *args, **kwargs):
186186
github_repository = models.CharField(
187187
max_length=1000, null=True, blank=True, default=""
188188
)
189+
# The github branch name used to create/update the challenge
190+
github_branch = models.CharField(
191+
max_length=200, null=True, blank=True, default=""
192+
)
189193
# The number of vCPU for a Fargate worker for the challenge. Default value
190194
# is 0.25 vCPU.
191195
worker_cpu_cores = models.IntegerField(null=True, blank=True, default=512)

apps/challenges/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Meta:
9494
"worker_instance_type",
9595
"sqs_retention_period",
9696
"github_repository",
97+
"github_branch",
9798
)
9899

99100

@@ -264,6 +265,9 @@ def __init__(self, *args, **kwargs):
264265
github_repository = context.get("github_repository")
265266
if github_repository:
266267
kwargs["data"]["github_repository"] = github_repository
268+
github_branch = context.get("github_branch")
269+
if github_branch:
270+
kwargs["data"]["github_branch"] = github_branch
267271

268272
class Meta:
269273
model = Challenge
@@ -302,6 +306,7 @@ class Meta:
302306
"max_docker_image_size",
303307
"cli_version",
304308
"github_repository",
309+
"github_branch",
305310
"vpc_cidr",
306311
"subnet_1_cidr",
307312
"subnet_2_cidr",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{% extends "base.html" %}
2+
{% block title %}Subscription Plan Details{% endblock %}
3+
4+
{% block content %}
5+
<div class="container mx-auto p-6">
6+
<h1 class="text-3xl font-bold mb-4">Subscription Plan Details for Your Challenge on EvalAI</h1>
7+
<p class="mb-4">Hi {{ host_name }},</p>
8+
<p class="mb-6">Thank you for your interest in hosting your challenge on EvalAI. We're excited to support your work.</p>
9+
10+
<p class="mb-4">We offer <strong>4 plans</strong> based on the compute needs of challenge hosts. Each plan is a monthly subscription (managed via Stripe) with no long-term commitment—you can cancel anytime once your challenge concludes. Stripe accepts all major payment methods and provides monthly receipts.</p>
11+
<p class="mb-6">Each plan includes one always-on worker to handle submissions (except for the Remote Evaluation Plan, which relies on your own infrastructure). If you expect higher submission volume—especially near deadlines—we can scale easily by adding additional workers. These extra workers will be billed separately according to your selected plan.</p>
12+
13+
<h2 class="text-2xl font-semibold mt-8 mb-4">Available Plans</h2>
14+
<div class="overflow-x-auto">
15+
<table class="table-auto w-full border-collapse">
16+
<thead>
17+
<tr class="bg-gray-200">
18+
<th class="px-4 py-2 border">Feature</th>
19+
<th class="px-4 py-2 border">Essentials<br><span class="text-sm">$125/mo</span></th>
20+
<th class="px-4 py-2 border">Core<br><span class="text-sm">$250/mo</span></th>
21+
<th class="px-4 py-2 border">Advanced<br><span class="text-sm">$850/mo</span></th>
22+
<th class="px-4 py-2 border">Remote Evaluation<br><span class="text-sm">$100/mo</span></th>
23+
</tr>
24+
</thead>
25+
<tbody>
26+
<tr class="hover:bg-gray-50">
27+
<td class="px-4 py-2 border font-medium">Compute Tier</td>
28+
<td class="px-4 py-2 border">0.5 vCPU, 1–4 GB RAM</td>
29+
<td class="px-4 py-2 border">1 vCPU, 2–4 GB RAM</td>
30+
<td class="px-4 py-2 border">1 vCPU, 4–16 GB RAM</td>
31+
<td class="px-4 py-2 border">Bring your own compute</td>
32+
</tr>
33+
<tr class="hover:bg-gray-50">
34+
<td class="px-4 py-2 border font-medium">Task Runtime</td>
35+
<td class="px-4 py-2 border">24/7 for 30 days</td>
36+
<td class="px-4 py-2 border">24/7 for 30 days</td>
37+
<td class="px-4 py-2 border">24/7 for 30 days</td>
38+
<td class="px-4 py-2 border">N/A</td>
39+
</tr>
40+
<tr class="hover:bg-gray-50">
41+
<td class="px-4 py-2 border font-medium">Ephemeral Storage</td>
42+
<td class="px-4 py-2 border">20 GB (expandable)</td>
43+
<td class="px-4 py-2 border">20 GB (expandable)</td>
44+
<td class="px-4 py-2 border">20 GB (expandable)</td>
45+
<td class="px-4 py-2 border">N/A</td>
46+
</tr>
47+
<tr class="hover:bg-gray-50">
48+
<td class="px-4 py-2 border font-medium">Concurrent Evaluations</td>
49+
<td class="px-4 py-2 border">1 container</td>
50+
<td class="px-4 py-2 border">1 container</td>
51+
<td class="px-4 py-2 border">1 container</td>
52+
<td class="px-4 py-2 border">N/A</td>
53+
</tr>
54+
<tr class="hover:bg-gray-50">
55+
<td class="px-4 py-2 border font-medium">Custom Dependencies</td>
56+
<td class="px-4 py-2 border">✔️</td>
57+
<td class="px-4 py-2 border">✔️</td>
58+
<td class="px-4 py-2 border">✔️</td>
59+
<td class="px-4 py-2 border">N/A</td>
60+
</tr>
61+
<tr class="hover:bg-gray-50">
62+
<td class="px-4 py-2 border font-medium">Submissions + Leaderboard Hosting</td>
63+
<td class="px-4 py-2 border">✔️</td>
64+
<td class="px-4 py-2 border">✔️</td>
65+
<td class="px-4 py-2 border">✔️</td>
66+
<td class="px-4 py-2 border">✔️</td>
67+
</tr>
68+
<tr class="hover:bg-gray-50">
69+
<td class="px-4 py-2 border font-medium">EvalAI Hosts the Evaluation</td>
70+
<td class="px-4 py-2 border">✔️</td>
71+
<td class="px-4 py-2 border">✔️</td>
72+
<td class="px-4 py-2 border">✔️</td>
73+
<td class="px-4 py-2 border"></td>
74+
</tr>
75+
</tbody>
76+
</table>
77+
</div>
78+
79+
<h2 class="text-2xl font-semibold mt-8 mb-4">Plan Subscription Links</h2>
80+
<ul class="list-disc list-inside space-y-2">
81+
<li><a href="https://buy.stripe.com/9B600i8Zt2PI8gA8srcEw01" class="text-blue-600 underline">Essentials Plan</a></li>
82+
<li><a href="https://buy.stripe.com/9B65kCcbF4XQdAUbEDcEw03" class="text-blue-600 underline">Core Plan</a></li>
83+
<li><a href="https://buy.stripe.com/28E28q1x1gGy40kcIHcEw06" class="text-blue-600 underline">Advanced Plan</a></li>
84+
<li><a href="https://buy.stripe.com/eVqfZg7Vp75Y8gA6kjcEw07" class="text-blue-600 underline">Remote Evaluation Plan</a></li>
85+
</ul>
86+
87+
<p class="mt-6">If you're unsure about which plan is the best fit for your challenge, we recommend starting with the <strong>Essentials plan</strong> and upgrading as needed.</p>
88+
<p class="mt-4">Please feel free to reach out if you have any questions or would like to discuss further.</p>
89+
<p class="mt-6">Looking forward to supporting your challenge!<br>EvalAI Team</p>
90+
</div>
91+
{% endblock %}

apps/challenges/utils.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
from botocore.exceptions import ClientError
1515
from django.conf import settings
1616
from django.core.files.base import ContentFile
17+
from django.core.mail import EmailMultiAlternatives
18+
from django.template.loader import render_to_string
1719
from moto import mock_ecr, mock_sts
20+
from participants.models import ParticipantTeam
1821

1922
from .models import (
2023
Challenge,
@@ -24,7 +27,6 @@
2427
ChallengeSponsor,
2528
DatasetSplit,
2629
Leaderboard,
27-
ParticipantTeam,
2830
)
2931
from .serializers import ChallengePrizeSerializer, ChallengeSponsorSerializer
3032

@@ -459,6 +461,106 @@ def send_emails(emails, template_id, template_data):
459461
)
460462

461463

464+
def send_subscription_plans_email(challenge):
465+
"""
466+
Sends email with subscription plan details to challenge hosts when they request approval
467+
Arguments:
468+
challenge {Class Object} -- Challenge model object
469+
"""
470+
try:
471+
# Get challenge host emails
472+
challenge_host_emails = (
473+
challenge.creator.get_all_challenge_host_email()
474+
)
475+
476+
if not challenge_host_emails:
477+
logger.warning(
478+
"No challenge host emails found for challenge {}".format(
479+
challenge.pk
480+
)
481+
)
482+
return
483+
484+
# Prepare template context
485+
challenge_url = "{}/web/challenges/challenge-page/{}".format(
486+
getattr(settings, "EVALAI_API_SERVER", "http://localhost:8000"),
487+
challenge.pk,
488+
)
489+
challenge_manage_url = (
490+
"{}/web/challenges/challenge-page/{}/manage".format(
491+
getattr(
492+
settings, "EVALAI_API_SERVER", "http://localhost:8000"
493+
),
494+
challenge.pk,
495+
)
496+
)
497+
498+
context = {
499+
"challenge_name": challenge.title,
500+
"challenge_url": challenge_url,
501+
"challenge_manage_url": challenge_manage_url,
502+
"challenge_id": challenge.pk,
503+
"host_team_name": challenge.creator.team_name,
504+
"support_email": getattr(
505+
settings, "CLOUDCV_TEAM_EMAIL", "[email protected]"
506+
),
507+
}
508+
509+
# Add challenge image if available
510+
if challenge.image:
511+
context["challenge_image_url"] = challenge.image.url
512+
513+
# Render the HTML template
514+
html_content = render_to_string(
515+
"challenges/subscription_plans_email.html", context
516+
)
517+
518+
# Create the email subject
519+
subject = f"EvalAI Subscription Plans - Challenge: {challenge.title}"
520+
521+
# Send emails to all challenge hosts
522+
emails_sent = 0
523+
for email in challenge_host_emails:
524+
try:
525+
# Send subscription plans email to challenge host
526+
email_message = EmailMultiAlternatives(
527+
subject=subject,
528+
body="Please view this email in HTML format.", # Plain text fallback
529+
from_email=getattr(
530+
settings, "CLOUDCV_TEAM_EMAIL", "[email protected]"
531+
),
532+
to=[email],
533+
)
534+
email_message.attach_alternative(html_content, "text/html")
535+
email_message.send()
536+
537+
emails_sent += 1
538+
logger.info(
539+
"Subscription plans email sent to {} for challenge {}".format(
540+
email, challenge.pk
541+
)
542+
)
543+
except Exception as e:
544+
logger.error(
545+
"Failed to send subscription plans email to {} for challenge {}: {}".format(
546+
email, challenge.pk, str(e)
547+
)
548+
)
549+
550+
logger.info(
551+
"Sent subscription plans email to {}/{} hosts for challenge {}".format(
552+
emails_sent, len(challenge_host_emails), challenge.pk
553+
)
554+
)
555+
556+
except Exception as e:
557+
logger.error(
558+
"Error sending subscription plans email for challenge {}: {}".format(
559+
challenge.pk, str(e)
560+
)
561+
)
562+
563+
462564
def parse_submission_meta_attributes(submission):
463565
"""
464566
Extracts submission attributes into Dict

apps/challenges/views.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
get_file_content,
157157
get_missing_keys_from_dict,
158158
send_emails,
159+
send_subscription_plans_email,
159160
)
160161

161162
logger = logging.getLogger(__name__)
@@ -3900,7 +3901,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk):
39003901
return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)
39013902

39023903
challenge_queryset = Challenge.objects.filter(
3903-
github_repository=request.data["GITHUB_REPOSITORY"]
3904+
github_repository=request.data["GITHUB_REPOSITORY"],
39043905
)
39053906

39063907
if challenge_queryset:
@@ -4827,6 +4828,14 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48274828
and send approval request for the challenge
48284829
"""
48294830
challenge = get_challenge_model(challenge_pk)
4831+
4832+
# Check if the user is a host of this challenge
4833+
if not is_user_a_host_of_challenge(request.user, challenge_pk):
4834+
response_data = {
4835+
"error": "Sorry, you are not authorized to request approval for this challenge!"
4836+
}
4837+
return Response(response_data, status=status.HTTP_403_FORBIDDEN)
4838+
48304839
challenge_phases = ChallengePhase.objects.filter(challenge=challenge)
48314840
unfinished_phases = []
48324841

@@ -4847,6 +4856,22 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48474856
{"error": error_message}, status=status.HTTP_406_NOT_ACCEPTABLE
48484857
)
48494858

4859+
# Send subscription plans email to challenge hosts
4860+
try:
4861+
send_subscription_plans_email(challenge)
4862+
logger.info(
4863+
"Subscription plans email sent successfully for challenge {}".format(
4864+
challenge_pk
4865+
)
4866+
)
4867+
except Exception as e:
4868+
logger.error(
4869+
"Failed to send subscription plans email for challenge {}: {}".format(
4870+
challenge_pk, str(e)
4871+
)
4872+
)
4873+
# Continue with the approval process even if email fails
4874+
48504875
if not settings.DEBUG:
48514876
try:
48524877
evalai_api_server = settings.EVALAI_API_SERVER
@@ -4888,7 +4913,7 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48884913
if webhook_response:
48894914
if webhook_response.content.decode("utf-8") == "ok":
48904915
response_data = {
4891-
"message": "Approval request sent!",
4916+
"message": "Approval request sent! You should also receive an email with subscription plan details.",
48924917
}
48934918
return Response(response_data, status=status.HTTP_200_OK)
48944919
else:

docker/dev/celery/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9.21
1+
FROM python:3.9.21-bullseye
22

33
ENV PYTHONUNBUFFERED 1
44

docker/dev/django/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9.21
1+
FROM python:3.9.21-bullseye
22

33
ENV PYTHONUNBUFFERED 1
44
ENV PIP_NO_CACHE_DIR=off

0 commit comments

Comments
 (0)