From d5b5c4902bf577d68ef0e1a1613768ca0c0a6c0e Mon Sep 17 00:00:00 2001 From: ayesha1145 <130880100+ayesha1145@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:51:00 +0000 Subject: [PATCH] feat: add mentorship system with mentor profiles, requests, sessions, and ratings --- web/admin.py | 24 ++ web/migrations/0064_add_mentorship_models.py | 155 ++++++++++ web/models.py | 92 ++++++ web/templates/mentorship/become_mentor.html | 45 +++ .../mentorship/mentor_dashboard.html | 94 ++++++ web/templates/mentorship/mentor_list.html | 76 +++++ web/templates/mentorship/mentor_profile.html | 89 ++++++ web/templates/mentorship/my_mentorship.html | 76 +++++ web/templates/mentorship/rate_session.html | 39 +++ .../mentorship/request_mentorship.html | 37 +++ .../mentorship/schedule_session.html | 39 +++ web/urls.py | 13 + web/views.py | 289 ++++++++++++++++++ 13 files changed, 1068 insertions(+) create mode 100644 web/migrations/0064_add_mentorship_models.py create mode 100644 web/templates/mentorship/become_mentor.html create mode 100644 web/templates/mentorship/mentor_dashboard.html create mode 100644 web/templates/mentorship/mentor_list.html create mode 100644 web/templates/mentorship/mentor_profile.html create mode 100644 web/templates/mentorship/my_mentorship.html create mode 100644 web/templates/mentorship/rate_session.html create mode 100644 web/templates/mentorship/request_mentorship.html create mode 100644 web/templates/mentorship/schedule_session.html diff --git a/web/admin.py b/web/admin.py index a0eb4328f..22432ae7e 100644 --- a/web/admin.py +++ b/web/admin.py @@ -28,6 +28,9 @@ ForumTopic, Goods, LearningStreak, + MentorProfile, + MentorshipRequest, + MentorshipSession, MembershipPlan, MembershipSubscriptionEvent, Notification, @@ -610,6 +613,27 @@ class ChallengeSubmissionAdmin(admin.ModelAdmin): # Unregister the default User admin and register our custom one admin.site.unregister(User) +@admin.register(MentorProfile) +class MentorProfileAdmin(admin.ModelAdmin): + list_display = ("user", "is_active", "is_free", "hourly_rate", "availability", "created_at") + list_filter = ("is_active", "is_free", "availability") + search_fields = ("user__username", "user__email") + + +@admin.register(MentorshipRequest) +class MentorshipRequestAdmin(admin.ModelAdmin): + list_display = ("student", "mentor", "status", "created_at") + list_filter = ("status",) + search_fields = ("student__username", "mentor__user__username") + + +@admin.register(MentorshipSession) +class MentorshipSessionAdmin(admin.ModelAdmin): + list_display = ("mentor", "student", "scheduled_at", "status", "rating") + list_filter = ("status",) + search_fields = ("mentor__user__username", "student__username") + + admin.site.register(User, CustomUserAdmin) diff --git a/web/migrations/0064_add_mentorship_models.py b/web/migrations/0064_add_mentorship_models.py new file mode 100644 index 000000000..841aa968e --- /dev/null +++ b/web/migrations/0064_add_mentorship_models.py @@ -0,0 +1,155 @@ +# Generated by Django 5.1.15 on 2026-03-22 15:55 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("web", "0063_virtualclassroom_virtualclassroomcustomization_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="MentorProfile", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("bio", models.TextField(blank=True)), + ("experience_years", models.PositiveIntegerField(default=0)), + ("hourly_rate", models.DecimalField(decimal_places=2, default=0.0, max_digits=8)), + ("is_free", models.BooleanField(default=True)), + ( + "availability", + models.CharField( + choices=[ + ("weekdays", "Weekdays"), + ("weekends", "Weekends"), + ("evenings", "Evenings"), + ("flexible", "Flexible"), + ], + default="flexible", + max_length=20, + ), + ), + ("is_active", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("subjects", models.ManyToManyField(blank=True, related_name="mentors", to="web.subject")), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentor_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="MentorshipSession", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("scheduled_at", models.DateTimeField()), + ("duration_minutes", models.PositiveIntegerField(default=60)), + ( + "status", + models.CharField( + choices=[("scheduled", "Scheduled"), ("completed", "Completed"), ("cancelled", "Cancelled")], + default="scheduled", + max_length=10, + ), + ), + ("notes", models.TextField(blank=True)), + ( + "rating", + models.PositiveIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ("review", models.TextField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "mentor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="sessions", to="web.mentorprofile" + ), + ), + ( + "student", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentorship_sessions", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "subject", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="web.subject" + ), + ), + ], + options={ + "ordering": ["-scheduled_at"], + }, + ), + migrations.CreateModel( + name="MentorshipRequest", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("message", models.TextField()), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("accepted", "Accepted"), + ("declined", "Declined"), + ("cancelled", "Cancelled"), + ], + default="pending", + max_length=10, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "mentor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="requests", to="web.mentorprofile" + ), + ), + ( + "student", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentorship_requests", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "subject", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="web.subject" + ), + ), + ], + options={ + "ordering": ["-created_at"], + + }, + ), + ] diff --git a/web/models.py b/web/models.py index 6b8ea8ef4..ce21bf33d 100644 --- a/web/models.py +++ b/web/models.py @@ -3176,3 +3176,95 @@ class Meta: ordering = ["-last_updated"] verbose_name = "Virtual Classroom Whiteboard" verbose_name_plural = "Virtual Classroom Whiteboards" + + +class MentorProfile(models.Model): + """A user who offers mentorship in one or more subjects.""" + + AVAILABILITY_CHOICES = [ + ("weekdays", "Weekdays"), + ("weekends", "Weekends"), + ("evenings", "Evenings"), + ("flexible", "Flexible"), + ] + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="mentor_profile") + subjects = models.ManyToManyField(Subject, related_name="mentors", blank=True) + bio = models.TextField(blank=True) + experience_years = models.PositiveIntegerField(default=0) + hourly_rate = models.DecimalField(max_digits=8, decimal_places=2, default=0.00) + is_free = models.BooleanField(default=True) + availability = models.CharField(max_length=20, choices=AVAILABILITY_CHOICES, default="flexible") + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return f"{self.user.username} - Mentor" + + @property + def average_rating(self): + result = self.sessions.filter(rating__isnull=False).aggregate(avg=Avg("rating"))["avg"] + return round(result, 1) if result else None + + @property + def total_sessions(self) -> int: + return self.sessions.filter(status="completed").count() + + +class MentorshipRequest(models.Model): + """A student request for mentorship.""" + + STATUS_CHOICES = [ + ("pending", "Pending"), + ("accepted", "Accepted"), + ("declined", "Declined"), + ("cancelled", "Cancelled"), + ] + + mentor = models.ForeignKey(MentorProfile, on_delete=models.CASCADE, related_name="requests") + student = models.ForeignKey(User, on_delete=models.CASCADE, related_name="mentorship_requests") + subject = models.ForeignKey(Subject, on_delete=models.SET_NULL, null=True, blank=True) + message = models.TextField() + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="pending") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return f"{self.student.username} -> {self.mentor.user.username} ({self.status})" + + +class MentorshipSession(models.Model): + """A scheduled or completed 1-on-1 mentorship session.""" + + STATUS_CHOICES = [ + ("scheduled", "Scheduled"), + ("completed", "Completed"), + ("cancelled", "Cancelled"), + ] + + mentor = models.ForeignKey(MentorProfile, on_delete=models.CASCADE, related_name="sessions") + student = models.ForeignKey(User, on_delete=models.CASCADE, related_name="mentorship_sessions") + subject = models.ForeignKey(Subject, on_delete=models.SET_NULL, null=True, blank=True) + scheduled_at = models.DateTimeField() + duration_minutes = models.PositiveIntegerField(default=60, validators=[MinValueValidator(15), MaxValueValidator(240)]) + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="scheduled") + notes = models.TextField(blank=True) + rating = models.PositiveIntegerField( + null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + review = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-scheduled_at"] + + def __str__(self) -> str: + return f"{self.mentor.user.username} + {self.student.username} @ {self.scheduled_at:%Y-%m-%d %H:%M}" diff --git a/web/templates/mentorship/become_mentor.html b/web/templates/mentorship/become_mentor.html new file mode 100644 index 000000000..259704d81 --- /dev/null +++ b/web/templates/mentorship/become_mentor.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Become a Mentor{% endblock title %} +{% block content %} +
+
+

