From 69aba729a4037aa5e5947097f6422ca87118bccf Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Fri, 21 Mar 2025 00:56:02 +0530 Subject: [PATCH 01/20] waiting rooms formation --- web/forms.py | 36 +++ web/migrations/0031_waitingroom.py | 46 ++++ web/models.py | 39 +++ web/notifications.py | 51 ++++ web/templates/base.html | 8 + .../emails/waiting_room_fulfilled.html | 41 ++++ web/templates/waiting_room/create.html | 118 +++++++++ web/templates/waiting_room/create_course.html | 146 +++++++++++ web/templates/waiting_room/detail.html | 133 +++++++++++ web/templates/waiting_room/list.html | 125 ++++++++++ web/tests/test_teams.py | 103 ++++++++ web/urls.py | 7 + web/views.py | 226 ++++++++++++++++++ 13 files changed, 1079 insertions(+) create mode 100644 web/migrations/0031_waitingroom.py create mode 100644 web/templates/emails/waiting_room_fulfilled.html create mode 100644 web/templates/waiting_room/create.html create mode 100644 web/templates/waiting_room/create_course.html create mode 100644 web/templates/waiting_room/detail.html create mode 100644 web/templates/waiting_room/list.html create mode 100644 web/tests/test_teams.py diff --git a/web/forms.py b/web/forms.py index b21b370a1..a95f22bc4 100644 --- a/web/forms.py +++ b/web/forms.py @@ -29,6 +29,7 @@ Storefront, Subject, SuccessStory, + WaitingRoom, ) from .referrals import handle_referral from .widgets import ( @@ -1281,3 +1282,38 @@ def __init__(self, *args, quiz=None, **kwargs): widget=TailwindTextarea(attrs={"rows": 2, "placeholder": "Your answer..."}), required=False, ) + + +class WaitingRoomForm(forms.ModelForm): + """Form for creating and editing waiting rooms.""" + + class Meta: + model = WaitingRoom + fields = ["title", "description", "subject", "topics"] + widgets = { + "title": TailwindInput(attrs={"placeholder": "What would you like to learn?"}), + "description": TailwindTextarea(attrs={"rows": 4, "placeholder": "Describe what you want to learn in more detail..."}), + "subject": TailwindInput(attrs={"placeholder": "Main subject (e.g., Mathematics, Programming)"}), + "topics": TailwindInput(attrs={ + "placeholder": "e.g., Python, Machine Learning, Data Science", + "class": "tag-input" + }), + } + help_texts = { + "title": "Give your waiting room a descriptive title", + "subject": "The main subject area for this waiting room", + "topics": "Enter topics separated by commas", + } + + def clean_topics(self): + """Validate and clean the topics field.""" + topics = self.cleaned_data.get("topics") + if not topics: + raise forms.ValidationError("Please enter at least one topic.") + + # Ensure we have at least one non-empty topic after splitting + topic_list = [t.strip() for t in topics.split(",") if t.strip()] + if not topic_list: + raise forms.ValidationError("Please enter at least one valid topic.") + + return topics diff --git a/web/migrations/0031_waitingroom.py b/web/migrations/0031_waitingroom.py new file mode 100644 index 000000000..c2f98b991 --- /dev/null +++ b/web/migrations/0031_waitingroom.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.6 on 2025-03-20 15:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("web", "0030_quiz_quizquestion_quizoption_userquiz"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="WaitingRoom", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("title", models.CharField(max_length=200)), + ("description", models.TextField(blank=True)), + ("subject", models.CharField(max_length=100)), + ("topics", models.TextField(help_text="Comma-separated list of topics")), + ("status", models.CharField( + choices=[("open", "Open"), ("closed", "Closed"), ("fulfilled", "Fulfilled")], + default="open", + max_length=10, + )), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("creator", models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="created_waiting_rooms", + to=settings.AUTH_USER_MODEL, + )), + ("participants", models.ManyToManyField( + blank=True, + related_name="joined_waiting_rooms", + to=settings.AUTH_USER_MODEL, + )), + ], + options={ + "ordering": ["-created_at"], + }, + ), + ] diff --git a/web/models.py b/web/models.py index 6ad7c808f..7f8414104 100644 --- a/web/models.py +++ b/web/models.py @@ -1637,3 +1637,42 @@ def get_status_display(self): def created_at(self): """Alias for start_time for template compatibility.""" return self.start_time + + +class WaitingRoom(models.Model): + """Model for storing waiting room requests for courses on specific subjects.""" + + STATUS_CHOICES = [("open", "Open"), ("closed", "Closed"), ("fulfilled", "Fulfilled")] + + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + subject = models.CharField(max_length=100) + topics = models.TextField(help_text="Comma-separated list of topics") + creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name="created_waiting_rooms") + participants = models.ManyToManyField(User, related_name="joined_waiting_rooms", blank=True) + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="open") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self): + return self.title + + def participant_count(self): + """Return the number of participants in the waiting room.""" + return self.participants.count() + + def topic_list(self): + """Return the list of topics as a list.""" + return [topic.strip() for topic in self.topics.split(",") if topic.strip()] + + def mark_as_fulfilled(self, course=None): + """Mark the waiting room as fulfilled and notify participants.""" + self.status = "fulfilled" + self.save() + + if course: + from .notifications import notify_waiting_room_fulfilled + notify_waiting_room_fulfilled(self, course) diff --git a/web/notifications.py b/web/notifications.py index 67a48ca47..7ab60b658 100644 --- a/web/notifications.py +++ b/web/notifications.py @@ -177,6 +177,57 @@ def send_weekly_progress_updates(): ) +def notify_waiting_room_fulfilled(waiting_room, course): + """ + Notify all participants in a waiting room that a course has been created. + + Args: + waiting_room (WaitingRoom): The waiting room that was fulfilled + course (Course): The course that was created from the waiting room + """ + subject = f"New Course Created: {course.title}" + + # Notify all participants + for participant in waiting_room.participants.all(): + notification_data = { + "title": subject, + "message": f"A new course has been created based on a waiting room you joined: '{waiting_room.title}'. " + f"The course '{course.title}' is now available for enrollment.", + "notification_type": "success", + } + + # Send notification + send_notification(participant, notification_data) + + # Send email with more details + html_message = render_to_string( + "emails/waiting_room_fulfilled.html", + { + "user": participant, + "waiting_room": waiting_room, + "course": course, + }, + ) + + send_mail( + subject, + "", # Plain text version - we're only sending HTML + settings.DEFAULT_FROM_EMAIL, + [participant.email], + html_message=html_message, + ) + + # Also notify the creator if they're not already a participant + if waiting_room.creator not in waiting_room.participants.all(): + notification_data = { + "title": subject, + "message": f"A new course has been created based on your waiting room: '{waiting_room.title}'. " + f"The course '{course.title}' is now available.", + "notification_type": "success", + } + send_notification(waiting_room.creator, notification_data) + + def send_email(subject, message, recipient_list): """ Send an email to the specified recipients and notify Slack. diff --git a/web/templates/base.html b/web/templates/base.html index f2dfcfa59..b1b5528a1 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -199,6 +199,9 @@ Subjects + + Waiting Rooms + Products @@ -391,6 +394,11 @@ Subjects + + + Waiting Rooms + diff --git a/web/templates/emails/waiting_room_fulfilled.html b/web/templates/emails/waiting_room_fulfilled.html new file mode 100644 index 000000000..1117f8464 --- /dev/null +++ b/web/templates/emails/waiting_room_fulfilled.html @@ -0,0 +1,41 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + + +

New Course Created!

+ +

Hello {{ user.first_name|default:user.username }},

+ +

Great news! A new course has been created based on a waiting room you joined:

+ +
+

Waiting Room: {{ waiting_room.title }}

+

Subject: {{ waiting_room.subject }}

+

Topics: {{ waiting_room.topics }}

+
+ +

The following course is now available for enrollment:

+ +
+

{{ course.title }}

+

{{ course.description|truncatewords:50 }}

+

Teacher: {{ course.teacher.get_full_name|default:course.teacher.username }}

+ {% if course.price == 0 %} +

Price: Free

+ {% else %} +

Price: ${{ course.price }}

+ {% endif %} +
+ +

Don't miss this opportunity to learn exactly what you've been looking for!

+ +
+ View Course Details +
+ +

Happy learning!

+

The Alpha One Labs Team

+ + +{% endblock %} diff --git a/web/templates/waiting_room/create.html b/web/templates/waiting_room/create.html new file mode 100644 index 000000000..8e344d51c --- /dev/null +++ b/web/templates/waiting_room/create.html @@ -0,0 +1,118 @@ +{% extends "base.html" %} + +{% block title %}Create Waiting Room{% endblock %} + +{% block content %} +
+ + +
+
+

Create a Waiting Room

+ +
+
+
+ + + +
+
+

+ Create a waiting room to express interest in subjects and topics you want to learn. + Other students can join your waiting room, and teachers can create courses based on popular demand. +

+
+
+
+ +
+ {% csrf_token %} + +
+
+ +
+ {{ form.title }} +
+ {% if form.title.errors %} +

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

+ {% endif %} +

Describe what you want to learn in more detail.

+
+ +
+ +
+ {{ form.subject }} +
+ {% if form.subject.errors %} +

{{ form.subject.errors.0 }}

+ {% endif %} +

Main subject area (e.g., Mathematics, Programming, Art)

+
+ +
+ +
+ {{ form.topics }} +
+ {% if form.topics.errors %} +

{{ form.topics.errors.0 }}

+ {% endif %} +

Enter comma-separated topics (e.g., Python, Machine Learning, Data Science)

+
+
+ +
+ +
+
+
+
+
+ +{% block extra_js %} + +{% endblock %} +{% endblock %} diff --git a/web/templates/waiting_room/create_course.html b/web/templates/waiting_room/create_course.html new file mode 100644 index 000000000..56bc17440 --- /dev/null +++ b/web/templates/waiting_room/create_course.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} + +{% block title %}Create Course from Waiting Room{% endblock %} + +{% block content %} +
+ + +
+
+
+

Create Course from Waiting Room

+ +
+
+
+ + + +
+
+

+ You are creating a course based on the waiting room "{{ waiting_room.title }}". + The form has been pre-filled with information from the waiting room. + You can modify any details as needed before creating the course. +

+
+
+
+ +
+

Waiting Room Details

