Skip to content

Commit 384dc7f

Browse files
Rajgupta36arkid15r
andauthored
Feat/mentee page (#2567)
* feat: merged filter-sync-issue branch as squash * feat: merged contributor-interested branch and resolved conflicts * fix the migration * added view all issues page * added queries and pages * fix error handling * added pr and issue linking command * update ui added pull requests card * update code * bug fixes * implemented suggestions * fix bug * fix backend test case * added label on the module frontend * update suggestion added pagination * fix backend test case * update mock data * update code and added queries * update formatting * revert changes and update test cases * update component better error handling * remove n+1 query * remove unused try/catch block * add set deadline mutation and component * update suggestion * update fields * fix type * fix type * added mobile view and resolves comments * fix test cases * fix date bug * update code * logic seperation * trigger on close button * update-code * update sugggestoins --------- Co-authored-by: Arkadii Yakovets <[email protected]>
1 parent 6b20a94 commit 384dc7f

File tree

21 files changed

+806
-11
lines changed

21 files changed

+806
-11
lines changed

backend/apps/mentorship/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .issue_user_interest import IssueUserInterest
44
from .mentee import MenteeAdmin
5+
from .mentee_module import MenteeModuleAdmin
56
from .mentee_program import MenteeProgramAdmin
67
from .mentor import MentorAdmin
78
from .module import ModuleAdmin
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Mentorship app MenteeModule model admin."""
2+
3+
from django.contrib import admin
4+
5+
from apps.mentorship.models.mentee_module import MenteeModule
6+
7+
8+
@admin.register(MenteeModule)
9+
class MenteeModuleAdmin(admin.ModelAdmin):
10+
"""Admin view for MenteeModule model."""
11+
12+
list_display = (
13+
"mentee",
14+
"module",
15+
"started_at",
16+
"ended_at",
17+
)
18+
19+
list_filter = (
20+
"module__program",
21+
"started_at",
22+
"ended_at",
23+
)
24+
25+
search_fields = (
26+
"mentee__github_user__login",
27+
"mentee__github_user__name",
28+
"module__name",
29+
"module__program__name",
30+
)
31+
32+
ordering = ["mentee__github_user__login", "module__name"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""GraphQL node for Mentee model."""
2+
3+
import strawberry
4+
5+
from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum
6+
7+
8+
@strawberry.type
9+
class MenteeNode:
10+
"""A GraphQL node representing a mentorship mentee."""
11+
12+
id: str
13+
login: str
14+
name: str
15+
avatar_url: str
16+
bio: str | None = None
17+
experience_level: ExperienceLevelEnum
18+
domains: list[str] | None = None
19+
tags: list[str] | None = None
20+
21+
@strawberry.field(name="avatarUrl")
22+
def resolve_avatar_url(self) -> str:
23+
"""Get the GitHub avatar URL of the mentee."""
24+
return self.avatar_url
25+
26+
@strawberry.field(name="experienceLevel")
27+
def resolve_experience_level(self) -> str:
28+
"""Get the experience level of the mentee."""
29+
return self.experience_level if self.experience_level else "beginner"

backend/apps/mentorship/api/internal/nodes/module.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from apps.github.api.internal.nodes.issue import IssueNode
88
from apps.github.api.internal.nodes.user import UserNode
99
from apps.github.models import Label
10+
from apps.github.models.user import User
1011
from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum
1112
from apps.mentorship.api.internal.nodes.mentor import MentorNode
1213
from apps.mentorship.api.internal.nodes.program import ProgramNode
@@ -36,6 +37,34 @@ def mentors(self) -> list[MentorNode]:
3637
"""Get the list of mentors for this module."""
3738
return self.mentors.all()
3839

40+
@strawberry.field
41+
def mentees(self) -> list[UserNode]:
42+
"""Get the list of mentees for this module."""
43+
mentee_users = (
44+
self.menteemodule_set.select_related("mentee__github_user")
45+
.filter(mentee__github_user__isnull=False)
46+
.values_list("mentee__github_user", flat=True)
47+
)
48+
49+
return list(User.objects.filter(id__in=mentee_users).order_by("login"))
50+
51+
@strawberry.field
52+
def issue_mentees(self, issue_number: int) -> list[UserNode]:
53+
"""Return mentees assigned to this module's issue identified by its number."""
54+
issue_ids = list(self.issues.filter(number=issue_number).values_list("id", flat=True))
55+
if not issue_ids:
56+
return []
57+
58+
# Get mentees assigned to tasks for this issue
59+
mentee_users = (
60+
Task.objects.filter(module=self, issue_id__in=issue_ids, assignee__isnull=False)
61+
.select_related("assignee")
62+
.values_list("assignee", flat=True)
63+
.distinct()
64+
)
65+
66+
return list(User.objects.filter(id__in=mentee_users).order_by("login"))
67+
3968
@strawberry.field
4069
def project_name(self) -> str | None:
4170
"""Get the project name for this module."""

backend/apps/mentorship/api/internal/queries/mentorship.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
"""GraphQL queries for mentorship role management."""
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, cast
6+
37
import strawberry
8+
from django.core.exceptions import ObjectDoesNotExist
9+
from django.db.models import Prefetch
410

11+
from apps.github.api.internal.nodes.issue import IssueNode
12+
from apps.github.models import Label
513
from apps.github.models.user import User as GithubUser
14+
from apps.mentorship.api.internal.nodes.mentee import MenteeNode
15+
from apps.mentorship.models import Module
16+
from apps.mentorship.models.mentee import Mentee
17+
from apps.mentorship.models.mentee_module import MenteeModule
618
from apps.mentorship.models.mentor import Mentor
719

20+
if TYPE_CHECKING:
21+
from apps.github.api.internal.nodes.issue import IssueNode
22+
823

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

3348
return Mentor.objects.filter(github_user=github_user).exists()
49+
50+
@strawberry.field
51+
def get_mentee_details(
52+
self, program_key: str, module_key: str, mentee_handle: str
53+
) -> MenteeNode:
54+
"""Get detailed information about a mentee in a specific module."""
55+
try:
56+
module = Module.objects.only("id").get(key=module_key, program__key=program_key)
57+
58+
github_user = GithubUser.objects.only("login", "name", "avatar_url", "bio").get(
59+
login=mentee_handle
60+
)
61+
62+
mentee = Mentee.objects.only("id", "experience_level", "domains", "tags").get(
63+
github_user=github_user
64+
)
65+
is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists()
66+
67+
if not is_enrolled:
68+
message = f"Mentee {mentee_handle} is not enrolled in module {module_key}"
69+
raise ObjectDoesNotExist(message)
70+
71+
return MenteeNode(
72+
id=cast("strawberry.ID", str(mentee.id)),
73+
login=github_user.login,
74+
name=github_user.name or github_user.login,
75+
avatar_url=github_user.avatar_url,
76+
bio=github_user.bio,
77+
experience_level=mentee.experience_level,
78+
domains=mentee.domains,
79+
tags=mentee.tags,
80+
)
81+
82+
except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e:
83+
message = f"Mentee details not found: {e}"
84+
raise ObjectDoesNotExist(message) from e
85+
86+
@strawberry.field
87+
def get_mentee_module_issues(
88+
self,
89+
program_key: str,
90+
module_key: str,
91+
mentee_handle: str,
92+
limit: int = 20,
93+
offset: int = 0,
94+
) -> list[IssueNode]:
95+
"""Get issues assigned to a mentee in a specific module."""
96+
try:
97+
module = Module.objects.only("id").get(key=module_key, program__key=program_key)
98+
99+
github_user = GithubUser.objects.only("id").get(login=mentee_handle)
100+
101+
mentee = Mentee.objects.only("id").get(github_user=github_user)
102+
is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists()
103+
104+
if not is_enrolled:
105+
message = f"Mentee {mentee_handle} is not enrolled in module {module_key}"
106+
raise ObjectDoesNotExist(message)
107+
108+
issues_qs = (
109+
module.issues.filter(assignees=github_user)
110+
.only("id", "number", "title", "state", "created_at", "url")
111+
.prefetch_related(
112+
Prefetch("labels", queryset=Label.objects.only("id", "name")),
113+
Prefetch(
114+
"assignees",
115+
queryset=GithubUser.objects.only("id", "login", "name", "avatar_url"),
116+
),
117+
)
118+
.order_by("-created_at")
119+
)
120+
issues = issues_qs[offset : offset + limit]
121+
122+
return list(issues)
123+
124+
except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e:
125+
message = f"Mentee issues not found: {e}"
126+
raise ObjectDoesNotExist(message) from e

cspell/custom-dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ mastg
8383
mcr
8484
mdfile
8585
mentee
86+
menteemodule_set
8687
mentees
8788
mern
8889
millify

frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client'
22

33
import { useQuery } from '@apollo/client'
4+
import upperFirst from 'lodash/upperFirst'
45
import { useParams } from 'next/navigation'
56
import { useEffect, useState } from 'react'
67
import { ErrorDisplay, handleAppError } from 'app/global-error'
78
import { GET_PROGRAM_ADMINS_AND_MODULES } from 'server/queries/moduleQueries'
89
import type { Module } from 'types/mentorship'
9-
import { titleCaseWord } from 'utils/capitalize'
1010
import { formatDate } from 'utils/dateFormatter'
1111
import DetailsCard from 'components/CardDetailsPage'
1212
import LoadingSpinner from 'components/LoadingSpinner'
@@ -49,7 +49,7 @@ const ModuleDetailsPage = () => {
4949
}
5050

5151
const moduleDetails = [
52-
{ label: 'Experience Level', value: titleCaseWord(module.experienceLevel) },
52+
{ label: 'Experience Level', value: upperFirst(module.experienceLevel) },
5353
{ label: 'Start Date', value: formatDate(module.startedAt) },
5454
{ label: 'End Date', value: formatDate(module.endedAt) },
5555
{

0 commit comments

Comments
 (0)