{% if mentor %}Edit Mentor Profile{% else %}Become a Mentor{% endif %}

+
+ {% csrf_token %} +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+ +
+ {% for s in subjects %}{% endfor %} +
+
+
+ + Cancel +
+
+
+
+{% endblock content %} diff --git a/web/templates/mentorship/mentor_dashboard.html b/web/templates/mentorship/mentor_dashboard.html new file mode 100644 index 000000000..f023570c0 --- /dev/null +++ b/web/templates/mentorship/mentor_dashboard.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} +{% block title %}Mentor Dashboard{% endblock title %} +{% block content %} +
+
+

Mentor Dashboard

+ Edit Profile +
+
+
+

{{ pending_requests.count }}

+

Pending Requests

+
+
+

{{ upcoming_sessions.count }}

+

Upcoming Sessions

+
+
+

{{ mentor.total_sessions }}

+

Completed Sessions

+
+
+
+
+

Pending Requests

+ {% if pending_requests %} +
+ {% for req in pending_requests %} +
+
+

{{ req.student.get_full_name|default:req.student.username }}

+ {% if req.subject %}{{ req.subject.name }}{% endif %} +
+

{{ req.message }}

+
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+ {% endfor %} +
+ {% else %} +
+

No pending requests.

+
+ {% endif %} + {% if accepted_requests %} +