+
+
+ Title: {{ waiting_room.title }} +
+
+ Description: {{ waiting_room.description }} +
+
+ Subject: {{ waiting_room.subject }} +
+
+ Topics: +
+ {% for topic in topics_list %} + {{ topic }} + {% endfor %} +
+
+
+ Participants: {{ waiting_room.participants.count }} +
+
+
+ +
+ {% csrf_token %} + +
+
+ +
+ {{ form.title }} +
+ {% if form.title.errors %} +

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

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

{{ form.subject.errors.0 }}

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

{{ form.image.errors.0 }}

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

{{ form.price.errors.0 }}

+ {% endif %} +

Leave as 0 for a free course

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

{{ form.max_students.errors.0 }}

+ {% endif %} +
+ {% endif %} +
+ +
+ +
+
+
+
+
+
+{% endblock %} diff --git a/web/templates/waiting_room/detail.html b/web/templates/waiting_room/detail.html new file mode 100644 index 000000000..679e681ef --- /dev/null +++ b/web/templates/waiting_room/detail.html @@ -0,0 +1,133 @@ +{% extends "base.html" %} + +{% block title %}{{ waiting_room.title }} - Waiting Room{% endblock %} + +{% block content %} +
+ + +
+
+
+

{{ waiting_room.title }}

+
+ {% if waiting_room.status == 'open' %} + Open + {% elif waiting_room.status == 'fulfilled' %} + Fulfilled + {% elif waiting_room.status == 'closed' %} + Closed + {% endif %} +
+
+ +
+

{{ waiting_room.description }}

+
+ +
+
+

Details

+
+
+ Subject: {{ waiting_room.subject }} +
+
+ Topics: +
+ {% for topic in topic_list %} + {{ topic }} + {% endfor %} +
+
+
+ Created by: {{ waiting_room.creator.get_full_name|default:waiting_room.creator.username }} +
+
+ Created on: {{ waiting_room.created_at|date:"F j, Y" }} +
+
+
+ +
+

Participants ({{ participant_count }})

+
+ {% if waiting_room.participants.all %} +
    + {% for participant in waiting_room.participants.all %} +
  • +
    +

    {{ participant.get_full_name|default:participant.username }}

    +
    +
  • + {% endfor %} +
+ {% else %} +

No participants yet.

+ {% endif %} +
+
+
+ +
+ {% if waiting_room.status == 'open' %} + {% if is_participant %} +
+ {% csrf_token %} + +
+ {% else %} +
+ {% csrf_token %} + +
+ {% endif %} + + {% if is_teacher %} + + Create Course From This Waiting Room + + {% endif %} + {% elif waiting_room.status == 'fulfilled' %} +
+
+
+ + + +
+
+

This waiting room has been fulfilled. A course has been created based on this waiting room.

+
+
+
+ {% else %} +
+
+
+ + + +
+
+

This waiting room is closed and no longer accepting participants.

+
+
+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/web/templates/waiting_room/list.html b/web/templates/waiting_room/list.html new file mode 100644 index 000000000..ce780d985 --- /dev/null +++ b/web/templates/waiting_room/list.html @@ -0,0 +1,125 @@ +{% extends "base.html" %} +{% load dict_filters %} +{% block title %}Waiting Rooms{% endblock %} + +{% block content %} +
+
+

Waiting Rooms

+ + Create Waiting Room + +
+ +
+

What are Waiting Rooms?

+

+ Waiting rooms allow you to express interest in subjects and topics you want to learn. + Join existing waiting rooms or create your own to find others interested in the same topics. + Teachers can see these waiting rooms and create courses based on popular demand. +

+
+ + + {% if user_created_rooms or user_joined_rooms %} +
+

Your Waiting Rooms

+
+ {% for room in user_created_rooms %} +
+
+
+

{{ room.title }}

+ Created by you +
+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %} +
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
+
+
+ {% endfor %} + + {% for room in user_joined_rooms %} + {% if room.creator != user %} +
+
+
+

{{ room.title }}

+ Joined +
+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %} +
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
+
+
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + + +
+

Open Waiting Rooms

+ {% if waiting_rooms %} +
+ {% for room in waiting_rooms %} + {% if room not in user_joined_rooms and room.creator != user %} +
+
+

{{ room.title }}

+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %} +
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
+
+
+ {% endif %} + {% endfor %} +
+ {% else %} +
+

No open waiting rooms available. Be the first to create one!

