Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
73aed2d
feat: merged filter-sync-issue branch as squash
Rajgupta36 Oct 8, 2025
2b03b65
feat: merged contributor-interested branch and resolved conflicts
Rajgupta36 Oct 8, 2025
a4e9fce
fix the migration
Rajgupta36 Sep 30, 2025
e69ba46
added view all issues page
Rajgupta36 Sep 30, 2025
52ab40a
added queries and pages
Rajgupta36 Oct 2, 2025
3c6a06b
fix error handling
Rajgupta36 Oct 2, 2025
553fecb
added pr and issue linking command
Rajgupta36 Oct 6, 2025
2e8aaed
update ui added pull requests card
Rajgupta36 Oct 7, 2025
5cd30e2
update code
Rajgupta36 Oct 7, 2025
ce1c571
bug fixes
Rajgupta36 Oct 8, 2025
857041c
implemented suggestions
Rajgupta36 Oct 8, 2025
77452c2
fix bug
Rajgupta36 Oct 8, 2025
2838aa0
fix backend test case
Rajgupta36 Oct 8, 2025
ca49e13
added label on the module frontend
Rajgupta36 Oct 9, 2025
b358fc2
update suggestion added pagination
Rajgupta36 Oct 12, 2025
ede2bc7
fix backend test case
Rajgupta36 Oct 12, 2025
fb70d24
update mock data
Rajgupta36 Oct 12, 2025
6e070a9
update code and added queries
Rajgupta36 Oct 14, 2025
f43cad7
update formatting
Rajgupta36 Oct 14, 2025
0851ebf
revert changes and update test cases
Rajgupta36 Oct 14, 2025
9400c37
update component better error handling
Rajgupta36 Oct 14, 2025
d7de361
remove n+1 query
Rajgupta36 Oct 14, 2025
7875028
remove unused try/catch block
Rajgupta36 Oct 14, 2025
86be65d
add set deadline mutation and component
Rajgupta36 Oct 16, 2025
42e41dd
update suggestion
Rajgupta36 Oct 16, 2025
77fa3bb
update fields
Rajgupta36 Oct 16, 2025
94af2ca
fix type
Rajgupta36 Oct 16, 2025
fd72300
fix type
Rajgupta36 Oct 16, 2025
b322dc2
added mobile view and resolves comments
Rajgupta36 Oct 18, 2025
fde2800
fix test cases
Rajgupta36 Oct 18, 2025
10da824
fix date bug
Rajgupta36 Oct 18, 2025
3584878
update code
Rajgupta36 Oct 19, 2025
d327b38
logic seperation
Rajgupta36 Oct 19, 2025
7165506
trigger on close button
Rajgupta36 Oct 21, 2025
d760622
update-code
Rajgupta36 Nov 7, 2025
4bc7c49
update sugggestoins
Rajgupta36 Nov 7, 2025
375302b
Merge branch 'feature/mentorship-portal' into pr/Rajgupta36/2567
arkid15r Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/apps/mentorship/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .issue_user_interest import IssueUserInterest
from .mentee import MenteeAdmin
from .mentee_module import MenteeModuleAdmin
from .mentee_program import MenteeProgramAdmin
from .mentor import MentorAdmin
from .module import ModuleAdmin
Expand Down
32 changes: 32 additions & 0 deletions backend/apps/mentorship/admin/mentee_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Mentorship app MenteeModule model admin."""

from django.contrib import admin

from apps.mentorship.models.mentee_module import MenteeModule


@admin.register(MenteeModule)
class MenteeModuleAdmin(admin.ModelAdmin):
"""Admin view for MenteeModule model."""

list_display = (
"mentee",
"module",
"started_at",
"ended_at",
)

list_filter = (
"module__program",
"started_at",
"ended_at",
)

search_fields = (
"mentee__github_user__login",
"mentee__github_user__name",
"module__name",
"module__program__name",
)

ordering = ["mentee__github_user__login", "module__name"]
29 changes: 29 additions & 0 deletions backend/apps/mentorship/api/internal/nodes/mentee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""GraphQL node for Mentee model."""

import strawberry

from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum


@strawberry.type
class MenteeNode:
"""A GraphQL node representing a mentorship mentee."""

id: str
login: str
name: str
avatar_url: str
bio: str | None = None
experience_level: ExperienceLevelEnum
domains: list[str] | None = None
tags: list[str] | None = None

@strawberry.field(name="avatarUrl")
def resolve_avatar_url(self) -> str:
"""Get the GitHub avatar URL of the mentee."""
return self.avatar_url

@strawberry.field(name="experienceLevel")
def resolve_experience_level(self) -> str:
"""Get the experience level of the mentee."""
return self.experience_level if self.experience_level else "beginner"
29 changes: 29 additions & 0 deletions backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models import Label
from apps.github.models.user import User
from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum
from apps.mentorship.api.internal.nodes.mentor import MentorNode
from apps.mentorship.api.internal.nodes.program import ProgramNode
Expand Down Expand Up @@ -36,6 +37,34 @@ def mentors(self) -> list[MentorNode]:
"""Get the list of mentors for this module."""
return self.mentors.all()

