Skip to content

Commit 76e39f8

Browse files
authored
[Fix #4675] Implement feature to send paid plan details to challenge hosts when requested for approval (#4701)
* implement email logic * Add templates + refine email logic * Add tests for paid plans email feature * Omit debugs * Update utils.ppy * implement email logic * Add templates + refine email logic * Add tests for paid plans email feature * Omit debugs * Update utils.ppy * Refine tests * Modify tests * remove unnecessary imports * Update docker-compose.yml * Handle undefined variables * Update test_views.py * Update env variables and tests * Update env variables and tests * Update email * fix isort checks * Resolve Flake8 checks * Update email template
1 parent 8cce5ad commit 76e39f8

File tree

6 files changed

+897
-8
lines changed

6 files changed

+897
-8
lines changed
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: 26 additions & 1 deletion
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__)
@@ -4812,6 +4813,14 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48124813
and send approval request for the challenge
48134814
"""
48144815
challenge = get_challenge_model(challenge_pk)
4816+
4817+
# Check if the user is a host of this challenge
4818+
if not is_user_a_host_of_challenge(request.user, challenge_pk):
4819+
response_data = {
4820+
"error": "Sorry, you are not authorized to request approval for this challenge!"
4821+
}
4822+
return Response(response_data, status=status.HTTP_403_FORBIDDEN)
4823+
48154824
challenge_phases = ChallengePhase.objects.filter(challenge=challenge)
48164825
unfinished_phases = []
48174826

@@ -4829,6 +4838,22 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48294838
{"error": error_message}, status=status.HTTP_406_NOT_ACCEPTABLE
48304839
)
48314840

4841+
# Send subscription plans email to challenge hosts
4842+
try:
4843+
send_subscription_plans_email(challenge)
4844+
logger.info(
4845+
"Subscription plans email sent successfully for challenge {}".format(
4846+
challenge_pk
4847+
)
4848+
)
4849+
except Exception as e:
4850+
logger.error(
4851+
"Failed to send subscription plans email for challenge {}: {}".format(
4852+
challenge_pk, str(e)
4853+
)
4854+
)
4855+
# Continue with the approval process even if email fails
4856+
48324857
if not settings.DEBUG:
48334858
try:
48344859
evalai_api_server = settings.EVALAI_API_SERVER
@@ -4870,7 +4895,7 @@ def request_challenge_approval_by_pk(request, challenge_pk):
48704895
if webhook_response:
48714896
if webhook_response.content.decode("utf-8") == "ok":
48724897
response_data = {
4873-
"message": "Approval request sent!",
4898+
"message": "Approval request sent! You should also receive an email with subscription plan details.",
48744899
}
48754900
return Response(response_data, status=status.HTTP_200_OK)
48764901
else:

settings/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@
297297

298298
# For inviting users to participant and host teams.
299299
ADMIN_EMAIL = "[email protected]"
300-
CLOUDCV_TEAM_EMAIL = "EvalAI Team <team@cloudcv.org>"
300+
CLOUDCV_TEAM_EMAIL = "team@eval.ai"
301301

302302
# Expiry time of a presigned url for uploading files to AWS, in seconds.
303303
PRESIGNED_URL_EXPIRY_TIME = 3600
@@ -327,7 +327,7 @@
327327
"TITLE": "EvalAI API",
328328
"DESCRIPTION": "EvalAI Documentation",
329329
"VERSION": "v1",
330-
"CONTACT": {"email": "team@cloudcv.org"},
330+
"CONTACT": {"email": "team@eval.ai"},
331331
"LICENSE": {"name": "BSD License"},
332332
}
333333

0 commit comments

Comments
 (0)