+
+ {% endif %} +
+
+{% endblock %} diff --git a/web/tests/test_teams.py b/web/tests/test_teams.py new file mode 100644 index 000000000..508669540 --- /dev/null +++ b/web/tests/test_teams.py @@ -0,0 +1,103 @@ +from django.contrib.auth.models import User +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone + +from web.models import TeamGoal, TeamGoalMember, TeamInvite + + +class TeamGoalTests(TestCase): + def setUp(self): + self.client = Client() + # Create users + self.user1 = User.objects.create_user(username="testuser1", password="testpassword", email="test1@example.com") + self.user2 = User.objects.create_user(username="testuser2", password="testpassword", email="test2@example.com") + + # Log in user1 + self.client.login(username="testuser1", password="testpassword") + + # Create a team goal + self.team_goal = TeamGoal.objects.create( + title="Test Team Goal", + description="Testing team collaboration", + creator=self.user1, + deadline=timezone.now() + timezone.timedelta(days=7) + ) + + # Add user1 as a team leader + self.member = TeamGoalMember.objects.create( + team_goal=self.team_goal, + user=self.user1, + role="leader" + ) + + def test_team_goal_list(self): + """Test the team goals listing page works correctly.""" + response = self.client.get(reverse("team_goals")) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Test Team Goal") + + def test_team_goal_detail(self): + """Test the team goal detail page works correctly.""" + response = self.client.get(reverse("team_goal_detail", args=[self.team_goal.id])) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Test Team Goal") + self.assertContains(response, "Testing team collaboration") + + def test_create_team_goal(self): + """Test creating a new team goal.""" + data = { + "title": "New Team Goal", + "description": "New Description", + "deadline": (timezone.now() + timezone.timedelta(days=14)).strftime("%Y-%m-%dT%H:%M") + } + response = self.client.post(reverse("create_team_goal"), data) + self.assertEqual(TeamGoal.objects.count(), 2) + new_goal = TeamGoal.objects.get(title="New Team Goal") + self.assertEqual(new_goal.creator, self.user1) + self.assertEqual(new_goal.members.count(), 1) # Creator should be added as member + + def test_team_invite(self): + """Test inviting a user to a team goal.""" + data = { + "recipient": self.user2.id, + "recipient_search": self.user2.username + } + response = self.client.post( + reverse("team_goal_detail", args=[self.team_goal.id]), + data + ) + self.assertEqual(TeamInvite.objects.count(), 1) + invite = TeamInvite.objects.first() + self.assertEqual(invite.sender, self.user1) + self.assertEqual(invite.recipient, self.user2) + + def test_accept_invite(self): + """Test accepting a team invitation.""" + # Create an invite + invite = TeamInvite.objects.create( + goal=self.team_goal, + sender=self.user1, + recipient=self.user2 + ) + + # Switch to user2 + self.client.logout() + self.client.login(username="testuser2", password="testpassword") + + # Accept the invite + response = self.client.post(reverse("accept_team_invite", args=[invite.id])) + + # Check that user2 is now a member + self.assertTrue(TeamGoalMember.objects.filter(team_goal=self.team_goal, user=self.user2).exists()) + + # Check that invite is now marked as accepted + invite.refresh_from_db() + self.assertEqual(invite.status, "accepted") + + def test_mark_contribution(self): + """Test marking a contribution as completed.""" + response = self.client.post(reverse("mark_team_contribution", args=[self.team_goal.id])) + self.member.refresh_from_db() + self.assertTrue(self.member.completed) + self.assertIsNotNone(self.member.completed_at) \ No newline at end of file diff --git a/web/urls.py b/web/urls.py index 8dcb7ed80..2560c16dc 100644 --- a/web/urls.py +++ b/web/urls.py @@ -132,6 +132,13 @@ name="calendar_links", ), path("streak/", streak_detail, name="streak_detail"), + # Waiting Room URLs + path("waiting-rooms/", views.waiting_room_list, name="waiting_room_list"), + path("waiting-rooms/create/", views.create_waiting_room, name="create_waiting_room"), + path("waiting-rooms//", views.waiting_room_detail, name="waiting_room_detail"), + path("waiting-rooms//join/", views.join_waiting_room, name="join_waiting_room"), + path("waiting-rooms//leave/", views.leave_waiting_room, name="leave_waiting_room"), + path("waiting-rooms//create-course/", views.create_course_from_waiting_room, name="create_course_from_waiting_room"), # Forum URLs path("forum/", views.forum_categories, name="forum_categories"), path("forum/category/create/", views.create_forum_category, name="create_forum_category"), diff --git a/web/views.py b/web/views.py index c71818446..4a576e860 100644 --- a/web/views.py +++ b/web/views.py @@ -69,6 +69,7 @@ TeacherSignupForm, TeachForm, UserRegistrationForm, + WaitingRoomForm, ) from .marketing import ( generate_social_share_content, @@ -115,6 +116,7 @@ SuccessStory, TimeSlot, WebRequest, + WaitingRoom, ) from .notifications import notify_session_reminder, notify_teacher_new_enrollment, send_enrollment_confirmation from .referrals import send_referral_reward_email @@ -319,6 +321,68 @@ def create_course(request): return render(request, "courses/create.html", {"form": form}) +@login_required +@teacher_required +def create_course_from_waiting_room(request, waiting_room_id): + waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) + topics_list = [topic.strip() for topic in waiting_room.topics.split(",")] if waiting_room.topics else [] + # Only allow the teacher to create a course + if not request.user.is_teacher: + messages.error(request, "Only teachers can create courses.") + return redirect("waiting_room_detail", waiting_room_id=waiting_room_id) + + if request.method == "POST": + form = CourseForm(request.POST, request.FILES) + if form.is_valid(): + course = form.save(commit=False) + course.teacher = request.user + course.save() + form.save_m2m() + + # Auto-enroll waiting room participants + for participant in waiting_room.participants.all(): + enrollment = Enrollment.objects.create( + student=participant, + course=course + ) + + # Send notification email to the participant + subject = f"New Course Created: {course.title}" + message = f"A new course has been created based on the waiting room '{waiting_room.title}' you joined. \n\n" + message += f"Course: {course.title}\n" + message += f"Teacher: {course.teacher.get_full_name() or course.teacher.username}\n\n" + message += f"You have been automatically enrolled in this course. Visit the course page to get started:\n" + message += request.build_absolute_uri(reverse('course_detail', args=[course.slug])) + + send_mail( + subject, + message, + settings.DEFAULT_FROM_EMAIL, + [participant.email], + fail_silently=True + ) + + # Mark the waiting room as fulfilled + waiting_room.status = "fulfilled" + waiting_room.save() + + messages.success(request, "Course created successfully! All waiting room participants have been enrolled.") + return redirect("course_detail", slug=course.slug) + else: + # Pre-fill the form with waiting room data + form = CourseForm(initial={ + 'title': waiting_room.title, + 'description': waiting_room.description, + 'subject': waiting_room.subject, + 'topics': waiting_room.topics, + }) + + return render(request, "waiting_room/create_course.html", { + "form": form, + "waiting_room": waiting_room, + "topics_list": topics_list + }) + def course_detail(request, slug): course = get_object_or_404(Course, slug=slug) @@ -3896,3 +3960,165 @@ def streak_detail(request): """ streak, created = LearningStreak.objects.get_or_create(user=request.user) return render(request, "streak_detail.html", {"streak": streak}) + + +@login_required +def waiting_room_list(request): + """View for displaying all open waiting rooms.""" + waiting_rooms = WaitingRoom.objects.filter(status='open') + + # Get waiting rooms created by the user + user_created_rooms = WaitingRoom.objects.filter(creator=request.user) + + # Get waiting rooms joined by the user + user_joined_rooms = request.user.joined_waiting_rooms.all() + + # Process topics for all waiting rooms + all_rooms = list(waiting_rooms) + list(user_created_rooms) + list(user_joined_rooms) + room_topics = {} + for room in all_rooms: + room_topics[room.id] = [topic.strip() for topic in room.topics.split(',') if topic.strip()] + + context = { + 'waiting_rooms': waiting_rooms, + 'user_created_rooms': user_created_rooms, + 'user_joined_rooms': user_joined_rooms, + 'room_topics': room_topics, + } + return render(request, 'waiting_room/list.html', context) + + +@login_required +def create_waiting_room(request): + """View for creating a new waiting room.""" + if request.method == 'POST': + form = WaitingRoomForm(request.POST) + if form.is_valid(): + waiting_room = form.save(commit=False) + waiting_room.creator = request.user + waiting_room.save() + + # Add the creator as a participant + waiting_room.participants.add(request.user) + + messages.success(request, 'Waiting room created successfully!') + return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) + else: + form = WaitingRoomForm() + + context = { + 'form': form, + } + return render(request, 'waiting_room/create.html', context) + + +@login_required +def waiting_room_detail(request, waiting_room_id): + """View for displaying details of a waiting room.""" + waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) + + # Check if the user is a participant + is_participant = request.user in waiting_room.participants.all() + + # Check if the user is the creator + is_creator = request.user == waiting_room.creator + + # Check if the user is a teacher + is_teacher = hasattr(request.user, 'profile') and request.user.profile.is_teacher + + context = { + 'waiting_room': waiting_room, + 'is_participant': is_participant, + 'is_creator': is_creator, + 'is_teacher': is_teacher, + 'participant_count': waiting_room.participants.count(), + 'topic_list': [topic.strip() for topic in waiting_room.topics.split(',') if topic.strip()], + } + return render(request, 'waiting_room/detail.html', context) + + +@login_required +def join_waiting_room(request, waiting_room_id): + """View for joining a waiting room.""" + waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) + + # Check if the waiting room is open + if waiting_room.status != 'open': + messages.error(request, 'This waiting room is no longer open for joining.') + return redirect('waiting_room_list') + + # Add the user as a participant if not already + if request.user not in waiting_room.participants.all(): + waiting_room.participants.add(request.user) + messages.success(request, f'You have joined the waiting room: {waiting_room.title}') + else: + messages.info(request, 'You are already a participant in this waiting room.') + + return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) + + +@login_required +def leave_waiting_room(request, waiting_room_id): + """View for leaving a waiting room.""" + waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) + + # Remove the user from participants + if request.user in waiting_room.participants.all(): + waiting_room.participants.remove(request.user) + messages.success(request, f'You have left the waiting room: {waiting_room.title}') + else: + messages.info(request, 'You are not a participant in this waiting room.') + + return redirect('waiting_room_list') + + +@login_required +def create_course_from_waiting_room(request, waiting_room_id): + """View for creating a course from a waiting room.""" + waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) + + # Check if the user is a teacher + if not hasattr(request.user, 'profile') or not request.user.profile.is_teacher: + messages.error(request, 'Only teachers can create courses from waiting rooms.') + return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) + + if request.method == 'POST': + form = CourseForm(request.POST, request.FILES) + if form.is_valid(): + course = form.save(commit=False) + course.teacher = request.user + + # Pre-fill course details from waiting room if not provided + if not course.title: + course.title = waiting_room.title + if not course.description: + course.description = waiting_room.description + if not course.subject: + course.subject = waiting_room.subject + + course.save() + + # Mark the waiting room as fulfilled + waiting_room.status = 'fulfilled' + waiting_room.save() + + # Send notifications to all participants + from .notifications import notify_waiting_room_fulfilled + notify_waiting_room_fulfilled(waiting_room, course) + + messages.success(request, f'Course created successfully from waiting room: {waiting_room.title}') + return redirect('course_detail', slug=course.slug) + else: + # Pre-fill the form with waiting room data + initial_data = { + 'title': waiting_room.title, + 'description': waiting_room.description, + 'subject': waiting_room.subject, + } + form = CourseForm(initial=initial_data) + + context = { + 'form': form, + 'waiting_room': waiting_room, + } + return render(request, 'waiting_room/create_course.html', context) From d66221b86ee528ca38ea8e2be1d105bf56492df5 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Fri, 21 Mar 2025 00:57:44 +0530 Subject: [PATCH 02/20] removed earlier added files --- web/tests/test_teams.py | 103 ---------------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 web/tests/test_teams.py diff --git a/web/tests/test_teams.py b/web/tests/test_teams.py deleted file mode 100644 index 508669540..000000000 --- a/web/tests/test_teams.py +++ /dev/null @@ -1,103 +0,0 @@ -from django.contrib.auth.models import User -from django.test import Client, TestCase -from django.urls import reverse -from django.utils import timezone - -from web.models import TeamGoal, TeamGoalMember, TeamInvite - - -class TeamGoalTests(TestCase): - def setUp(self): - self.client = Client() - # Create users - self.user1 = User.objects.create_user(username="testuser1", password="testpassword", email="test1@example.com") - self.user2 = User.objects.create_user(username="testuser2", password="testpassword", email="test2@example.com") - - # Log in user1 - self.client.login(username="testuser1", password="testpassword") - - # Create a team goal - self.team_goal = TeamGoal.objects.create( - title="Test Team Goal", - description="Testing team collaboration", - creator=self.user1, - deadline=timezone.now() + timezone.timedelta(days=7) - ) - - # Add user1 as a team leader - self.member = TeamGoalMember.objects.create( - team_goal=self.team_goal, - user=self.user1, - role="leader" - ) - - def test_team_goal_list(self): - """Test the team goals listing page works correctly.""" - response = self.client.get(reverse("team_goals")) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Test Team Goal") - - def test_team_goal_detail(self): - """Test the team goal detail page works correctly.""" - response = self.client.get(reverse("team_goal_detail", args=[self.team_goal.id])) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Test Team Goal") - self.assertContains(response, "Testing team collaboration") - - def test_create_team_goal(self): - """Test creating a new team goal.""" - data = { - "title": "New Team Goal", - "description": "New Description", - "deadline": (timezone.now() + timezone.timedelta(days=14)).strftime("%Y-%m-%dT%H:%M") - } - response = self.client.post(reverse("create_team_goal"), data) - self.assertEqual(TeamGoal.objects.count(), 2) - new_goal = TeamGoal.objects.get(title="New Team Goal") - self.assertEqual(new_goal.creator, self.user1) - self.assertEqual(new_goal.members.count(), 1) # Creator should be added as member - - def test_team_invite(self): - """Test inviting a user to a team goal.""" - data = { - "recipient": self.user2.id, - "recipient_search": self.user2.username - } - response = self.client.post( - reverse("team_goal_detail", args=[self.team_goal.id]), - data - ) - self.assertEqual(TeamInvite.objects.count(), 1) - invite = TeamInvite.objects.first() - self.assertEqual(invite.sender, self.user1) - self.assertEqual(invite.recipient, self.user2) - - def test_accept_invite(self): - """Test accepting a team invitation.""" - # Create an invite - invite = TeamInvite.objects.create( - goal=self.team_goal, - sender=self.user1, - recipient=self.user2 - ) - - # Switch to user2 - self.client.logout() - self.client.login(username="testuser2", password="testpassword") - - # Accept the invite - response = self.client.post(reverse("accept_team_invite", args=[invite.id])) - - # Check that user2 is now a member - self.assertTrue(TeamGoalMember.objects.filter(team_goal=self.team_goal, user=self.user2).exists()) - - # Check that invite is now marked as accepted - invite.refresh_from_db() - self.assertEqual(invite.status, "accepted") - - def test_mark_contribution(self): - """Test marking a contribution as completed.""" - response = self.client.post(reverse("mark_team_contribution", args=[self.team_goal.id])) - self.member.refresh_from_db() - self.assertTrue(self.member.completed) - self.assertIsNotNone(self.member.completed_at) \ No newline at end of file From cc19f3c3e55915fe6c27c356ef3f849a7faeed6a Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sat, 22 Mar 2025 16:33:56 +0530 Subject: [PATCH 03/20] merged migrations --- web/forms.py | 35 +- web/middleware.py | 43 +- .../0033_waitingroom_fulfilled_course.py | 62 ++- web/migrations/0034_waitingroom.py | 46 --- web/models.py | 11 +- web/notifications.py | 16 +- web/settings.py | 2 +- web/templates/base.html | 3 +- web/templates/courses/create.html | 35 +- .../emails/waiting_room_fulfilled.html | 92 +++-- web/templates/waiting_room/create.html | 196 +++++---- web/templates/waiting_room/create_course.html | 244 +++++------ web/templates/waiting_room/detail.html | 386 ++++++++++-------- web/templates/waiting_room/list.html | 253 ++++++------ web/templates/waiting_room/room_card.html | 44 +- web/urls.py | 6 +- web/views.py | 188 ++++----- 17 files changed, 821 insertions(+), 841 deletions(-) delete mode 100644 web/migrations/0034_waitingroom.py diff --git a/web/forms.py b/web/forms.py index 9cd493888..cf18ba8fb 100644 --- a/web/forms.py +++ b/web/forms.py @@ -30,9 +30,9 @@ Storefront, Subject, SuccessStory, - WaitingRoom, TeamGoal, TeamInvite, + WaitingRoom, ) from .referrals import handle_referral from .widgets import ( @@ -261,21 +261,21 @@ def clean_max_students(self): msg = "Maximum number of students must be greater than zero" raise forms.ValidationError(msg) return max_students - + def clean_title(self): title = self.cleaned_data.get("title") if not title: raise forms.ValidationError("Title is required") - + # Check if title contains valid characters for slugification - if not re.match(r'^[\w\s-]+$', title): + if not re.match(r"^[\w\s-]+$", title): raise forms.ValidationError("Title can only contain letters, numbers, spaces, and hyphens") - + # Check if a course with this slug already exists slug = slugify(title) if Course.objects.filter(slug=slug).exists(): - raise forms.ValidationError("A course with a similar title already exists. Please choose a different title.") - + raise forms.ValidationError("A course with a similar title already exists.") + return title @@ -1389,39 +1389,40 @@ def __init__(self, *args, quiz=None, **kwargs): class WaitingRoomForm(forms.ModelForm): """Form for creating and editing waiting rooms.""" - + class Meta: model = WaitingRoom fields = ["title", "description", "subject", "topics"] + def clean_subject(self): - subject_name = self.cleaned_data.get('subject') + subject_name = self.cleaned_data.get("subject") if not Subject.objects.filter(name=subject_name).exists(): raise forms.ValidationError(f"Subject '{subject_name}' does not exist.") return subject_name + widgets = { "title": TailwindInput(attrs={"placeholder": "What would you like to learn?"}), - "description": TailwindTextarea(attrs={"rows": 4, "placeholder": "Describe what you want to learn in more detail..."}), + "description": TailwindTextarea(attrs={"rows": 4, "placeholder": "Describe what you want to learn"}), "subject": TailwindInput(attrs={"placeholder": "Main subject (e.g., Mathematics, Programming)"}), - "topics": TailwindInput(attrs={ - "placeholder": "e.g., Python, Machine Learning, Data Science", - "class": "tag-input" - }), + "topics": TailwindInput( + attrs={"placeholder": "e.g., Python, Machine Learning, Data Science", "class": "tag-input"} + ), } help_texts = { "title": "Give your waiting room a descriptive title", "subject": "The main subject area for this waiting room", "topics": "Enter topics separated by commas", } - + def clean_topics(self): """Validate and clean the topics field.""" topics = self.cleaned_data.get("topics") if not topics: raise forms.ValidationError("Please enter at least one topic.") - + # Ensure we have at least one non-empty topic after splitting topic_list = [t.strip() for t in topics.split(",") if t.strip()] if not topic_list: raise forms.ValidationError("Please enter at least one valid topic.") - + return topics diff --git a/web/middleware.py b/web/middleware.py index 55c47afa8..5d3f18052 100644 --- a/web/middleware.py +++ b/web/middleware.py @@ -1,8 +1,9 @@ import logging import traceback +from django.contrib import messages from django.http import Http404 -from django.shortcuts import render +from django.shortcuts import redirect, render from django.urls import Resolver404, resolve from .models import Course, WebRequest @@ -132,36 +133,26 @@ def __call__(self, request): def process_view(self, request, view_func, view_args, view_kwargs): # Only validate if coming from waiting room - if 'waiting_room_data' not in request.session: + if "waiting_room_data" not in request.session: return None - + # Only validate on course creation POST - if request.method != 'POST' or view_func.__name__ != 'CourseCreateView': + if request.method != "POST" or view_func.__name__ != "CourseCreateView": return None - + # Get waiting room data - waiting_room_data = request.session['waiting_room_data'] - + waiting_room_data = request.session["waiting_room_data"] + # Validate subject and topics - subject_match = ( - request.POST.get('subject', '').strip().lower() == - waiting_room_data['subject'] - ) - - submitted_topics = { - t.strip().lower() - for t in request.POST.get('topics', '').split(',') - if t.strip() - } - topics_match = submitted_topics.issuperset(waiting_room_data['topics']) - + subject_match = request.POST.get("subject", "").strip().lower() == waiting_room_data["subject"] + + submitted_topics = {t.strip().lower() for t in request.POST.get("topics", "").split(",") if t.strip()} + topics_match = submitted_topics.issuperset(waiting_room_data["topics"]) + if not (subject_match and topics_match): - messages.error( - request, - "Course must match waiting room's subject and include all requested topics." - ) - return redirect('course_create') - + messages.error(request, "Course must match waiting room's subject and include all requested topics.") + return redirect("course_create") + # Clear waiting room data after validation - del request.session['waiting_room_data'] + del request.session["waiting_room_data"] return None diff --git a/web/migrations/0033_waitingroom_fulfilled_course.py b/web/migrations/0033_waitingroom_fulfilled_course.py index 13506b5ab..14ee90f3e 100644 --- a/web/migrations/0033_waitingroom_fulfilled_course.py +++ b/web/migrations/0033_waitingroom_fulfilled_course.py @@ -1,25 +1,65 @@ # Generated by Django 5.1.6 on 2025-03-22 09:17 import django.db.models.deletion +from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("web", "0032_waitingroom"), + ("web", "0032_rename_completion_date_teamgoalmember_completed_at_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ - migrations.AddField( - model_name="waitingroom", - name="fulfilled_course", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="fulfilled_waiting_rooms", - to="web.course", - ), + migrations.CreateModel( + name="WaitingRoom", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("title", models.CharField(max_length=200)), + ("description", models.TextField(blank=True)), + ("subject", models.CharField(max_length=100)), + ("topics", models.TextField(help_text="Comma-separated list of topics")), + ( + "status", + models.CharField( + choices=[("open", "Open"), ("closed", "Closed"), ("fulfilled", "Fulfilled")], + default="open", + max_length=10, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "creator", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="created_waiting_rooms", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "participants", + models.ManyToManyField( + blank=True, + related_name="joined_waiting_rooms", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "fulfilled_course", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fulfilled_waiting_rooms", + to="web.course", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, ), ] diff --git a/web/migrations/0034_waitingroom.py b/web/migrations/0034_waitingroom.py deleted file mode 100644 index 97727bffd..000000000 --- a/web/migrations/0034_waitingroom.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-20 15:55 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("web", "0031_achievement_badge_icon_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="WaitingRoom", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("title", models.CharField(max_length=200)), - ("description", models.TextField(blank=True)), - ("subject", models.CharField(max_length=100)), - ("topics", models.TextField(help_text="Comma-separated list of topics")), - ("status", models.CharField( - choices=[("open", "Open"), ("closed", "Closed"), ("fulfilled", "Fulfilled")], - default="open", - max_length=10, - )), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("creator", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="created_waiting_rooms", - to=settings.AUTH_USER_MODEL, - )), - ("participants", models.ManyToManyField( - blank=True, - related_name="joined_waiting_rooms", - to=settings.AUTH_USER_MODEL, - )), - ], - options={ - "ordering": ["-created_at"], - }, - ), - ] diff --git a/web/models.py b/web/models.py index bee2b592b..48f1721f4 100644 --- a/web/models.py +++ b/web/models.py @@ -1781,13 +1781,9 @@ class WaitingRoom(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) fulfilled_course = models.ForeignKey( - 'Course', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='fulfilled_waiting_rooms' + "Course", on_delete=models.SET_NULL, null=True, blank=True, related_name="fulfilled_waiting_rooms" ) - + class Meta: ordering = ["-created_at"] @@ -1806,7 +1802,8 @@ def mark_as_fulfilled(self, course=None): """Mark the waiting room as fulfilled and notify participants.""" self.status = "fulfilled" self.save() - + if course: from .notifications import notify_waiting_room_fulfilled + notify_waiting_room_fulfilled(self, course) diff --git a/web/notifications.py b/web/notifications.py index 38a4f8ca7..53be9a7c1 100644 --- a/web/notifications.py +++ b/web/notifications.py @@ -180,25 +180,25 @@ def send_weekly_progress_updates(): def notify_waiting_room_fulfilled(waiting_room, course): """ Notify all participants in a waiting room that a course has been created. - + Args: waiting_room (WaitingRoom): The waiting room that was fulfilled course (Course): The course that was created from the waiting room """ subject = f"New Course Created: {course.title}" - + # Notify all participants for participant in waiting_room.participants.all(): notification_data = { "title": subject, "message": f"A new course has been created based on a waiting room you joined: '{waiting_room.title}'. " - f"The course '{course.title}' is now available for enrollment.", + f"The course '{course.title}' is now available for enrollment.", "notification_type": "success", } - + # Send notification send_notification(participant, notification_data) - + # Send email with more details html_message = render_to_string( "emails/waiting_room_fulfilled.html", @@ -208,7 +208,7 @@ def notify_waiting_room_fulfilled(waiting_room, course): "course": course, }, ) - + send_mail( subject, "", # Plain text version - we're only sending HTML @@ -216,13 +216,13 @@ def notify_waiting_room_fulfilled(waiting_room, course): [participant.email], html_message=html_message, ) - + # Also notify the creator if they're not already a participant if waiting_room.creator not in waiting_room.participants.all(): notification_data = { "title": subject, "message": f"A new course has been created based on your waiting room: '{waiting_room.title}'. " - f"The course '{course.title}' is now available.", + f"The course '{course.title}' is now available.", "notification_type": "success", } send_notification(waiting_room.creator, notification_data) diff --git a/web/settings.py b/web/settings.py index 07107cd5b..4e7fcec5a 100644 --- a/web/settings.py +++ b/web/settings.py @@ -242,7 +242,7 @@ # Email settings if DEBUG: - EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" print("Using console email backend with Slack notifications for development") DEFAULT_FROM_EMAIL = "noreply@example.com" # Default for development SENDGRID_API_KEY = None # Not needed in development diff --git a/web/templates/base.html b/web/templates/base.html index 3f57f354c..abd616e7a 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -199,7 +199,8 @@ Subjects - + Waiting Rooms {{ form.media }} {% endblock extra_head %} - {% block content %}
@@ -21,25 +19,26 @@