Ready to Schedule

+
+ {% for req in accepted_requests %} +
+
+

{{ req.student.get_full_name|default:req.student.username }}

+ {% if req.subject %}

{{ req.subject.name }}

{% endif %} +
+ Schedule +
+ {% endfor %} +
+ {% endif %} +
+
+

Upcoming Sessions

+ {% if upcoming_sessions %} +
+ {% for s in upcoming_sessions %} +
+
+

{{ s.student.get_full_name|default:s.student.username }}

+ {{ s.scheduled_at|date:"M d, Y H:i" }} +
+

{{ s.duration_minutes }} min{% if s.subject %} - {{ s.subject.name }}{% endif %}

+
+ {% csrf_token %} + +
+
+ {% endfor %} +
+ {% else %} +
+

No upcoming sessions.

+
+ {% endif %} +
+
+
+{% endblock content %} diff --git a/web/templates/mentorship/mentor_list.html b/web/templates/mentorship/mentor_list.html new file mode 100644 index 000000000..665a5a498 --- /dev/null +++ b/web/templates/mentorship/mentor_list.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} +{% block title %}Find a Mentor{% endblock title %} +{% block content %} +
+
+
+

Find a Mentor

+

Connect with experienced mentors in your area of interest.

+
+ + Become a Mentor + +
+
+
+ + +
+
+ + +
+
+ + +
+ + Clear +
+ {% if mentors %} +
+ {% for mentor in mentors %} +
+
+ {% if mentor.user.profile.avatar %} + + {% else %} +
+ +
+ {% endif %} +
+

{{ mentor.user.get_full_name|default:mentor.user.username }}

+

{{ mentor.experience_years }} yr{{ mentor.experience_years|pluralize }} experience

+ {% if mentor.average_rating %} + {{ mentor.average_rating }} ({{ mentor.total_sessions }} sessions) + {% endif %} +
+
+ {% if mentor.bio %}

{{ mentor.bio }}

{% endif %} +
+ {% for s in mentor.subjects.all %}{{ s.name }}{% endfor %} +
+
+ + {% if mentor.is_free %}Free{% else %}${{ mentor.hourly_rate }}/hr{% endif %} + + View Profile +
+
+ {% endfor %} +
+ {% else %} +
+ +

No mentors found. Try adjusting your filters.