@strawberry.field
def mentees(self) -> list[UserNode]:
"""Get the list of mentees for this module."""
mentee_users = (
self.menteemodule_set.select_related("mentee__github_user")
.filter(mentee__github_user__isnull=False)
.values_list("mentee__github_user", flat=True)
)

return list(User.objects.filter(id__in=mentee_users).order_by("login"))

@strawberry.field
def issue_mentees(self, issue_number: int) -> list[UserNode]:
"""Return mentees assigned to this module's issue identified by its number."""
issue_ids = list(self.issues.filter(number=issue_number).values_list("id", flat=True))
if not issue_ids:
return []

# Get mentees assigned to tasks for this issue
mentee_users = (
Task.objects.filter(module=self, issue_id__in=issue_ids, assignee__isnull=False)
.select_related("assignee")
.values_list("assignee", flat=True)
.distinct()
)

return list(User.objects.filter(id__in=mentee_users).order_by("login"))

@strawberry.field
def project_name(self) -> str | None:
"""Get the project name for this module."""
Expand Down
93 changes: 93 additions & 0 deletions backend/apps/mentorship/api/internal/queries/mentorship.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
"""GraphQL queries for mentorship role management."""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

import strawberry
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Prefetch

from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.models import Label
from apps.github.models.user import User as GithubUser
from apps.mentorship.api.internal.nodes.mentee import MenteeNode
from apps.mentorship.models import Module
from apps.mentorship.models.mentee import Mentee
from apps.mentorship.models.mentee_module import MenteeModule
from apps.mentorship.models.mentor import Mentor

if TYPE_CHECKING:
from apps.github.api.internal.nodes.issue import IssueNode


@strawberry.type
class UserRolesResult:
Expand All @@ -31,3 +46,81 @@ def is_mentor(self, login: str) -> bool:
return False

return Mentor.objects.filter(github_user=github_user).exists()

@strawberry.field
def get_mentee_details(
self, program_key: str, module_key: str, mentee_handle: str
) -> MenteeNode:
"""Get detailed information about a mentee in a specific module."""
try:
module = Module.objects.only("id").get(key=module_key, program__key=program_key)

github_user = GithubUser.objects.only("login", "name", "avatar_url", "bio").get(
login=mentee_handle
)

mentee = Mentee.objects.only("id", "experience_level", "domains", "tags").get(
github_user=github_user
)
is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists()

if not is_enrolled:
message = f"Mentee {mentee_handle} is not enrolled in module {module_key}"
raise ObjectDoesNotExist(message)

return MenteeNode(
id=cast("strawberry.ID", str(mentee.id)),
login=github_user.login,
name=github_user.name or github_user.login,
avatar_url=github_user.avatar_url,
bio=github_user.bio,
experience_level=mentee.experience_level,
domains=mentee.domains,
tags=mentee.tags,
)

except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e:
message = f"Mentee details not found: {e}"
raise ObjectDoesNotExist(message) from e

@strawberry.field
def get_mentee_module_issues(
self,
program_key: str,
module_key: str,
mentee_handle: str,
limit: int = 20,
offset: int = 0,
) -> list[IssueNode]:
"""Get issues assigned to a mentee in a specific module."""
try:
module = Module.objects.only("id").get(key=module_key, program__key=program_key)

github_user = GithubUser.objects.only("id").get(login=mentee_handle)

mentee = Mentee.objects.only("id").get(github_user=github_user)
is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists()

if not is_enrolled:
message = f"Mentee {mentee_handle} is not enrolled in module {module_key}"
raise ObjectDoesNotExist(message)

issues_qs = (
module.issues.filter(assignees=github_user)
.only("id", "number", "title", "state", "created_at", "url")
.prefetch_related(
Prefetch("labels", queryset=Label.objects.only("id", "name")),
Prefetch(
"assignees",
queryset=GithubUser.objects.only("id", "login", "name", "avatar_url"),
),
)
.order_by("-created_at")
)
issues = issues_qs[offset : offset + limit]

return list(issues)

except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e:
message = f"Mentee issues not found: {e}"
raise ObjectDoesNotExist(message) from e
1 change: 1 addition & 0 deletions cspell/custom-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ mastg
mcr
mdfile
mentee
menteemodule_set
mentees
mern
millify
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client'

import { useQuery } from '@apollo/client'
import upperFirst from 'lodash/upperFirst'
import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { GET_PROGRAM_ADMINS_AND_MODULES } from 'server/queries/moduleQueries'
import type { Module } from 'types/mentorship'
import { titleCaseWord } from 'utils/capitalize'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
Expand Down Expand Up @@ -49,7 +49,7 @@ const ModuleDetailsPage = () => {
}

const moduleDetails = [
{ label: 'Experience Level', value: titleCaseWord(module.experienceLevel) },
{ label: 'Experience Level', value: upperFirst(module.experienceLevel) },
{ label: 'Start Date', value: formatDate(module.startedAt) },
{ label: 'End Date', value: formatDate(module.endedAt) },
{
Expand Down
Loading