Create New Course

Fill out the form below to create your new course.

- {% if request.session.waiting_room_data %} -
-
-
- - - -
-
-

- Creating course for waiting room. Ensure the subject and topics match the waiting room's request. -

+
+
+
+ + + +
+
+

+ Creating course for waiting room. Ensure the subject and topics match the waiting room's request. +

+
-
{% endif %} -
Create New Course
- - {% endblock content %} diff --git a/web/templates/emails/waiting_room_fulfilled.html b/web/templates/emails/waiting_room_fulfilled.html index 1117f8464..4a2192605 100644 --- a/web/templates/emails/waiting_room_fulfilled.html +++ b/web/templates/emails/waiting_room_fulfilled.html @@ -1,41 +1,59 @@ {% extends "emails/base_email.html" %} {% block content %} - - -

New Course Created!

- -

Hello {{ user.first_name|default:user.username }},

- -

Great news! A new course has been created based on a waiting room you joined:

- -
-

Waiting Room: {{ waiting_room.title }}

-

Subject: {{ waiting_room.subject }}

-

Topics: {{ waiting_room.topics }}

-
- -

The following course is now available for enrollment:

- -
-

{{ course.title }}

-

{{ course.description|truncatewords:50 }}

-

Teacher: {{ course.teacher.get_full_name|default:course.teacher.username }}