+
+ {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/web/templates/mentorship/mentor_profile.html b/web/templates/mentorship/mentor_profile.html new file mode 100644 index 000000000..22b810709 --- /dev/null +++ b/web/templates/mentorship/mentor_profile.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} +{% block title %}{{ mentor.user.get_full_name|default:mentor.user.username }} - Mentor Profile{% endblock title %} +{% block content %} +
+ +
+
+
+
+ {% if mentor.user.profile.avatar %} + + {% else %} +
+ +
+ {% endif %} +
+

{{ mentor.user.get_full_name|default:mentor.user.username }}

+

{{ mentor.experience_years }} year{{ mentor.experience_years|pluralize }} of experience

+ {% if mentor.average_rating %} + {{ mentor.average_rating }} avg rating • {{ mentor.total_sessions }} sessions + {% endif %} +
+
+ {% if mentor.bio %} +

{{ mentor.bio }}

+ {% endif %} +
+ {% for s in mentor.subjects.all %} + {{ s.name }} + {% endfor %} +
+
+ {{ mentor.get_availability_display }} + + {% if mentor.is_free %}Free{% else %}${{ mentor.hourly_rate }}/hr{% endif %} + +
+
+ {% if reviews %} +
+

Reviews

+
+ {% for r in reviews %} +
+
+ {{ r.student.get_full_name|default:r.student.username }} + + {% for i in "12345" %}{% if forloop.counter <= r.rating %}{% else %}{% endif %}{% endfor %} + +
+ {% if r.review %}

{{ r.review }}

{% endif %} +
+ {% endfor %} +
+
+ {% endif %} +
+
+
+ {% if existing_request %} +
+ + Request {{ existing_request.get_status_display }} + + {% if existing_request.status == 'pending' %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ {% else %} + + Request Mentorship + + {% endif %} +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/web/templates/mentorship/my_mentorship.html b/web/templates/mentorship/my_mentorship.html new file mode 100644 index 000000000..fe25c6974 --- /dev/null +++ b/web/templates/mentorship/my_mentorship.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} +{% block title %}My Mentorship{% endblock title %} +{% block content %} +
+
+

My Mentorship

+ Find Mentors +
+
+
+

My Requests

+ {% if requests_sent %} +
+ {% for req in requests_sent %} +
+
+
+

{{ req.mentor.user.get_full_name|default:req.mentor.user.username }}

+ {% if req.subject %}

{{ req.subject.name }}

{% endif %} +

{{ req.created_at|date:"M d, Y" }}

+
+ + {{ req.get_status_display }} + +
+
+ {% endfor %} +
+ {% else %} +
+ +

No requests sent yet.

+ Find a mentor +
+ {% endif %} +
+
+

My Sessions

+ {% if sessions %} +
+ {% for s in sessions %} +
+
+

{{ s.mentor.user.get_full_name|default:s.mentor.user.username }}

+ + {{ s.get_status_display }} + +
+

{{ s.scheduled_at|date:"M d, Y H:i" }} • {{ s.duration_minutes }} min

+ {% if s.status == 'completed' and not s.rating %} + Rate this session + {% elif s.rating %} + + {% for i in "12345" %}{% if forloop.counter <= s.rating %}{% endif %}{% endfor %} + + {% endif %} +
+ {% endfor %} +
+ {% else %} +
+ +

No sessions yet.

+
+ {% endif %} +
+
+
+{% endblock content %} \ No newline at end of file diff --git a/web/templates/mentorship/rate_session.html b/web/templates/mentorship/rate_session.html new file mode 100644 index 000000000..3458be15b --- /dev/null +++ b/web/templates/mentorship/rate_session.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Rate Session{% endblock title %} +{% block content %} +
+
+

Rate Your Session

+

+ with {{ session.mentor.user.get_full_name|default:session.mentor.user.username }} + on {{ session.scheduled_at|date:"M d, Y" }} +

+
+ {% csrf_token %} +
+ +
+ {% for i in "12345" %} + + {% endfor %} +
+
+
+ + +
+
+ + Skip +
+
+
+
+{% endblock content %} diff --git a/web/templates/mentorship/request_mentorship.html b/web/templates/mentorship/request_mentorship.html new file mode 100644 index 000000000..d3699ff9d --- /dev/null +++ b/web/templates/mentorship/request_mentorship.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block title %}Request Mentorship{% endblock title %} +{% block content %} +
+ +
+

