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

Preview

@@ -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 @@

{{ form.github_milestone_url.errors }}
{% endif %} +
+ + {{ form.captcha }} + {% if form.captcha.errors %} +
{{ form.captcha.errors }}
+ {% endif %} +
+
+ + {{ form.captcha }} + {% if form.captcha.errors %} +
{{ form.captcha.errors }}
+ {% endif %} +
- + {{ form.captcha }} - {% if form.captcha.errors %}

{{ form.captcha.errors.0 }}

{% endif %} + {% if form.captcha.errors %} +

{{ form.captcha.errors.0 }}

+ {% endif %}
{% if form.non_field_errors %}
diff --git a/web/templates/web/forum/create_topic.html b/web/templates/web/forum/create_topic.html index c55ea488f..16b99777c 100644 --- a/web/templates/web/forum/create_topic.html +++ b/web/templates/web/forum/create_topic.html @@ -87,7 +87,7 @@

+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CAPTCHA {{ form.captcha }} {% if form.captcha.errors %}
{{ form.captcha.errors }}
diff --git a/web/templates/web/forum/edit_topic.html b/web/templates/web/forum/edit_topic.html index 608304875..3eccfaa5f 100644 --- a/web/templates/web/forum/edit_topic.html +++ b/web/templates/web/forum/edit_topic.html @@ -68,7 +68,7 @@

Edit Topic

+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CAPTCHA {{ form.captcha }} {% if form.captcha.errors %}
{{ form.captcha.errors }}
From 5d81923ac91eb080c327019bbc2253976a105697 Mon Sep 17 00:00:00 2001 From: Ananya Date: Mon, 16 Mar 2026 00:20:41 +0530 Subject: [PATCH 04/10] remove captcha for authenticated users --- web/forms.py | 11 --------- web/migrations/0052_add_meme_slug.py | 8 +++---- web/templates/add_meme.html | 8 ------- web/templates/success_stories/create.html | 9 ------- web/templates/videos/submit_request.html | 8 ------- web/templates/web/forum/create_topic.html | 8 ------- web/templates/web/forum/edit_topic.html | 8 ------- web/views.py | 29 +++++++++++++---------- 8 files changed, 21 insertions(+), 68 deletions(-) diff --git a/web/forms.py b/web/forms.py index b4799c7dd..14fa42bd3 100644 --- a/web/forms.py +++ b/web/forms.py @@ -578,8 +578,6 @@ def clean(self): class ReviewForm(forms.ModelForm): - captcha = CaptchaField(widget=TailwindCaptchaTextInput) - class Meta: model = Review fields = ("rating", "comment") @@ -912,7 +910,6 @@ 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 @@ -1259,7 +1256,6 @@ 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") @@ -1314,8 +1310,6 @@ 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"] @@ -1672,7 +1666,6 @@ 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 @@ -1911,8 +1904,6 @@ class Meta: class StudyGroupForm(forms.ModelForm): - captcha = CaptchaField(widget=TailwindCaptchaTextInput) - class Meta: model = StudyGroup fields = ["name", "description", "course", "max_members", "is_private"] @@ -1926,7 +1917,6 @@ 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 @@ -1977,7 +1967,6 @@ 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 diff --git a/web/migrations/0052_add_meme_slug.py b/web/migrations/0052_add_meme_slug.py index c1063ba5b..e0d0d9975 100644 --- a/web/migrations/0052_add_meme_slug.py +++ b/web/migrations/0052_add_meme_slug.py @@ -37,17 +37,17 @@ def add_slug_column_if_not_exists(apps, schema_editor): column_name = "slug" if vendor == "sqlite": - cursor.execute(f"PRAGMA table_info({table_name})") + cursor.execute("PRAGMA table_info(web_meme)") columns = [info[1] for info in cursor.fetchall()] if column_name in columns: return - cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} VARCHAR(255) DEFAULT ''") + cursor.execute("ALTER TABLE web_meme ADD COLUMN slug VARCHAR(255) DEFAULT ''") elif vendor == "mysql": - cursor.execute(f"SHOW COLUMNS FROM {table_name} LIKE '{column_name}'") + cursor.execute("SHOW COLUMNS FROM web_meme LIKE %s", [column_name]) if cursor.fetchone(): return - cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} VARCHAR(255) DEFAULT ''") + cursor.execute("ALTER TABLE web_meme ADD COLUMN slug VARCHAR(255) DEFAULT ''") elif vendor == "postgresql": # Add column if not exists for PostgreSQL diff --git a/web/templates/add_meme.html b/web/templates/add_meme.html index 89ffab297..61e1f3116 100644 --- a/web/templates/add_meme.html +++ b/web/templates/add_meme.html @@ -74,14 +74,6 @@

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 18cffe566..e2fb282d6 100644 --- a/web/templates/success_stories/create.html +++ b/web/templates/success_stories/create.html @@ -106,15 +106,6 @@