- {% if course.price == 0 %} -

Price: Free

- {% else %} -

Price: ${{ course.price }}

- {% endif %} -
- -

Don't miss this opportunity to learn exactly what you've been looking for!

- -
- -

Happy learning!

-

The Alpha One Labs Team

- - + + +

New Course Created!

+

Hello {{ user.first_name|default:user.username }},

+

Great news! A new course has been created based on a waiting room you joined:

+
+

+ Waiting Room: {{ waiting_room.title }} +

+

+ Subject: {{ waiting_room.subject }} +

+

+ Topics: {{ waiting_room.topics }} +

+
+

The following course is now available for enrollment:

+
+

{{ course.title }}

+

{{ course.description|truncatewords:50 }}

+

+ Teacher: {{ course.teacher.get_full_name|default:course.teacher.username }} +

+ {% if course.price == 0 %} +

+ Price: Free +

+ {% else %} +

+ Price: ${{ course.price }} +

+ {% endif %} +
+

Don't miss this opportunity to learn exactly what you've been looking for!

+ +

Happy learning!

+

The Alpha One Labs Team

+ + {% endblock %} diff --git a/web/templates/waiting_room/create.html b/web/templates/waiting_room/create.html index 8e344d51c..ebf79ec12 100644 --- a/web/templates/waiting_room/create.html +++ b/web/templates/waiting_room/create.html @@ -1,118 +1,104 @@ {% extends "base.html" %} {% block title %}Create Waiting Room{% endblock %} - {% block content %} -
+
-
-
-

Create a Waiting Room

- -
-
-
- - - -
-
-

- Create a waiting room to express interest in subjects and topics you want to learn. - Other students can join your waiting room, and teachers can create courses based on popular demand. -

-
-
+
+

Create a Waiting Room

+
+
+
+ + +
- - - {% csrf_token %} - -
-
- -
- {{ form.title }} -
- {% if form.title.errors %} -

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

- {% endif %} -

Describe what you want to learn in more detail.

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

{{ form.subject.errors.0 }}

- {% endif %} -

Main subject area (e.g., Mathematics, Programming, Art)

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

{{ form.topics.errors.0 }}

- {% endif %} -

Enter comma-separated topics (e.g., Python, Machine Learning, Data Science)

-
-
- -
- -
- +
+

+ Create a waiting room to express interest in subjects and topics you want to learn. + Other students can join your waiting room, and teachers can create courses based on popular demand. +

+
+
+
+ {% csrf_token %} +
+
+ +
{{ form.title }}
+ {% if form.title.errors %}

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

{% endif %} +

Describe what you want to learn in more detail.

+
+
+ +
{{ form.subject }}
+ {% if form.subject.errors %}

{{ form.subject.errors.0 }}

{% endif %} +

Main subject area (e.g., Mathematics, Programming, Art)

+
+
+ +
{{ form.topics }}
+ {% if form.topics.errors %}

{{ form.topics.errors.0 }}

{% endif %} +

Enter comma-separated topics (e.g., Python, Machine Learning, Data Science)

+
+
+
+ +
+
+
-
- -{% block extra_js %} - -{% endblock %} + }); + } + }); + + {% endblock %} {% endblock %} diff --git a/web/templates/waiting_room/create_course.html b/web/templates/waiting_room/create_course.html index dac6f6c3a..916d782ed 100644 --- a/web/templates/waiting_room/create_course.html +++ b/web/templates/waiting_room/create_course.html @@ -1,146 +1,124 @@ {% extends "base.html" %} {% block title %}Create Course from Waiting Room{% endblock %} - {% block content %} -
+
-
-
-
-

Create Course from Waiting Room

- -
-
-
- - - -
-
-

- You are creating a course based on the waiting room "{{ waiting_room.title }}". - The form has been pre-filled with information from the waiting room. - You can modify any details as needed before creating the course. -

-
-
+
+
+

Create Course from Waiting Room

+
+
+
+ + + +
+
+

+ You are creating a course based on the waiting room "{{ waiting_room.title }}". + The form has been pre-filled with information from the waiting room. + You can modify any details as needed before creating the course. +

+
+
+
+
+

Waiting Room Details

+
+
+ Title: {{ waiting_room.title }} +
+
+ Description: {{ waiting_room.description }} +
+
+ Subject: {{ waiting_room.subject }} +
+
+ Topics: +
+ {% for topic in topics_list %} + {{ topic }} + {% endfor %}
- -
-

Waiting Room Details

-
-
- Title: {{ waiting_room.title }} -
-
- Description: {{ waiting_room.description }} -
-
- Subject: {{ waiting_room.subject }} -
-
- Topics: -
- {% for topic in topics_list %} - {{ topic }} - {% endfor %} -
-
-
- Participants: {{ waiting_room.participants.count }} -
-
+
+
+ Participants: {{ waiting_room.participants.count }} +
+
+
+
+ {% csrf_token %} +
+
+ +
{{ form.title }}
+ {% if form.title.errors %}

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

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

{{ form.subject.errors.0 }}

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

{{ form.image.errors.0 }}

{% endif %}
- - - {% csrf_token %} - -
-
- -
- {{ form.title }} -
- {% if form.title.errors %} -

{{ form.title.errors.0 }}

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

{{ form.description.errors.0 }}

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

{{ form.subject.errors.0 }}

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

{{ form.image.errors.0 }}

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

{{ form.price.errors.0 }}

- {% endif %} -

Leave as 0 for a free course

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

{{ form.max_students.errors.0 }}

- {% endif %} -
- {% endif %} -
- -
- -
- + {% endif %} + {% if form.price %} +
+ +
{{ form.price }}
+ {% if form.price.errors %}

{{ form.price.errors.0 }}

{% endif %} +

Leave as 0 for a free course

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

{{ form.max_students.errors.0 }}

{% endif %} +
+ {% endif %} +
+
+
+
+
-
+
{% endblock %} diff --git a/web/templates/waiting_room/detail.html b/web/templates/waiting_room/detail.html index c46027aa7..14fd6ede9 100644 --- a/web/templates/waiting_room/detail.html +++ b/web/templates/waiting_room/detail.html @@ -1,201 +1,225 @@ {% extends "base.html" %} {% block title %}{{ waiting_room.title }} - Waiting Room{% endblock %} - {% block content %} -
+
-
-
-
-

{{ waiting_room.title }}

-
- {% if waiting_room.status == 'open' %} - Open - {% elif waiting_room.status == 'fulfilled' %} - Fulfilled -
-
- 🎉 -

Course Available!

-
- - {% if waiting_room.fulfilled_course %} -
-

The course "{{ waiting_room.fulfilled_course.title }}" has been created based on your request.

- -
- {% else %} -

A course matching your request has been created!

- {% for message in messages %} - {% if 'course_' in message.extra_tags %} - {% with course_slug=message.extra_tags|slice:'7:' %} - - View Course - - - - - {% endwith %} - {% endif %} - {% endfor %} - {% endif %} +
+
+

{{ waiting_room.title }}

+
+ {% if waiting_room.status == 'open' %} + Open + {% elif waiting_room.status == 'fulfilled' %} + Fulfilled +
+
+ 🎉 +

Course Available!

+
+ {% if waiting_room.fulfilled_course %} +
+

+ The course "{{ waiting_room.fulfilled_course.title }}" has been created based on your request. +

+ - {% elif waiting_room.status == 'closed' %} - Closed +
+ {% else %} +

A course matching your request has been created!

+ {% for message in messages %} + {% if 'course_' in message.extra_tags %} + {% with course_slug=message.extra_tags|slice:'7:' %} + + View Course + + + + + {% endwith %} {% endif %} + {% endfor %} + {% endif %} +
+ {% elif waiting_room.status == 'closed' %} + Closed + {% endif %} +
+
+
+

{{ waiting_room.description }}

+
+
+
+

Details

+
+
+ Subject: {{ waiting_room.subject }} +
+
+ Topics: +
+ {% for topic in topic_list %} + {{ topic }} + {% endfor %}
+
+
+ Created by: {{ waiting_room.creator.get_full_name|default:waiting_room.creator.username }} +
+
+ Created on: {{ waiting_room.created_at|date:"F j, Y" }} +
- -
-

{{ waiting_room.description }}

+
+
+

Participants ({{ participant_count }})

+
+ {% if waiting_room.participants.all %} +
    + {% for participant in waiting_room.participants.all %} +
  • +
    +

    {{ participant.get_full_name|default:participant.username }}

    +
    +
  • + {% endfor %} +
+ {% else %} +

No participants yet.

+ {% endif %}
- -
-
-

Details

-
-
- Subject: {{ waiting_room.subject }} -
-
- Topics: -
- {% for topic in topic_list %} - {{ topic }} - {% endfor %} -
-
-
- Created by: {{ waiting_room.creator.get_full_name|default:waiting_room.creator.username }} -
-
- Created on: {{ waiting_room.created_at|date:"F j, Y" }} -
-
-
- -
-

