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 %}
+
+
+
+{% 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 %}
+
+
+
+
+
{{ 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 }}
+
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+ {% 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 %}
+
+
+ {% 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
+
+
+
+ {% 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' %}
+
+ {% 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 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 %}
+
+ {% 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" }}
+
+
+
+
+{% 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 %}
+
+{% 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 }}
+
+
+
+{% 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})