From f4bae4af063a66ab42a1dd1194a7f5bca8b464a8 Mon Sep 17 00:00:00 2001
From: Ananya
Date: Tue, 3 Mar 2026 21:19:39 +0530
Subject: [PATCH 01/10] feat: add captcha protection to user-facing forms
---
web/forms.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/web/forms.py b/web/forms.py
index 96489d524..b4799c7dd 100644
--- a/web/forms.py
+++ b/web/forms.py
@@ -578,6 +578,8 @@ def clean(self):
class ReviewForm(forms.ModelForm):
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
+
class Meta:
model = Review
fields = ("rating", "comment")
@@ -910,6 +912,7 @@ class SuccessStoryForm(forms.ModelForm):
content = MarkdownxFormField(
label="Content", help_text="Use markdown for formatting. You can use **bold**, *italic*, lists, etc."
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class Meta:
model = SuccessStory
@@ -929,6 +932,8 @@ class Meta:
class LearnForm(forms.ModelForm):
"""Form for creating and editing waiting rooms."""
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
+
class Meta:
model = WaitingRoom
fields = ["title", "description", "subject", "topics"]
@@ -1254,6 +1259,7 @@ class ForumTopicForm(forms.Form):
widget=TailwindURLInput(attrs={"placeholder": "https://github.com/your-org/your-repo/milestone/1"}),
help_text="Link to a related GitHub milestone (optional)",
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
def clean_github_issue_url(self):
url = self.cleaned_data.get("github_issue_url")
@@ -1308,6 +1314,8 @@ class Meta:
class BlogPostForm(forms.ModelForm):
"""Form for creating and editing blog posts."""
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
+
class Meta:
model = BlogPost
fields = ["title", "content", "excerpt", "featured_image", "status", "tags"]
@@ -1664,6 +1672,7 @@ class MemeForm(forms.ModelForm):
),
help_text="If your subject isn't listed, enter a new one here",
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class Meta:
model = Meme
@@ -1902,6 +1911,8 @@ class Meta:
class StudyGroupForm(forms.ModelForm):
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
+
class Meta:
model = StudyGroup
fields = ["name", "description", "course", "max_members", "is_private"]
@@ -1915,6 +1926,7 @@ class VideoRequestForm(forms.ModelForm):
# Only allow href, title and target attributes on anchor tags for security
"a": ["href", "title", "target"],
}
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class Meta:
model = VideoRequest
@@ -1965,6 +1977,7 @@ class SurveyForm(forms.ModelForm):
widget=TailwindInput(attrs={"placeholder": "Enter survey title"}),
help_text="Give your survey a clear and descriptive title",
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class Meta:
model = Survey
From cdcb7c5dfd805eb03e83d633750851217cfe4232 Mon Sep 17 00:00:00 2001
From: Ananya
Date: Tue, 3 Mar 2026 21:40:49 +0530
Subject: [PATCH 02/10] fixes
---
web/templates/add_meme.html | 7 +++++++
web/templates/success_stories/create.html | 9 +++++++++
web/templates/videos/submit_request.html | 14 ++++++++++++--
web/templates/web/forum/create_topic.html | 8 ++++++++
web/templates/web/forum/edit_topic.html | 8 ++++++++
5 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/web/templates/add_meme.html b/web/templates/add_meme.html
index e7c12a71e..f564e409d 100644
--- a/web/templates/add_meme.html
+++ b/web/templates/add_meme.html
@@ -65,6 +65,8 @@ Add a New Educational Meme
@@ -72,6 +74,11 @@
Add a New Educational Meme
+
+
+ {{ form.captcha }}
+ {% if form.captcha.errors %}
{{ form.captcha.errors.0 }}
{% endif %}
+
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
diff --git a/web/templates/success_stories/create.html b/web/templates/success_stories/create.html
index e2fb282d6..18cffe566 100644
--- a/web/templates/success_stories/create.html
+++ b/web/templates/success_stories/create.html
@@ -106,6 +106,15 @@
Draft: Save but don't publish yet. Published: Make visible to everyone.
+
+
+
+ {{ form.captcha }}
+ {% if form.captcha.errors %}
+
{{ form.captcha.errors }}
+ {% endif %}
+
-{% endblock %}
+{% endblock content %}
diff --git a/web/templates/web/forum/create_topic.html b/web/templates/web/forum/create_topic.html
index af6146f7f..c55ea488f 100644
--- a/web/templates/web/forum/create_topic.html
+++ b/web/templates/web/forum/create_topic.html
@@ -85,6 +85,14 @@
diff --git a/web/templates/videos/submit_request.html b/web/templates/videos/submit_request.html
index d6b271fb5..d32d75fcd 100644
--- a/web/templates/videos/submit_request.html
+++ b/web/templates/videos/submit_request.html
@@ -2,9 +2,7 @@
{% load static %}
-{% block title %}
- Request a Video
-{% endblock title %}
+{% block title %}Request a Video{% endblock %}
{% block content %}
@@ -58,4 +56,4 @@
Request an Educational Video
-{% endblock content %}
+{% endblock %}
diff --git a/web/templates/videos/upload.html b/web/templates/videos/upload.html
index de4a2fec6..10fd28b47 100644
--- a/web/templates/videos/upload.html
+++ b/web/templates/videos/upload.html
@@ -2,7 +2,7 @@
{% load static %}
-{% block title %}Upload Educational Video{% endblock %}
+{% block title %}Upload Educational Video{% endblock title %}
{% block content %}
@@ -94,6 +94,18 @@
There were errors
Describe what viewers will learn from this video
+
+ {% if form.captcha %}
+
+
+ {{ form.captcha }}
+ {% if form.captcha.errors %}
+
+ {% for error in form.captcha.errors %}
{{ error }}
{% endfor %}
+
+ {% endif %}
+
+ {% endif %}
-{% endblock %}
+{% endblock content %}
diff --git a/web/views.py b/web/views.py
index 53068490d..5ac2b0b9f 100644
--- a/web/views.py
+++ b/web/views.py
@@ -1134,7 +1134,7 @@ def run_cmd(cmd):
def send_slack_message(message):
webhook_url = os.getenv("SLACK_WEBHOOK_URL")
if not webhook_url:
- logger.warning("SLACK_WEBHOOK_URL not configured")
+ print("Warning: SLACK_WEBHOOK_URL not configured")
return
payload = {"text": f"```{message}```"}
@@ -3254,7 +3254,7 @@ def create_forum_category(request):
messages.success(request, f"Forum category '{category.name}' created successfully!")
return redirect("forum_category", slug=category.slug)
else:
- logger.warning("Forum category form validation failed: %s", form.errors)
+ print(form.errors)
else:
form = ForumCategoryForm()
@@ -3497,7 +3497,11 @@ def system_status(request):
status["sendgrid"]["message"] = f"API Error: {str(e)}"
else:
status["sendgrid"]["status"] = "error"
- status["sendgrid"]["message"] = "SendGrid API key not configured"
+
+ if settings.DEBUG:
+ status["sendgrid"]["message"] = "SendGrid API key not configured"
+ else:
+ status["sendgrid"]["message"] = "Email service unavailable"
# Check disk space
try:
@@ -4885,7 +4889,7 @@ def virtual_classroom_list(request):
return render(
request,
"virtual_classroom/list.html",
- {"classrooms": classrooms, "user": request.user}, # Pass the user object which includes the profile
+ {"classrooms": classrooms},
)
@@ -4893,7 +4897,6 @@ def virtual_classroom_list(request):
@require_POST
def join_global_virtual_classroom(request):
"""Join (or create) the global virtual classroom and redirect to it."""
-
teacher = User.objects.filter(is_staff=True, is_active=True).order_by("-is_superuser", "date_joined").first()
if not teacher:
@@ -4986,12 +4989,10 @@ def virtual_classroom_detail(request, classroom_id):
"""View to display a virtual classroom."""
classroom = get_object_or_404(VirtualClassroom, id=classroom_id)
- is_teacher = request.user == classroom.teacher
- can_access = can_access_classroom(request.user, classroom)
- is_enrolled = can_access and not is_teacher
-
# Check if user is teacher or enrolled student
- if not can_access:
+ is_teacher = request.user == classroom.teacher
+ is_enrolled = can_access_classroom(request.user, classroom)
+ if not is_enrolled:
messages.error(request, "You do not have access to this virtual classroom.")
if classroom.course:
return redirect("course_detail", slug=classroom.course.slug)
@@ -6067,7 +6068,7 @@ def upload_educational_video(request):
If user leaves title/description blank, we back‑fill from YouTube/Vimeo.
"""
if request.method == "POST":
- form = EducationalVideoForm(request.POST)
+ form = EducationalVideoForm(request.POST, user=request.user)
if form.is_valid():
video = form.save(commit=False)
if request.user.is_authenticated:
@@ -6092,7 +6093,7 @@ def upload_educational_video(request):
return JsonResponse({"success": False, "error": error_text}, status=400)
else:
- form = EducationalVideoForm()
+ form = EducationalVideoForm(user=request.user)
return render(request, "videos/upload.html", {"form": form})
From 0e402e036eb19516e6024f7870ab17ced8aa7a29 Mon Sep 17 00:00:00 2001
From: Ananya
Date: Tue, 17 Mar 2026 19:22:49 +0530
Subject: [PATCH 08/10] lint error fixed
---
web/forms.py | 1 +
web/templates/videos/upload.html | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/web/forms.py b/web/forms.py
index b16d745e2..b1edf4b76 100644
--- a/web/forms.py
+++ b/web/forms.py
@@ -872,6 +872,7 @@ def __init__(self, *args, **kwargs):
# If the user is authenticated, remove captcha field
if user and user.is_authenticated:
del self.fields["captcha"]
+
def clean_video_url(self):
url = self.cleaned_data.get("video_url", "").strip()
if not url:
diff --git a/web/templates/videos/upload.html b/web/templates/videos/upload.html
index 10fd28b47..78b097459 100644
--- a/web/templates/videos/upload.html
+++ b/web/templates/videos/upload.html
@@ -2,7 +2,9 @@
{% load static %}
-{% block title %}Upload Educational Video{% endblock title %}
+{% block title %}
+ Upload Educational Video
+{% endblock title %}
{% block content %}
From 971efc4f46420999d989518435b675b0cddfef1c Mon Sep 17 00:00:00 2001
From: Ananya
Date: Thu, 19 Mar 2026 21:26:39 +0530
Subject: [PATCH 09/10] revert: reset PR #1000 files to origin/main
---
web/forms.py | 9 ---------
web/templates/videos/upload.html | 18 ++----------------
web/views.py | 4 ++--
3 files changed, 4 insertions(+), 27 deletions(-)
diff --git a/web/forms.py b/web/forms.py
index b1edf4b76..96489d524 100644
--- a/web/forms.py
+++ b/web/forms.py
@@ -860,19 +860,12 @@ class Meta:
),
}
- captcha = CaptchaField(widget=TailwindCaptchaTextInput)
-
def __init__(self, *args, **kwargs):
- user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
# Order subjects by 'order' first, then alphabetically by 'name'
self.fields["category"].queryset = Subject.objects.all().order_by("order", "name")
- # If the user is authenticated, remove captcha field
- if user and user.is_authenticated:
- del self.fields["captcha"]
-
def clean_video_url(self):
url = self.cleaned_data.get("video_url", "").strip()
if not url:
@@ -936,8 +929,6 @@ class Meta:
class LearnForm(forms.ModelForm):
"""Form for creating and editing waiting rooms."""
- captcha = CaptchaField(widget=TailwindCaptchaTextInput)
-
class Meta:
model = WaitingRoom
fields = ["title", "description", "subject", "topics"]
diff --git a/web/templates/videos/upload.html b/web/templates/videos/upload.html
index 78b097459..de4a2fec6 100644
--- a/web/templates/videos/upload.html
+++ b/web/templates/videos/upload.html
@@ -2,9 +2,7 @@
{% load static %}
-{% block title %}
- Upload Educational Video
-{% endblock title %}
+{% block title %}Upload Educational Video{% endblock %}
{% block content %}
@@ -96,18 +94,6 @@
There were errors
Describe what viewers will learn from this video
-
- {% if form.captcha %}
-
-
- {{ form.captcha }}
- {% if form.captcha.errors %}
-
- {% for error in form.captcha.errors %}
{{ error }}
{% endfor %}
-
- {% endif %}
-
- {% endif %}
-{% endblock content %}
+{% endblock %}
diff --git a/web/views.py b/web/views.py
index 5ac2b0b9f..b4d485749 100644
--- a/web/views.py
+++ b/web/views.py
@@ -6068,7 +6068,7 @@ def upload_educational_video(request):
If user leaves title/description blank, we back‑fill from YouTube/Vimeo.
"""
if request.method == "POST":
- form = EducationalVideoForm(request.POST, user=request.user)
+ form = EducationalVideoForm(request.POST)
if form.is_valid():
video = form.save(commit=False)
if request.user.is_authenticated:
@@ -6093,7 +6093,7 @@ def upload_educational_video(request):
return JsonResponse({"success": False, "error": error_text}, status=400)
else:
- form = EducationalVideoForm(user=request.user)
+ form = EducationalVideoForm()
return render(request, "videos/upload.html", {"form": form})
From a01887403b20852b3c405cbc52426657c953f4b3 Mon Sep 17 00:00:00 2001
From: Ananya
Date: Thu, 19 Mar 2026 23:38:32 +0530
Subject: [PATCH 10/10] made all changes again
---
web/forms.py | 1 +
web/forms_additional.py | 3 +++
web/tests/test_educationalvideosupload.py | 23 +++++++++++++++++++++++
web/tests/test_forms.py | 10 ++++++++--
4 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/web/forms.py b/web/forms.py
index 96489d524..f032d7164 100644
--- a/web/forms.py
+++ b/web/forms.py
@@ -843,6 +843,7 @@ class EducationalVideoForm(forms.ModelForm):
),
help_text="Optional – what this video is about",
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class Meta:
model = EducationalVideo
diff --git a/web/forms_additional.py b/web/forms_additional.py
index 53fc35bd4..7ecdfcdef 100644
--- a/web/forms_additional.py
+++ b/web/forms_additional.py
@@ -1,8 +1,10 @@
+from captcha.fields import CaptchaField
from django import forms
from django.contrib.auth.models import User
from .models import BlogComment, Course, CourseMaterial, Review, StudyGroup
from .widgets import (
+ TailwindCaptchaTextInput,
TailwindCheckboxInput,
TailwindEmailInput,
TailwindFileInput,
@@ -56,6 +58,7 @@ class LearningInquiryForm(forms.Form):
],
widget=TailwindSelect(),
)
+ captcha = CaptchaField(widget=TailwindCaptchaTextInput)
class TeachingInquiryForm(forms.Form):
diff --git a/web/tests/test_educationalvideosupload.py b/web/tests/test_educationalvideosupload.py
index 5fedb2e7a..33e745afc 100644
--- a/web/tests/test_educationalvideosupload.py
+++ b/web/tests/test_educationalvideosupload.py
@@ -1,4 +1,5 @@
import json
+from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TestCase
@@ -11,6 +12,10 @@
class EducationalVideoUploadTests(TestCase):
def setUp(self):
+ self.captcha_patcher = patch("captcha.fields.CaptchaField.clean", return_value="PASSED")
+ self.captcha_patcher.start()
+ self.addCleanup(self.captcha_patcher.stop)
+
# create a user and two categories
self.user = User.objects.create_user(username="tester", password="password")
self.math = Subject.objects.create(name="Math", slug="math", order=1)
@@ -32,6 +37,8 @@ def test_post_upload_authenticated(self):
"video_url": "https://youtu.be/dQw4w9WgXcQ",
"description": "A great test",
"category": self.math.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data)
self.assertRedirects(resp, reverse("educational_videos_list"))
@@ -47,6 +54,8 @@ def test_post_upload_anonymous(self):
"video_url": "https://youtu.be/dQw4w9WgXcQ",
"description": "Anonymous desc",
"category": self.bio.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data)
self.assertRedirects(resp, reverse("educational_videos_list"))
@@ -63,6 +72,8 @@ def test_quick_add_ajax_missing_category(self):
"title": "Bad Quick",
"description": "",
"category": "",
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(resp.status_code, 400)
@@ -78,6 +89,8 @@ def test_quick_add_ajax_success(self):
"title": "Good Quick",
"description": "Auto desc",
"category": self.math.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(resp.status_code, 200)
@@ -93,6 +106,8 @@ def test_youtube_embed_url_validation(self):
"video_url": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"description": "Test embed URL",
"category": self.math.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data)
self.assertRedirects(resp, reverse("educational_videos_list"))
@@ -106,6 +121,8 @@ def test_youtube_embed_url_no_www_validation(self):
"video_url": "https://youtube.com/embed/dQw4w9WgXcQ",
"description": "Test embed URL without www",
"category": self.math.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data)
self.assertRedirects(resp, reverse("educational_videos_list"))
@@ -119,6 +136,8 @@ def test_vimeo_video_path_url_validation(self):
"video_url": "https://vimeo.com/video/123456789",
"description": "Test Vimeo video path URL",
"category": self.bio.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data)
self.assertRedirects(resp, reverse("educational_videos_list"))
@@ -132,6 +151,8 @@ def test_invalid_youtube_embed_short_id(self):
"video_url": "https://www.youtube.com/embed/shortid",
"description": "Test invalid embed URL",
"category": self.math.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(resp.status_code, 400)
@@ -147,6 +168,8 @@ def test_invalid_vimeo_short_id(self):
"video_url": "https://vimeo.com/video/1234567", # 7 digits, need 8+
"description": "Test invalid Vimeo URL",
"category": self.bio.id,
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
resp = self.client.post(self.upload_url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(resp.status_code, 400)
diff --git a/web/tests/test_forms.py b/web/tests/test_forms.py
index 9fb2f2a14..841aa240e 100644
--- a/web/tests/test_forms.py
+++ b/web/tests/test_forms.py
@@ -473,7 +473,8 @@ def test_invalid_message_form(self):
class LearningInquiryFormTests(TestCase):
- def test_valid_inquiry_form(self):
+ @patch("captcha.fields.CaptchaField.clean", return_value="PASSED")
+ def test_valid_inquiry_form(self, _mock_captcha_clean):
form_data = {
"name": "John Doe",
"email": "john@example.com",
@@ -481,11 +482,14 @@ def test_valid_inquiry_form(self):
"learning_goals": "I want to learn web development",
"preferred_schedule": "Weekends",
"experience_level": "beginner",
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
form = LearningInquiryForm(data=form_data)
self.assertTrue(form.is_valid())
- def test_invalid_inquiry_form(self):
+ @patch("captcha.fields.CaptchaField.clean", return_value="PASSED")
+ def test_invalid_inquiry_form(self, _mock_captcha_clean):
form_data = {
"name": "", # Name is required
"email": "invalid-email", # Invalid email
@@ -493,6 +497,8 @@ def test_invalid_inquiry_form(self):
"learning_goals": "", # Learning goals are required
"preferred_schedule": "", # Schedule is required
"experience_level": "invalid_level", # Invalid level
+ "captcha_0": "dummy-hash",
+ "captcha_1": "PASSED",
}
form = LearningInquiryForm(data=form_data)
self.assertFalse(form.is_valid())