Participants ({{ participant_count }})

-
- {% if waiting_room.participants.all %} -
    - {% for participant in waiting_room.participants.all %} -
  • -
    -

    {{ participant.get_full_name|default:participant.username }}

    -
    -
  • - {% endfor %} -
- {% else %} -

No participants yet.

- {% endif %} -
+
+
+ {% if matching_courses %} +
+

Matching Courses

+
+ {% for course in matching_courses %} +
+

{{ course.title }}

+

{{ course.description }}

+
+ by {{ course.teacher.get_full_name }} + + Enroll Now + + + + +
+ {% endfor %}
- - {% if matching_courses %} -
-

Matching Courses

-
- {% for course in matching_courses %} -
-

{{ course.title }}

-

{{ course.description }}

-
- by {{ course.teacher.get_full_name }} - - Enroll Now - - - - -
-
- {% endfor %} +
+ {% endif %} +
+ {% if waiting_room.status == 'open' %} + {% if is_participant %} +
+ {% csrf_token %} + +
+ {% else %} +
+ {% csrf_token %} + +
+ {% endif %} + {% if is_teacher %} + + Create Course From This Waiting Room + + {% endif %} + {% elif waiting_room.status == 'fulfilled' %} +
+
+
+ + + +
+
+

+ This waiting room has been fulfilled. A course has been created based on this waiting room. +

+
- {% endif %} - -
- {% if waiting_room.status == 'open' %} - {% if is_participant %} -
- {% csrf_token %} - -
- {% else %} -
- {% csrf_token %} - -
- {% endif %} - - {% if is_teacher %} - - Create Course From This Waiting Room - - {% endif %} - {% elif waiting_room.status == 'fulfilled' %} -
-
-
- - - -
-
-

This waiting room has been fulfilled. A course has been created based on this waiting room.

-
-
-
- {% else %} -
-
-
- - - -
-
-

This waiting room is closed and no longer accepting participants.

-
-
-
- {% endif %} + {% else %} +
+
+
+ + + +
+
+

This waiting room is closed and no longer accepting participants.

+
+
+ {% endif %}
+
-
+
{% endblock %} diff --git a/web/templates/waiting_room/list.html b/web/templates/waiting_room/list.html index fc11afb3f..8cded8304 100644 --- a/web/templates/waiting_room/list.html +++ b/web/templates/waiting_room/list.html @@ -1,180 +1,183 @@ {% extends "base.html" %} + {% load dict_filters %} -{% block title %}Waiting Rooms{% endblock %} +{% block title %}Waiting Rooms{% endblock %} {% block content %} -
+
-

Waiting Rooms

- - Create Waiting Room - +

Waiting Rooms

+ Create Waiting Room
-
-

What are Waiting Rooms?

-

- Waiting rooms allow you to express interest in subjects and topics you want to learn. - Join existing waiting rooms or create your own to find others interested in the same topics. - Teachers can see these waiting rooms and create courses based on popular demand. -

+

What are Waiting Rooms?

+

+ Waiting rooms allow you to express interest in subjects and topics you want to learn. + Join existing waiting rooms or create your own to find others interested in the same topics. + Teachers can see these waiting rooms and create courses based on popular demand. +

- {% if open_rooms %} -
+
-

Open Waiting Rooms

- {{ open_rooms.count }} +

Open Waiting Rooms

+ {{ open_rooms.count }}
- {% for room in open_rooms %} + {% for room in open_rooms %}
- {% include 'waiting_room/room_card.html' with room=room room_topics=room_topics %} + {% include 'waiting_room/room_card.html' with room=room room_topics=room_topics %}
- {% endfor %} + {% endfor %}
-
+
{% endif %} - {% if fulfilled_rooms %} -
+
-

Fulfilled Waiting Rooms

- {{ fulfilled_rooms.count }} +

Fulfilled Waiting Rooms

+ {{ fulfilled_rooms.count }}
- {% for room in fulfilled_rooms %} + {% for room in fulfilled_rooms %}
- {% include 'waiting_room/room_card.html' with room=room room_topics=room_topics %} - {% if room.fulfilled_course %} + {% include 'waiting_room/room_card.html' with room=room room_topics=room_topics %} + {% if room.fulfilled_course %} - {% endif %} + {% endif %}
- {% endfor %} + {% endfor %}
-
+
{% endif %} - {% if user_created_rooms or user_joined_rooms %} -
+

Your Waiting Rooms

- {% for room in user_created_rooms %} + {% for room in user_created_rooms %}
-
-
-

{{ room.title }}

- Created by you -
-

{{ room.description|truncatechars:100 }}

-
- Subject: {{ room.subject }} -
-
- Topics: -
- {% for topic in room_topics|get_item:room.id %} - {{ topic }} - {% endfor %} -
-
-
- {{ room.participants.count }} participant{{ room.participants.count|pluralize }} - View Details -
+
+
+

{{ room.title }}

+ Created by you +
+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %} +
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
+
- {% endfor %} - - {% for room in user_joined_rooms %} + {% endfor %} + {% for room in user_joined_rooms %} {% if room.creator != user %} -
+
-
-

{{ room.title }}

- Joined -
-

{{ room.description|truncatechars:100 }}

-
- Subject: {{ room.subject }} -
-
- Topics: -
- {% for topic in room_topics|get_item:room.id %} - {{ topic }} - {% endfor %} -
-
-
- {{ room.participants.count }} participant{{ room.participants.count|pluralize }} - View Details +
+

{{ room.title }}

+ Joined +
+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %}
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
-
+
{% endif %} - {% endfor %} + {% endfor %}
-
+
{% endif %} -
-

Open Waiting Rooms

- {% if waiting_rooms %} +

Open Waiting Rooms

+ {% if waiting_rooms %}
- {% for room in open_rooms %} + {% for room in open_rooms %} {% if room not in user_joined_rooms and room.creator != user %} -
+
-

{{ room.title }}

-

{{ room.description|truncatechars:100 }}

-
- Subject: {{ room.subject }} -
-
- Topics: -
- {% for topic in room_topics|get_item:room.id %} - {{ topic }} - {% endfor %} -
-
-
- {{ room.participants.count }} participant{{ room.participants.count|pluralize }} - View Details +

{{ room.title }}

+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %}
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
-
+
{% endif %} - {% endfor %} + {% endfor %}
- {% else %} + {% else %}
-

No open waiting rooms available. Be the first to create one!

+

No open waiting rooms available. Be the first to create one!

- {% endif %} + {% endif %}
-
+
{% endblock %} diff --git a/web/templates/waiting_room/room_card.html b/web/templates/waiting_room/room_card.html index 8d281ad9e..68e916d70 100644 --- a/web/templates/waiting_room/room_card.html +++ b/web/templates/waiting_room/room_card.html @@ -1,25 +1,27 @@ {% load dict_filters %} +
-
-

{{ room.title }}

- {% if room.creator == user %} - Created by you - {% endif %} -
-

{{ room.description|truncatechars:100 }}

-
- Subject: {{ room.subject }} -
-
- Topics: -
- {% for topic in room_topics|get_item:room.id %} - {{ topic }} - {% endfor %} -
-
-
- {{ room.participants.count }} participant{{ room.participants.count|pluralize }} - View Details +
+

{{ room.title }}

+ {% if room.creator == user %} + Created by you + {% endif %} +
+

{{ room.description|truncatechars:100 }}