Draft: Save but don't publish yet. Published: Make visible to everyone.

- -
- - {{ form.captcha }} - {% if form.captcha.errors %} -

{{ form.captcha.errors }}

- {% endif %} -

{{ form.description.errors.0 }}

{% endif %} -
- - {{ form.captcha }} - {% if form.captcha.errors %} -

{{ form.captcha.errors.0 }}

- {% endif %} -
-
- - {{ form.captcha }} - {% if form.captcha.errors %} -
{{ form.captcha.errors }}
- {% endif %} -
-
- - {{ form.captcha }} - {% if form.captcha.errors %} -
{{ form.captcha.errors }}
- {% endif %} -
diff --git a/web/views.py b/web/views.py index b4d485749..02028a547 100644 --- a/web/views.py +++ b/web/views.py @@ -192,7 +192,6 @@ from .referrals import send_referral_reward_email from .social import get_social_stats from .utils import ( - can_access_classroom, cancel_subscription, create_leaderboard_context, create_subscription, @@ -1134,7 +1133,7 @@ def run_cmd(cmd): def send_slack_message(message): webhook_url = os.getenv("SLACK_WEBHOOK_URL") if not webhook_url: - print("Warning: SLACK_WEBHOOK_URL not configured") + logger.warning("SLACK_WEBHOOK_URL not configured") return payload = {"text": f"```{message}```"} @@ -3254,7 +3253,7 @@ def create_forum_category(request): messages.success(request, f"Forum category '{category.name}' created successfully!") return redirect("forum_category", slug=category.slug) else: - print(form.errors) + logger.error(f"Form errors: {form.errors}") else: form = ForumCategoryForm() @@ -3497,11 +3496,7 @@ def system_status(request): status["sendgrid"]["message"] = f"API Error: {str(e)}" else: status["sendgrid"]["status"] = "error" - - if settings.DEBUG: - status["sendgrid"]["message"] = "SendGrid API key not configured" - else: - status["sendgrid"]["message"] = "Email service unavailable" + status["sendgrid"]["message"] = "SendGrid API key not configured" # Check disk space try: @@ -4889,7 +4884,7 @@ def virtual_classroom_list(request): return render( request, "virtual_classroom/list.html", - {"classrooms": classrooms}, + {"classrooms": classrooms, "user": request.user}, # Pass the user object which includes the profile ) @@ -4897,6 +4892,7 @@ 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: @@ -4991,12 +4987,21 @@ def virtual_classroom_detail(request, classroom_id): # Check if user is teacher or enrolled student is_teacher = request.user == classroom.teacher - is_enrolled = can_access_classroom(request.user, classroom) - if not is_enrolled: + is_enrolled = False + + if classroom.course: + # For classrooms with a course, check course enrollments + is_enrolled = classroom.course.enrollments.filter(student=request.user, status="approved").exists() + else: + # For standalone classrooms, check VirtualClassroomParticipant table + is_enrolled = VirtualClassroomParticipant.objects.filter(classroom=classroom, user=request.user).exists() + + if not (is_teacher or 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) - return redirect("virtual_classroom_list") + else: + return redirect("virtual_classroom_list") # Get or create customization settings to prevent DoesNotExist errors customization, created = VirtualClassroomCustomization.objects.get_or_create( From a935f83e1520fb5accd6a64872ac57e2b870c7cc Mon Sep 17 00:00:00 2001 From: Ananya Date: Mon, 16 Mar 2026 00:39:46 +0530 Subject: [PATCH 05/10] fix: use lazy logging and restore can_access_classroom helper in virtual classroom --- web/views.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/web/views.py b/web/views.py index 02028a547..1a97bb69d 100644 --- a/web/views.py +++ b/web/views.py @@ -192,6 +192,7 @@ from .referrals import send_referral_reward_email from .social import get_social_stats from .utils import ( + can_access_classroom, cancel_subscription, create_leaderboard_context, create_subscription, @@ -3253,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.error(f"Form errors: {form.errors}") + logger.warning("Forum category form validation failed: %s", form.errors) else: form = ForumCategoryForm() @@ -4985,18 +4986,12 @@ def virtual_classroom_detail(request, classroom_id): """View to display a virtual classroom.""" classroom = get_object_or_404(VirtualClassroom, id=classroom_id) - # Check if user is teacher or enrolled student is_teacher = request.user == classroom.teacher - is_enrolled = False - - if classroom.course: - # For classrooms with a course, check course enrollments - is_enrolled = classroom.course.enrollments.filter(student=request.user, status="approved").exists() - else: - # For standalone classrooms, check VirtualClassroomParticipant table - is_enrolled = VirtualClassroomParticipant.objects.filter(classroom=classroom, user=request.user).exists() + can_access = can_access_classroom(request.user, classroom) + is_enrolled = can_access and not is_teacher - if not (is_teacher or is_enrolled): + # Check if user is teacher or enrolled student + if not can_access: messages.error(request, "You do not have access to this virtual classroom.") if classroom.course: return redirect("course_detail", slug=classroom.course.slug) From 668183a892fab7896530c9bdf7e22fc9fc8a9a72 Mon Sep 17 00:00:00 2001 From: Ananya Date: Mon, 16 Mar 2026 01:10:20 +0530 Subject: [PATCH 06/10] minor readability fix --- web/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/views.py b/web/views.py index 1a97bb69d..53068490d 100644 --- a/web/views.py +++ b/web/views.py @@ -4995,8 +4995,7 @@ def virtual_classroom_detail(request, classroom_id): messages.error(request, "You do not have access to this virtual classroom.") if classroom.course: return redirect("course_detail", slug=classroom.course.slug) - else: - return redirect("virtual_classroom_list") + return redirect("virtual_classroom_list") # Get or create customization settings to prevent DoesNotExist errors customization, created = VirtualClassroomCustomization.objects.get_or_create( From cc430565c9a5506e8b06e2d5deaf852ca1d76912 Mon Sep 17 00:00:00 2001 From: Ananya Date: Tue, 17 Mar 2026 19:16:15 +0530 Subject: [PATCH 07/10] "reset hard to make proper changes" --- web/forms.py | 6 ++++++ web/migrations/0052_add_meme_slug.py | 8 ++++---- web/templates/add_meme.html | 2 -- web/templates/videos/submit_request.html | 6 ++---- web/templates/videos/upload.html | 16 +++++++++++++-- web/views.py | 25 ++++++++++++------------ 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/web/forms.py b/web/forms.py index 14fa42bd3..b16d745e2 100644 --- a/web/forms.py +++ b/web/forms.py @@ -860,12 +860,18 @@ 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: diff --git a/web/migrations/0052_add_meme_slug.py b/web/migrations/0052_add_meme_slug.py index e0d0d9975..c1063ba5b 100644 --- a/web/migrations/0052_add_meme_slug.py +++ b/web/migrations/0052_add_meme_slug.py @@ -37,17 +37,17 @@ def add_slug_column_if_not_exists(apps, schema_editor): column_name = "slug" if vendor == "sqlite": - cursor.execute("PRAGMA table_info(web_meme)") + cursor.execute(f"PRAGMA table_info({table_name})") columns = [info[1] for info in cursor.fetchall()] if column_name in columns: return - cursor.execute("ALTER TABLE web_meme ADD COLUMN slug VARCHAR(255) DEFAULT ''") + cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} VARCHAR(255) DEFAULT ''") elif vendor == "mysql": - cursor.execute("SHOW COLUMNS FROM web_meme LIKE %s", [column_name]) + cursor.execute(f"SHOW COLUMNS FROM {table_name} LIKE '{column_name}'") if cursor.fetchone(): return - cursor.execute("ALTER TABLE web_meme ADD COLUMN slug VARCHAR(255) DEFAULT ''") + cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} VARCHAR(255) DEFAULT ''") elif vendor == "postgresql": # Add column if not exists for PostgreSQL diff --git a/web/templates/add_meme.html b/web/templates/add_meme.html index 61e1f3116..e7c12a71e 100644 --- a/web/templates/add_meme.html +++ b/web/templates/add_meme.html @@ -65,8 +65,6 @@

Add a New Educational Meme

Preview

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())