Request Mentorship

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + Cancel +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/web/templates/mentorship/schedule_session.html b/web/templates/mentorship/schedule_session.html new file mode 100644 index 000000000..2d36c2775 --- /dev/null +++ b/web/templates/mentorship/schedule_session.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Schedule Session{% endblock title %} +{% block content %} +
+ +
+

Schedule Session

+

with {{ mentorship_request.student.get_full_name|default:mentorship_request.student.username }}

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + Cancel +
+
+
+
+{% endblock content %} diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..1fb3cf71d 100644 --- a/web/urls.py +++ b/web/urls.py @@ -94,6 +94,19 @@ path("profile/", views.profile, name="profile"), path("accounts/profile/", views.profile, name="accounts_profile"), path("accounts/delete/", views.delete_account, name="delete_account"), + + # Mentorship URLs + path('mentorship/', views.mentor_list, name='mentor_list'), + path('mentorship/become/', views.become_mentor, name='become_mentor'), + path('mentorship/dashboard/', views.mentor_dashboard, name='mentor_dashboard'), + path('mentorship/my/', views.my_mentorship, name='my_mentorship'), + path('mentorship//', views.mentor_profile_view, name='mentor_profile_view'), + path('mentorship//request/', views.request_mentorship, name='request_mentorship'), + path('mentorship/requests//respond/', views.respond_to_request, name='respond_to_request'), + path('mentorship/requests//cancel/', views.cancel_mentorship_request, name='cancel_mentorship_request'), + path('mentorship/requests//schedule/', views.schedule_session, name='schedule_session'), + path('mentorship/sessions//complete/', views.complete_session, name='complete_session'), + path('mentorship/sessions//rate/', views.rate_session, name='rate_session'), # Dashboard URLs path("dashboard/student/", views.student_dashboard, name="student_dashboard"), path("dashboard/teacher/", views.teacher_dashboard, name="teacher_dashboard"), diff --git a/web/views.py b/web/views.py index b4d485749..1e61cb9a4 100644 --- a/web/views.py +++ b/web/views.py @@ -114,6 +114,9 @@ from .models import ( Achievement, Badge, + MentorProfile, + MentorshipRequest, + MentorshipSession, BlogComment, BlogPost, CartItem, @@ -8839,3 +8842,289 @@ def leave_session_waiting_room(request, course_slug): messages.info(request, "You are not in the session waiting room for this course.") return redirect("course_detail", slug=course_slug) + + +# --- Mentorship Views --- + + +@login_required +def mentor_list(request): + mentors = MentorProfile.objects.filter(is_active=True).select_related( + "user", "user__profile" + ).prefetch_related("subjects") + subject_id = request.GET.get("subject") + availability = request.GET.get("availability") + is_free = request.GET.get("is_free") + if subject_id: + mentors = mentors.filter(subjects__id=subject_id) + if availability: + mentors = mentors.filter(availability=availability) + if is_free == "1": + mentors = mentors.filter(is_free=True) + subjects = Subject.objects.all() + return render(request, "mentorship/mentor_list.html", { + "mentors": mentors, + "subjects": subjects, + "selected_subject": subject_id, + "selected_availability": availability, + "is_free": is_free, + "availability_choices": MentorProfile.AVAILABILITY_CHOICES, + }) + + +@login_required +def mentor_profile_view(request, mentor_id): + mentor = get_object_or_404(MentorProfile, id=mentor_id, is_active=True) + existing_request = MentorshipRequest.objects.filter( + mentor=mentor, student=request.user, status__in=["pending", "accepted"] + ).first() + reviews = MentorshipSession.objects.filter( + mentor=mentor, status="completed", rating__isnull=False + ).select_related("student").order_by("-scheduled_at")[:5] + return render(request, "mentorship/mentor_profile.html", { + "mentor": mentor, + "existing_request": existing_request, + "reviews": reviews, + }) + + +@login_required +def request_mentorship(request, mentor_id): + mentor = get_object_or_404(MentorProfile, id=mentor_id, is_active=True) + if mentor.user == request.user: + messages.error(request, "You cannot request mentorship from yourself.") + return redirect("mentor_profile_view", mentor_id=mentor_id) + existing = MentorshipRequest.objects.filter( + mentor=mentor, student=request.user, status__in=["pending", "accepted"] + ).first() + if existing: + messages.error(request, "You have already sent a request to this mentor.") + return redirect("mentor_profile_view", mentor_id=mentor_id) + if request.method == "POST": + message = request.POST.get("message", "").strip() + subject_id = request.POST.get("subject") + if not message: + messages.error(request, "Please include a message with your request.") + return redirect("request_mentorship", mentor_id=mentor_id) + subject = None + if subject_id: + subject = mentor.subjects.filter(id=subject_id).first() + if not subject: + messages.error(request, "Please select a valid subject from the mentor profile.") + return redirect("request_mentorship", mentor_id=mentor_id) + MentorshipRequest.objects.create( + mentor=mentor, student=request.user, subject=subject, message=message + ) + messages.success(request, "Mentorship request sent!") + return redirect("mentor_profile_view", mentor_id=mentor_id) + return render(request, "mentorship/request_mentorship.html", { + "mentor": mentor, "subjects": mentor.subjects.all() + }) + + +@login_required +def my_mentorship(request): + requests_sent = MentorshipRequest.objects.filter( + student=request.user + ).select_related("mentor__user", "subject").order_by("-created_at") + sessions = MentorshipSession.objects.filter( + student=request.user + ).select_related("mentor__user", "subject").order_by("-scheduled_at") + return render(request, "mentorship/my_mentorship.html", { + "requests_sent": requests_sent, + "sessions": sessions, + }) + + +@login_required +def cancel_mentorship_request(request, request_id): + mentorship_request = get_object_or_404( + MentorshipRequest, id=request_id, student=request.user, status="pending" + ) + if request.method == "POST": + mentorship_request.status = "cancelled" + mentorship_request.save(update_fields=["status", "updated_at"]) + messages.success(request, "Request cancelled.") + return redirect("my_mentorship") + + +@login_required +def become_mentor(request): + try: + mentor = request.user.mentor_profile + except MentorProfile.DoesNotExist: + mentor = None + if request.method == "POST": + bio = request.POST.get("bio", "").strip() + try: + experience_years = max(0, int(request.POST.get("experience_years", 0))) + except (ValueError, TypeError): + messages.error(request, "Please enter a valid number for years of experience.") + return redirect("become_mentor") + is_free = request.POST.get("is_free") == "on" + try: + hourly_rate = max(0.0, float(request.POST.get("hourly_rate", 0))) + except (ValueError, TypeError): + messages.error(request, "Please enter a valid hourly rate.") + return redirect("become_mentor") + availability = request.POST.get("availability", "flexible") + if availability not in dict(MentorProfile.AVAILABILITY_CHOICES): + availability = "flexible" + subject_ids = request.POST.getlist("subjects") + if mentor: + mentor.bio = bio + mentor.experience_years = experience_years + mentor.is_free = is_free + mentor.hourly_rate = hourly_rate + mentor.availability = availability + mentor.is_active = True + mentor.save() + else: + mentor = MentorProfile.objects.create( + user=request.user, + bio=bio, + experience_years=experience_years, + is_free=is_free, + hourly_rate=hourly_rate, + availability=availability, + ) + mentor.subjects.set(Subject.objects.filter(id__in=subject_ids)) + messages.success(request, "Mentor profile saved!") + return redirect("mentor_dashboard") + return render(request, "mentorship/become_mentor.html", { + "mentor": mentor, + "subjects": Subject.objects.all(), + "availability_choices": MentorProfile.AVAILABILITY_CHOICES, + }) + + +@login_required +def mentor_dashboard(request): + try: + mentor = request.user.mentor_profile + except MentorProfile.DoesNotExist: + messages.error(request, "You do not have a mentor profile.") + return redirect("become_mentor") + pending_requests = mentor.requests.filter(status="pending").select_related("student", "subject") + accepted_requests = mentor.requests.filter(status="accepted").select_related("student", "subject") + upcoming_sessions = mentor.sessions.filter( + status="scheduled", scheduled_at__gte=timezone.now() + ).select_related("student", "subject").order_by("scheduled_at") + past_sessions = mentor.sessions.filter( + status="completed" + ).select_related("student", "subject").order_by("-scheduled_at")[:10] + return render(request, "mentorship/mentor_dashboard.html", { + "mentor": mentor, + "pending_requests": pending_requests, + "accepted_requests": accepted_requests, + "upcoming_sessions": upcoming_sessions, + "past_sessions": past_sessions, + }) + + +@login_required +def respond_to_request(request, request_id): + try: + mentor = request.user.mentor_profile + except MentorProfile.DoesNotExist: + return redirect("become_mentor") + mentorship_request = get_object_or_404( + MentorshipRequest, id=request_id, mentor=mentor, status="pending" + ) + if request.method == "POST": + action = request.POST.get("action") + if action == "accept": + mentorship_request.status = "accepted" + messages.success(request, f"Request from {mentorship_request.student.username} accepted.") + elif action == "decline": + mentorship_request.status = "declined" + messages.success(request, "Request declined.") + mentorship_request.save(update_fields=["status", "updated_at"]) + return redirect("mentor_dashboard") + + +@login_required +def schedule_session(request, request_id): + try: + mentor = request.user.mentor_profile + except MentorProfile.DoesNotExist: + return redirect("become_mentor") + mentorship_request = get_object_or_404( + MentorshipRequest, id=request_id, mentor=mentor, status="accepted" + ) + if request.method == "POST": + from django.utils.dateparse import parse_datetime + scheduled_at_raw = parse_datetime(request.POST.get("scheduled_at", "").strip()) + if not scheduled_at_raw: + messages.error(request, "Please provide a valid date and time.") + return redirect("schedule_session", request_id=request_id) + import datetime + if timezone.is_naive(scheduled_at_raw): + scheduled_at = timezone.make_aware(scheduled_at_raw) + else: + scheduled_at = scheduled_at_raw + if scheduled_at <= timezone.now(): + messages.error(request, "Session must be scheduled in the future.") + return redirect("schedule_session", request_id=request_id) + try: + duration = int(request.POST.get("duration_minutes", 60)) + if duration < 15: + raise ValueError + except (ValueError, TypeError): + duration = 60 + notes = request.POST.get("notes", "").strip() + MentorshipSession.objects.create( + mentor=mentor, + student=mentorship_request.student, + subject=mentorship_request.subject, + scheduled_at=scheduled_at, + duration_minutes=duration, + notes=notes, + ) + messages.success(request, "Session scheduled!") + return redirect("mentor_dashboard") + return render(request, "mentorship/schedule_session.html", { + "mentor": mentor, "mentorship_request": mentorship_request + }) + + +@login_required +def complete_session(request, session_id): + try: + mentor = request.user.mentor_profile + except MentorProfile.DoesNotExist: + return redirect("become_mentor") + session = get_object_or_404(MentorshipSession, id=session_id, mentor=mentor, status="scheduled") + if request.method == "POST": + if session.scheduled_at > timezone.now(): + messages.error(request, "You cannot complete a session that has not started yet.") + return redirect("mentor_dashboard") + session.status = "completed" + session.notes = request.POST.get("notes", session.notes).strip() + session.save(update_fields=["status", "notes", "updated_at"]) + messages.success(request, "Session marked as completed.") + return redirect("mentor_dashboard") + + +@login_required +def rate_session(request, session_id): + session = get_object_or_404( + MentorshipSession, id=session_id, student=request.user, status="completed" + ) + if session.rating: + messages.error(request, "You have already rated this session.") + return redirect("my_mentorship") + if request.method == "POST": + try: + rating = int(request.POST.get("rating", 0)) + if rating < 1 or rating > 5: + raise ValueError + except (ValueError, TypeError): + messages.error(request, "Rating must be between 1 and 5.") + return redirect("rate_session", session_id=session_id) + session.rating = rating + session.review = request.POST.get("review", "").strip() + session.save(update_fields=["rating", "review", "updated_at"]) + messages.success(request, "Thank you for your feedback!") + return redirect("my_mentorship") + return render(request, "mentorship/rate_session.html", {"session": session})