+
+ Subject: {{ room.subject }} +
+
+ Topics: +
+ {% for topic in room_topics|get_item:room.id %} + {{ topic }} + {% endfor %}
+
+
+ {{ room.participants.count }} participant{{ room.participants.count|pluralize }} + View Details +
diff --git a/web/urls.py b/web/urls.py index 53eab4672..9602dd864 100644 --- a/web/urls.py +++ b/web/urls.py @@ -139,7 +139,11 @@ path("waiting-rooms//", views.waiting_room_detail, name="waiting_room_detail"), path("waiting-rooms//join/", views.join_waiting_room, name="join_waiting_room"), path("waiting-rooms//leave/", views.leave_waiting_room, name="leave_waiting_room"), - path("waiting-rooms//create-course/", views.create_course_from_waiting_room, name="create_course_from_waiting_room"), + path( + "waiting-rooms//create-course/", + views.create_course_from_waiting_room, + name="create_course_from_waiting_room", + ), # Forum URLs path("forum/", views.forum_categories, name="forum_categories"), path("forum/category/create/", views.create_forum_category, name="create_forum_category"), diff --git a/web/views.py b/web/views.py index 6442e3472..f617160e8 100644 --- a/web/views.py +++ b/web/views.py @@ -125,8 +125,8 @@ TeamGoalMember, TeamInvite, TimeSlot, - WebRequest, WaitingRoom, + WebRequest, ) from .notifications import ( notify_session_reminder, @@ -349,33 +349,33 @@ def create_course(request): if form.is_valid(): course = form.save(commit=False) course.teacher = request.user - course.status = 'published' # Set status to published + course.status = "published" # Set status to published course.save() form.save_m2m() # Save many-to-many relationships # Handle waiting room if course was created from one - if 'waiting_room_data' in request.session: - waiting_room = get_object_or_404(WaitingRoom, id=request.session['waiting_room_data']['id']) - + if "waiting_room_data" in request.session: + waiting_room = get_object_or_404(WaitingRoom, id=request.session["waiting_room_data"]["id"]) + # Update waiting room status and link to course - waiting_room.status = 'fulfilled' + waiting_room.status = "fulfilled" waiting_room.fulfilled_course = course - waiting_room.save(update_fields=['status', 'fulfilled_course']) - + waiting_room.save(update_fields=["status", "fulfilled_course"]) + # Send notifications to all participants for participant in waiting_room.participants.all(): messages.success( request, - f'A new course matching your request has been created: {course.title}', - extra_tags=f'course_{course.slug}' + f"A new course matching your request has been created: {course.title}", + extra_tags=f"course_{course.slug}", ) - + # Clear waiting room data from session - del request.session['waiting_room_data'] - + del request.session["waiting_room_data"] + # Redirect back to waiting room to show the update - return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) - + return redirect("waiting_room_detail", waiting_room_id=waiting_room.id) + return redirect("course_detail", slug=course.slug) else: form = CourseForm() @@ -387,21 +387,21 @@ def create_course(request): @teacher_required def create_course_from_waiting_room(request, waiting_room_id): waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) - + # Ensure waiting room is open if waiting_room.status != "open": messages.error(request, "This waiting room is no longer open.") return redirect("waiting_room_detail", waiting_room_id=waiting_room_id) - + # Store waiting room data in session for validation - request.session['waiting_room_data'] = { - 'id': waiting_room.id, - 'subject': waiting_room.subject.strip().lower(), - 'topics': [t.strip().lower() for t in waiting_room.topics.split(',') if t.strip()], + request.session["waiting_room_data"] = { + "id": waiting_room.id, + "subject": waiting_room.subject.strip().lower(), + "topics": [t.strip().lower() for t in waiting_room.topics.split(",") if t.strip()], } - + # Redirect to regular course creation form - return redirect(reverse('create_course')) + return redirect(reverse("create_course")) def course_detail(request, slug): @@ -4404,159 +4404,143 @@ def streak_detail(request): def waiting_room_list(request): """View for displaying waiting rooms categorized by status.""" # Get waiting rooms by status - open_rooms = WaitingRoom.objects.filter(status='open') - fulfilled_rooms = WaitingRoom.objects.filter(status='fulfilled') - closed_rooms = WaitingRoom.objects.filter(status='closed') - + open_rooms = WaitingRoom.objects.filter(status="open") + fulfilled_rooms = WaitingRoom.objects.filter(status="fulfilled") + closed_rooms = WaitingRoom.objects.filter(status="closed") + # Get waiting rooms created by the user user_created_rooms = WaitingRoom.objects.filter(creator=request.user) - + # Get waiting rooms joined by the user user_joined_rooms = request.user.joined_waiting_rooms.all() - + # Process topics for all waiting rooms - all_rooms = list(open_rooms) + list(fulfilled_rooms) + list(closed_rooms) + \ - list(user_created_rooms) + list(user_joined_rooms) + all_rooms = ( + list(open_rooms) + + list(fulfilled_rooms) + + list(closed_rooms) + + list(user_created_rooms) + + list(user_joined_rooms) + ) room_topics = {} for room in all_rooms: - room_topics[room.id] = [topic.strip() for topic in room.topics.split(',') if topic.strip()] - + room_topics[room.id] = [topic.strip() for topic in room.topics.split(",") if topic.strip()] + context = { - 'open_rooms': open_rooms, - 'fulfilled_rooms': fulfilled_rooms, - 'closed_rooms': closed_rooms, - 'user_created_rooms': user_created_rooms, - 'user_joined_rooms': user_joined_rooms, - 'room_topics': room_topics, + "open_rooms": open_rooms, + "fulfilled_rooms": fulfilled_rooms, + "closed_rooms": closed_rooms, + "user_created_rooms": user_created_rooms, + "user_joined_rooms": user_joined_rooms, + "room_topics": room_topics, } - return render(request, 'waiting_room/list.html', context) + return render(request, "waiting_room/list.html", context) @login_required def create_waiting_room(request): """View for creating a new waiting room.""" - if request.method == 'POST': + if request.method == "POST": form = WaitingRoomForm(request.POST) if form.is_valid(): waiting_room = form.save(commit=False) waiting_room.creator = request.user waiting_room.save() - + # Add the creator as a participant waiting_room.participants.add(request.user) - - messages.success(request, 'Waiting room created successfully!') - return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) + + messages.success(request, "Waiting room created successfully!") + return redirect("waiting_room_detail", waiting_room_id=waiting_room.id) else: form = WaitingRoomForm() - + context = { - 'form': form, + "form": form, } - return render(request, 'waiting_room/create.html', context) + return render(request, "waiting_room/create.html", context) def find_matching_courses(waiting_room): """Find courses that match the waiting room's subject and topics.""" # Get courses with matching subject name (case-insensitive) - matching_courses = Course.objects.filter( - subject__name__iexact=waiting_room.subject, - status='published' - ) - + matching_courses = Course.objects.filter(subject__name__iexact=waiting_room.subject, status="published") + # Filter courses that have all required topics - required_topics = {t.strip().lower() for t in waiting_room.topics.split(',') if t.strip()} - + required_topics = {t.strip().lower() for t in waiting_room.topics.split(",") if t.strip()} + # Further filter courses by checking if their topics contain all required topics final_matches = [] for course in matching_courses: - course_topics = {t.strip().lower() for t in course.topics.split(',') if t.strip()} + course_topics = {t.strip().lower() for t in course.topics.split(",") if t.strip()} if course_topics.issuperset(required_topics): final_matches.append(course) - + return final_matches + def waiting_room_detail(request, waiting_room_id): """View for displaying details of a waiting room.""" waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) - + # Check if the user is a participant is_participant = request.user in waiting_room.participants.all() - + # Check if the user is the creator is_creator = request.user == waiting_room.creator - + # Check if the user is a teacher - is_teacher = hasattr(request.user, 'profile') and request.user.profile.is_teacher - + is_teacher = hasattr(request.user, "profile") and request.user.profile.is_teacher + # Find matching courses # matching_courses = find_matching_courses(waiting_room) - + context = { - 'waiting_room': waiting_room, - 'is_participant': is_participant, - 'is_creator': is_creator, - 'is_teacher': is_teacher, - 'participant_count': waiting_room.participants.count(), - 'topic_list': [topic.strip() for topic in waiting_room.topics.split(',') if topic.strip()], + "waiting_room": waiting_room, + "is_participant": is_participant, + "is_creator": is_creator, + "is_teacher": is_teacher, + "participant_count": waiting_room.participants.count(), + "topic_list": [topic.strip() for topic in waiting_room.topics.split(",") if topic.strip()], # 'matching_courses': matching_courses, } - return render(request, 'waiting_room/detail.html', context) + return render(request, "waiting_room/detail.html", context) @login_required def join_waiting_room(request, waiting_room_id): """View for joining a waiting room.""" waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) - + # Check if the waiting room is open - if waiting_room.status != 'open': - messages.error(request, 'This waiting room is no longer open for joining.') - return redirect('waiting_room_list') - + if waiting_room.status != "open": + messages.error(request, "This waiting room is no longer open for joining.") + return redirect("waiting_room_list") + # Add the user as a participant if not already if request.user not in waiting_room.participants.all(): waiting_room.participants.add(request.user) - messages.success(request, f'You have joined the waiting room: {waiting_room.title}') + messages.success(request, f"You have joined the waiting room: {waiting_room.title}") else: - messages.info(request, 'You are already a participant in this waiting room.') - - return redirect('waiting_room_detail', waiting_room_id=waiting_room.id) + messages.info(request, "You are already a participant in this waiting room.") + + return redirect("waiting_room_detail", waiting_room_id=waiting_room.id) @login_required def leave_waiting_room(request, waiting_room_id): """View for leaving a waiting room.""" waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) - + # Remove the user from participants if request.user in waiting_room.participants.all(): waiting_room.participants.remove(request.user) - messages.success(request, f'You have left the waiting room: {waiting_room.title}') + messages.success(request, f"You have left the waiting room: {waiting_room.title}") else: - messages.info(request, 'You are not a participant in this waiting room.') - - return redirect('waiting_room_list') + messages.info(request, "You are not a participant in this waiting room.") + return redirect("waiting_room_list") -@login_required -def create_course_from_waiting_room(request, waiting_room_id): - waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) - - # Ensure waiting room is open - if waiting_room.status != "open": - messages.error(request, "This waiting room is no longer open.") - return redirect("waiting_room_detail", waiting_room_id=waiting_room_id) - - # Store waiting room data in session for validation - request.session['waiting_room_data'] = { - 'id': waiting_room.id, - 'subject': waiting_room.subject.strip().lower(), - 'topics': [t.strip().lower() for t in waiting_room.topics.split(',') if t.strip()], - } - - # Redirect to regular course creation form - return redirect(reverse('create_course')) def is_superuser(user): return user.is_superuser From 2c032d640c0894ea05861e66c9090ae1012eabd4 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sat, 22 Mar 2025 16:38:05 +0530 Subject: [PATCH 04/20] switched email verification back to mandatory --- web/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/settings.py b/web/settings.py index 4e7fcec5a..5e595157c 100644 --- a/web/settings.py +++ b/web/settings.py @@ -173,7 +173,7 @@ # Allauth settings ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False # Since we're using email authentication -ACCOUNT_EMAIL_VERIFICATION = "optional" # Require email verification +ACCOUNT_EMAIL_VERIFICATION = "mandatory" # Require email verification ACCOUNT_LOGIN_METHODS = {"email"} ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_PREVENT_ENUMERATION = True # Prevent user enumeration From ca8907d3e9157f109a67fbcad8f9db3f281c36c9 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sat, 22 Mar 2025 17:05:17 +0530 Subject: [PATCH 05/20] coderabbit suggestions implemented --- web/forms.py | 6 ----- web/notifications.py | 1 + web/templates/waiting_room/list.html | 38 ---------------------------- web/views.py | 4 --- 4 files changed, 1 insertion(+), 48 deletions(-) diff --git a/web/forms.py b/web/forms.py index cf18ba8fb..f1981d25e 100644 --- a/web/forms.py +++ b/web/forms.py @@ -1394,12 +1394,6 @@ class Meta: model = WaitingRoom fields = ["title", "description", "subject", "topics"] - def clean_subject(self): - subject_name = self.cleaned_data.get("subject") - if not Subject.objects.filter(name=subject_name).exists(): - raise forms.ValidationError(f"Subject '{subject_name}' does not exist.") - return subject_name - widgets = { "title": TailwindInput(attrs={"placeholder": "What would you like to learn?"}), "description": TailwindTextarea(attrs={"rows": 4, "placeholder": "Describe what you want to learn"}), diff --git a/web/notifications.py b/web/notifications.py index 53be9a7c1..8b1f4a1d2 100644 --- a/web/notifications.py +++ b/web/notifications.py @@ -206,6 +206,7 @@ def notify_waiting_room_fulfilled(waiting_room, course): "user": participant, "waiting_room": waiting_room, "course": course, + "site_url": settings.SITE_URL, }, ) diff --git a/web/templates/waiting_room/list.html b/web/templates/waiting_room/list.html index 8cded8304..66aec1cd0 100644 --- a/web/templates/waiting_room/list.html +++ b/web/templates/waiting_room/list.html @@ -141,43 +141,5 @@

{{ room.title }}

{% endif %} - -
-

Open Waiting Rooms

- {% if waiting_rooms %} -
- {% for room in open_rooms %} - {% if room not in user_joined_rooms and room.creator != user %} -
-
-

{{ room.title }}

-

{{ room.description|truncatechars:100 }}

-
- Subject: {{ room.subject }} -
-
- Topics: -
- {% for topic in room_topics|get_item:room.id %} - {{ topic }} - {% endfor %} -
-
-
- {{ room.participants.count }} participant{{ room.participants.count|pluralize }} - View Details -
-
-
- {% endif %} - {% endfor %} -
- {% else %} -
-

No open waiting rooms available. Be the first to create one!

-
- {% endif %} -
{% endblock %} diff --git a/web/views.py b/web/views.py index f617160e8..c7598f084 100644 --- a/web/views.py +++ b/web/views.py @@ -4492,9 +4492,6 @@ def waiting_room_detail(request, waiting_room_id): # Check if the user is a teacher is_teacher = hasattr(request.user, "profile") and request.user.profile.is_teacher - # Find matching courses - # matching_courses = find_matching_courses(waiting_room) - context = { "waiting_room": waiting_room, "is_participant": is_participant, @@ -4502,7 +4499,6 @@ def waiting_room_detail(request, waiting_room_id): "is_teacher": is_teacher, "participant_count": waiting_room.participants.count(), "topic_list": [topic.strip() for topic in waiting_room.topics.split(",") if topic.strip()], - # 'matching_courses': matching_courses, } return render(request, "waiting_room/detail.html", context) From 814dd43d51bb84cbb894aa56107848ade98f4da0 Mon Sep 17 00:00:00 2001 From: Ishaan Arora <178517080+ishaan-arora-1@users.noreply.github.com> Date: Sun, 23 Mar 2025 01:41:02 +0530 Subject: [PATCH 06/20] Update and rename 0033_waitingroom_fulfilled_course.py to 0034_waitingroom_fulfilled_course.py --- ...fulfilled_course.py => 0034_waitingroom_fulfilled_course.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename web/migrations/{0033_waitingroom_fulfilled_course.py => 0034_waitingroom_fulfilled_course.py} (96%) diff --git a/web/migrations/0033_waitingroom_fulfilled_course.py b/web/migrations/0034_waitingroom_fulfilled_course.py similarity index 96% rename from web/migrations/0033_waitingroom_fulfilled_course.py rename to web/migrations/0034_waitingroom_fulfilled_course.py index 14ee90f3e..465929412 100644 --- a/web/migrations/0033_waitingroom_fulfilled_course.py +++ b/web/migrations/0034_waitingroom_fulfilled_course.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("web", "0032_rename_completion_date_teamgoalmember_completed_at_and_more"), + ("web", "0033_gradeablelink_linkgrade.py"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] From e5e39384c2d7cad571ceb888d207fd513b73dc75 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sun, 23 Mar 2025 03:14:14 +0530 Subject: [PATCH 07/20] upgraded migrations --- web/migrations/0034_waitingroom_fulfilled_course.py | 2 +- web/models.py | 4 +++- web/views.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/migrations/0034_waitingroom_fulfilled_course.py b/web/migrations/0034_waitingroom_fulfilled_course.py index 465929412..11d426fd9 100644 --- a/web/migrations/0034_waitingroom_fulfilled_course.py +++ b/web/migrations/0034_waitingroom_fulfilled_course.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("web", "0033_gradeablelink_linkgrade.py"), + ("web", "0033_gradeablelink_linkgrade"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] diff --git a/web/models.py b/web/models.py index 54252f34a..537f3de91 100644 --- a/web/models.py +++ b/web/models.py @@ -1784,6 +1784,9 @@ class WaitingRoom(models.Model): "Course", on_delete=models.SET_NULL, null=True, blank=True, related_name="fulfilled_waiting_rooms" ) + def __str__(self): + return self.title + class GradeableLink(models.Model): """Model for storing links that users want to get grades on.""" @@ -1804,7 +1807,6 @@ class GradeableLink(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - class Meta: ordering = ["-created_at"] diff --git a/web/views.py b/web/views.py index 7cac7c33f..641ea40ca 100644 --- a/web/views.py +++ b/web/views.py @@ -4469,7 +4469,7 @@ def create_waiting_room(request): def find_matching_courses(waiting_room): """Find courses that match the waiting room's subject and topics.""" # Get courses with matching subject name (case-insensitive) - matching_courses = Course.objects.filter(subject__name__iexact=waiting_room.subject, status="published") + matching_courses = Course.objects.filter(subject__iexact=waiting_room.subject, status="published") # Filter courses that have all required topics required_topics = {t.strip().lower() for t in waiting_room.topics.split(",") if t.strip()} From a4cf47844b2757eba2a6fbfd6008f4e0335b87b2 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sun, 23 Mar 2025 14:06:23 +0530 Subject: [PATCH 08/20] updated migrations --- ...fulfilled_course.py => 0038_waitingroom_fulfilled_course.py} | 2 +- web/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename web/migrations/{0034_waitingroom_fulfilled_course.py => 0038_waitingroom_fulfilled_course.py} (97%) diff --git a/web/migrations/0034_waitingroom_fulfilled_course.py b/web/migrations/0038_waitingroom_fulfilled_course.py similarity index 97% rename from web/migrations/0034_waitingroom_fulfilled_course.py rename to web/migrations/0038_waitingroom_fulfilled_course.py index 11d426fd9..dd39f7a49 100644 --- a/web/migrations/0034_waitingroom_fulfilled_course.py +++ b/web/migrations/0038_waitingroom_fulfilled_course.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("web", "0033_gradeablelink_linkgrade"), + ("web", "0037_profile_how_did_you_hear_about_us"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] diff --git a/web/views.py b/web/views.py index ff6e880a8..2019fc849 100644 --- a/web/views.py +++ b/web/views.py @@ -132,8 +132,8 @@ TeamGoalMember, TeamInvite, TimeSlot, - WaitingRoom, UserBadge, + WaitingRoom, WebRequest, ) from .notifications import ( From b4894787279fbd36a74b5e05be2f567a20637ee2 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sun, 23 Mar 2025 14:13:07 +0530 Subject: [PATCH 09/20] added proper verification --- web/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/views.py b/web/views.py index 2019fc849..ece39b8d3 100644 --- a/web/views.py +++ b/web/views.py @@ -4518,13 +4518,13 @@ def waiting_room_detail(request, waiting_room_id): waiting_room = get_object_or_404(WaitingRoom, id=waiting_room_id) # Check if the user is a participant - is_participant = request.user in waiting_room.participants.all() + is_participant = request.user.is_authenticated and request.user in waiting_room.participants.all() # Check if the user is the creator - is_creator = request.user == waiting_room.creator + is_creator = request.user.is_authenticated and request.user == waiting_room.creator # Check if the user is a teacher - is_teacher = hasattr(request.user, "profile") and request.user.profile.is_teacher + is_teacher = request.user.is_authenticated and hasattr(request.user, "profile") and request.user.profile.is_teacher context = { "waiting_room": waiting_room, From 211f9bce24697f0f24837aa88fa35f5bcd6fd6a2 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Mon, 24 Mar 2025 12:35:44 +0530 Subject: [PATCH 10/20] integrating with learn form --- web/forms.py | 39 ++++++++++++++++--- .../0039_alter_waitingroom_options.py | 17 ++++++++ web/templates/emails/learn_interest.html | 25 +++++++++--- web/views.py | 31 ++++++++++++--- 4 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 web/migrations/0039_alter_waitingroom_options.py diff --git a/web/forms.py b/web/forms.py index ae9499b8b..378fda167 100644 --- a/web/forms.py +++ b/web/forms.py @@ -733,12 +733,37 @@ class Meta: } -class LearnForm(forms.Form): - subject = forms.CharField( +class LearnForm(forms.ModelForm): + title = forms.CharField( max_length=100, widget=TailwindInput( attrs={ - "placeholder": "What would you like to learn?", + "placeholder": "Title for your learning request", + "class": "block w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-orange-500", + } + ), + ) + description = forms.CharField( + widget=TailwindTextarea( + attrs={ + "placeholder": "Describe what you want to learn in detail...", + "rows": 4, + "class": "block w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-orange-500", + } + ), + ) + subject = forms.ModelChoiceField( + queryset=Subject.objects.all().order_by('order', 'name'), + widget=TailwindSelect( + attrs={ + "class": "block w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-orange-500", + } + ), + ) + topics = forms.CharField( + widget=TailwindInput( + attrs={ + "placeholder": "Enter topics you want to learn (comma-separated)", "class": "block w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-orange-500", } ), @@ -754,8 +779,8 @@ class LearnForm(forms.Form): message = forms.CharField( widget=TailwindTextarea( attrs={ - "placeholder": "Tell us more about what you want to learn...", - "rows": 4, + "placeholder": "Any additional message or requirements...", + "rows": 3, "class": "block w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-orange-500", } ), @@ -767,6 +792,10 @@ class LearnForm(forms.Form): ) ) + class Meta: + model = WaitingRoom + fields = ['title', 'description', 'subject', 'topics'] + class TeachForm(forms.Form): subject = forms.CharField( diff --git a/web/migrations/0039_alter_waitingroom_options.py b/web/migrations/0039_alter_waitingroom_options.py new file mode 100644 index 000000000..aaa870eae --- /dev/null +++ b/web/migrations/0039_alter_waitingroom_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.6 on 2025-03-24 06:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("web", "0038_waitingroom_fulfilled_course"), + ] + + operations = [ + migrations.AlterModelOptions( + name="waitingroom", + options={}, + ), + ] diff --git a/web/templates/emails/learn_interest.html b/web/templates/emails/learn_interest.html index 71d8222f1..6dc45e5c6 100644 --- a/web/templates/emails/learn_interest.html +++ b/web/templates/emails/learn_interest.html @@ -41,17 +41,30 @@
-

New Learning Interest

+

New Learning Request

-

Subject Interest: {{ subject }}

-

- From: {{ email }} -

+

{{ title }}

+

Waiting Room ID: {{ waiting_room_id }}

+

Subject: {{ subject }}

+

Topics: {{ topics }}

+

From: {{ email }}

+ +

Description:

+

{{ description }}

+ {% if message %} -

Additional Information:

+

Additional Requirements:

{{ message }}

{% endif %} + +