Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
591adf7
fixed day left incorrect data
anurag2787 Jan 30, 2026
b39aaab
added deadline cols
anurag2787 Jan 31, 2026
e62dbb5
added filtering option
anurag2787 Jan 31, 2026
9786093
fixed coderabbit review
anurag2787 Jan 31, 2026
38ee33a
fixed review
anurag2787 Jan 31, 2026
cfa29cb
Merge branch 'main' into feature/deadline-status
anurag2787 Feb 7, 2026
5bc601b
fixed coderabbit review
anurag2787 Feb 7, 2026
750cda9
Merge branch 'main' into feature/deadline-status
anurag2787 Feb 19, 2026
f483e97
fixed merge conflict
anurag2787 Feb 19, 2026
87cf76b
revert changes
anurag2787 Feb 19, 2026
8caddab
Merge branch 'main' into feature/deadline-status
anurag2787 Feb 23, 2026
b5979eb
Merge branch 'main' into feature/deadline-status
anurag2787 Feb 27, 2026
e400b77
Merge branch 'main' into feature/deadline-status
anurag2787 Mar 3, 2026
fd9825e
Merge branch 'main' into feature/deadline-status
anurag2787 Mar 6, 2026
12cc228
Merge branch 'main' into feature/deadline-status
anurag2787 Mar 10, 2026
5d54ef2
Added deadline to mentee also
anurag2787 Mar 10, 2026
2005824
Merge branch 'main' into feature/deadline-status
anurag2787 Mar 10, 2026
599d826
fixed review
anurag2787 Mar 10, 2026
d249754
added lable
anurag2787 Mar 10, 2026
92f34e8
revert
anurag2787 Mar 10, 2026
39330e7
fixed sonarqube warning
anurag2787 Mar 10, 2026
c2f3905
Merge branch 'main' of github.com:anurag2787/Nest into feature/deadli…
anurag2787 Mar 14, 2026
430996d
Improved test coverage to above 95%
anurag2787 Mar 14, 2026
ec82664
fixed sonarqube warning
anurag2787 Mar 14, 2026
093b6f0
Merge branch 'main' of github.com:OWASP/Nest into pr/anurag2787/3695
kasya Mar 14, 2026
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
11 changes: 11 additions & 0 deletions backend/apps/github/api/internal/nodes/issue.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""GitHub issue GraphQL node."""

from datetime import datetime

import strawberry
import strawberry_django
from strawberry.types import Info

from apps.github.api.internal.nodes.pull_request import PullRequestNode
from apps.github.api.internal.nodes.user import UserNode
Expand Down Expand Up @@ -59,3 +62,11 @@ def interested_users(self, root: Issue) -> list[UserNode]:
"user__login"
)
]

@strawberry.field
def task_deadline(self, root: Issue, info: Info) -> datetime | None:
"""Return the deadline for the latest assigned task linked to this issue."""
mapping = getattr(info.context, "task_deadlines_by_issue", None)
if mapping is None:
return None
return mapping.get(root.number)
43 changes: 38 additions & 5 deletions backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime

import strawberry
from strawberry.types import Info

from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.pull_request import PullRequestNode
Expand Down Expand Up @@ -76,9 +77,30 @@ def project_name(self) -> str | None:

@strawberry.field
def issues(
self, limit: int = 20, offset: int = 0, label: str | None = None
self, info: Info, limit: int = 20, offset: int = 0, label: str | None = None
) -> list[IssueNode]:
"""Return paginated issues linked to this module, optionally filtered by label."""
info.context.current_module = self

# BULK load data
task_rows = (
Task.objects.filter(module=self, deadline_at__isnull=False)
.order_by("issue__number", "-assigned_at")
.values("issue__number", "deadline_at", "assigned_at")
)

deadline_map = {}
assigned_map = {}

for row in task_rows:
num = row["issue__number"]
if num not in deadline_map:
deadline_map[num] = row["deadline_at"]
assigned_map[num] = row["assigned_at"]

info.context.task_deadlines_by_issue = deadline_map
info.context.task_assigned_at_by_issue = assigned_map

Comment thread
anurag2787 marked this conversation as resolved.
queryset = self.issues.select_related("repository", "author").prefetch_related(
"assignees", "labels"
)
Expand Down Expand Up @@ -110,8 +132,10 @@ def available_labels(self) -> list[str]:
return sorted(label_names)

@strawberry.field
def issue_by_number(self, number: int) -> IssueNode | None:
def issue_by_number(self, info: Info, number: int) -> IssueNode | None:
"""Return a single issue by its GitHub number within this module's linked issues."""
info.context.current_module = self

return (
self.issues.select_related("repository", "author")
.prefetch_related("assignees", "labels")
Expand All @@ -133,8 +157,13 @@ def interested_users(self, issue_number: int) -> list[UserNode]:
return [i.user for i in interests]

@strawberry.field
def task_deadline(self, issue_number: int) -> datetime | None:
def task_deadline(self, info: Info, issue_number: int) -> datetime | None:
"""Return the deadline for the latest assigned task linked to this module and issue."""
mapping = getattr(info.context, "task_deadlines_by_issue", None)
if mapping:
return mapping.get(issue_number)

# fallback (single issue query)
return (
Task.objects.filter(
module=self,
Expand All @@ -147,8 +176,12 @@ def task_deadline(self, issue_number: int) -> datetime | None:
)

@strawberry.field
def task_assigned_at(self, issue_number: int) -> datetime | None:
"""Return the latest assignment time for tasks linked to this module and issue number."""
def task_assigned_at(self, info: Info, issue_number: int) -> datetime | None:
"""Return the latest assignment time for tasks linked to this module and issue."""
mapping = getattr(info.context, "task_assigned_at_by_issue", None)
if mapping:
return mapping.get(issue_number)

return (
Task.objects.filter(
module=self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_issue_node_fields(self):
"pull_requests",
"repository_name",
"state",
"task_deadline",
"title",
"url",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ const ModuleIssueDetailsPage = () => {
const formatDeadline = (deadline: string | null) => {
if (!deadline) return { text: 'No deadline set', color: 'text-gray-600 dark:text-gray-300' }

const now = new Date()

const todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())

const deadlineDate = new Date(deadline)
const today = new Date()

const deadlineUTC = new Date(
const deadlineUTC = Date.UTC(
deadlineDate.getUTCFullYear(),
deadlineDate.getUTCMonth(),
deadlineDate.getUTCDate()
)
const todayUTC = new Date(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate())

const isOverdue = deadlineUTC < todayUTC
const daysLeft = Math.ceil((deadlineUTC.getTime() - todayUTC.getTime()) / (1000 * 60 * 60 * 24))
const daysLeft = Math.ceil((deadlineUTC - todayUTC) / (1000 * 60 * 60 * 24))
const isOverdue = daysLeft < 0

let statusText: string
if (isOverdue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,58 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { GetModuleIssuesDocument } from 'types/__generated__/moduleQueries.generated'
import IssuesTable, { type IssueRow } from 'components/IssuesTable'
import IssuesTable from 'components/IssuesTable'
import LoadingSpinner from 'components/LoadingSpinner'
import Pagination from 'components/Pagination'

const ITEMS_PER_PAGE = 20
const LABEL_ALL = 'all'
const DEADLINE_ALL = 'all'
const DEADLINE_OPTIONS = [
{ key: 'all', label: 'All' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'due-soon', label: 'Due Soon' },
{ key: 'upcoming', label: 'Upcoming' },
{ key: 'no-deadline', label: 'No Deadline' },
]

const getDeadlineCategory = (deadline?: string | null): string => {
if (!deadline) return 'no-deadline'

const now = new Date()
const deadlineDate = new Date(deadline)
const nowStartUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
const deadlineStartUtc = Date.UTC(
deadlineDate.getUTCFullYear(),
deadlineDate.getUTCMonth(),
deadlineDate.getUTCDate()
)
const diffDays = Math.floor((deadlineStartUtc - nowStartUtc) / 86400000)

if (diffDays < 0) return 'overdue'
if (diffDays <= 7) return 'due-soon'
return 'upcoming'
}

const IssuesPage = () => {
const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>()
const router = useRouter()
const searchParams = useSearchParams()
const [selectedLabel, setSelectedLabel] = useState<string>(searchParams.get('label') || LABEL_ALL)
const [selectedDeadline, setSelectedDeadline] = useState<string>(
searchParams.get('deadline') || DEADLINE_ALL
)
const [currentPage, setCurrentPage] = useState(1)

const isDeadlineFilterActive = selectedDeadline !== DEADLINE_ALL
const MAX_ISSUES_FOR_DEADLINE_FILTER = 1000

const { data, loading, error } = useQuery(GetModuleIssuesDocument, {
variables: {
programKey,
moduleKey,
limit: ITEMS_PER_PAGE,
offset: (currentPage - 1) * ITEMS_PER_PAGE,
limit: isDeadlineFilterActive ? MAX_ISSUES_FOR_DEADLINE_FILTER : ITEMS_PER_PAGE,
offset: isDeadlineFilterActive ? 0 : (currentPage - 1) * ITEMS_PER_PAGE,
label: selectedLabel === LABEL_ALL ? null : selectedLabel,
Comment thread
anurag2787 marked this conversation as resolved.
},
skip: !programKey || !moduleKey,
Expand All @@ -38,19 +70,35 @@ const IssuesPage = () => {

const moduleData = data?.getModule

const moduleIssues: IssueRow[] = useMemo(() => {
return (moduleData?.issues || []).map((i) => ({
const { moduleIssues, filteredCount } = useMemo(() => {
const allIssues = (moduleData?.issues || []).map((i) => ({
objectID: i.id,
number: i.number,
title: i.title,
state: i.state,
isMerged: i.isMerged,
labels: i.labels || [],
assignees: i.assignees || [],
deadline: i.taskDeadline ?? null,
}))
}, [moduleData])

const totalPages = Math.ceil((moduleData?.issuesCount || 0) / ITEMS_PER_PAGE)
if (selectedDeadline !== DEADLINE_ALL) {
// Filter by deadline category
const filtered = allIssues.filter(
(issue) => getDeadlineCategory(issue.deadline) === selectedDeadline
)
// Apply client-side pagination on filtered results
const start = (currentPage - 1) * ITEMS_PER_PAGE
const paginatedIssues = filtered.slice(start, start + ITEMS_PER_PAGE)
return { moduleIssues: paginatedIssues, filteredCount: filtered.length }
}

return { moduleIssues: allIssues, filteredCount: moduleData?.issuesCount || 0 }
}, [moduleData, selectedDeadline, currentPage])

const totalPages = Math.ceil(
(isDeadlineFilterActive ? filteredCount : moduleData?.issuesCount || 0) / ITEMS_PER_PAGE
)

const allLabels: string[] = useMemo(() => {
const serverLabels = moduleData?.availableLabels
Expand All @@ -77,6 +125,18 @@ const IssuesPage = () => {
router.replace(`?${params.toString()}`)
}

const handleDeadlineChange = (deadline: string) => {
setSelectedDeadline(deadline)
setCurrentPage(1)
const params = new URLSearchParams(searchParams.toString())
if (deadline === DEADLINE_ALL) {
params.delete('deadline')
} else {
params.set('deadline', deadline)
}
router.replace(`?${params.toString()}`)
}

const handlePageChange = (page: number) => {
setCurrentPage(page)
}
Expand All @@ -97,43 +157,76 @@ const IssuesPage = () => {
return (
<div className="mt-16 min-h-screen bg-white p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
<div className="mb-6 flex items-center justify-between">
<div className="mb-6 flex items-center justify-between gap-4">
<h1 className="text-3xl font-bold">{moduleData.name} Issues</h1>
<div className="inline-flex h-12 items-center rounded-lg bg-gray-200 dark:bg-[#323232]">
<Select
labelPlacement="outside-left"
size="md"
aria-label="Filter by label"
label="Label :"
classNames={{
label:
'font-small text-sm text-gray-600 hover:cursor-pointer dark:text-gray-300 pl-[1.4rem] w-auto',
trigger: 'bg-gray-200 dark:bg-[#323232] pl-0 text-nowrap w-40',
popoverContent: 'text-md min-w-40 dark:bg-[#323232] rounded-none p-0',
}}
selectedKeys={new Set([selectedLabel])}
onSelectionChange={(keys) => {
const [key] = Array.from(keys as Set<string>)
if (key) handleLabelChange(key)
}}
>
{[LABEL_ALL, ...allLabels].map((l) => (
<SelectItem
key={l}
classNames={{
base: 'text-sm hover:bg-[#D1DBE6] dark:hover:bg-[#454545] rounded-none px-3 py-0.5',
}}
>
{l === LABEL_ALL ? 'All' : l}
</SelectItem>
))}
</Select>
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center sm:gap-3">
<div className="inline-flex h-12 items-center rounded-lg bg-gray-200 dark:bg-[#323232]">
<Select
labelPlacement="outside-left"
size="md"
aria-label="Filter by label"
label="Label :"
classNames={{
label:
'font-small text-sm text-gray-600 hover:cursor-pointer dark:text-gray-300 pl-[1.4rem] w-auto',
trigger: 'bg-gray-200 dark:bg-[#323232] pl-0 text-nowrap w-40',
popoverContent: 'text-md min-w-40 dark:bg-[#323232] rounded-none p-0',
}}
selectedKeys={new Set([selectedLabel])}
onSelectionChange={(keys) => {
const [key] = Array.from(keys as Set<string>)
if (key) handleLabelChange(key)
}}
>
{[LABEL_ALL, ...allLabels].map((l) => (
<SelectItem
key={l}
classNames={{
base: 'text-sm hover:bg-[#D1DBE6] dark:hover:bg-[#454545] rounded-none px-3 py-0.5',
}}
>
{l === LABEL_ALL ? 'All' : l}
</SelectItem>
))}
</Select>
</div>
<div className="inline-flex h-12 items-center rounded-lg bg-gray-200 dark:bg-[#323232]">
<Select
labelPlacement="outside-left"
size="md"
aria-label="Filter by deadline"
label="Deadline :"
classNames={{
label:
'font-small text-sm text-gray-600 hover:cursor-pointer dark:text-gray-300 pl-[1.4rem] w-auto',
trigger: 'bg-gray-200 dark:bg-[#323232] pl-0 text-nowrap w-36',
popoverContent: 'text-md min-w-36 dark:bg-[#323232] rounded-none p-0',
}}
selectedKeys={new Set([selectedDeadline])}
onSelectionChange={(keys) => {
const [key] = Array.from(keys as Set<string>)
if (key) handleDeadlineChange(key)
}}
>
{DEADLINE_OPTIONS.map((option) => (
<SelectItem
key={option.key}
classNames={{
base: 'text-sm hover:bg-[#D1DBE6] dark:hover:bg-[#454545] rounded-none px-3 py-0.5',
}}
>
{option.label}
</SelectItem>
))}
</Select>
</div>
</div>
</div>

<IssuesTable
issues={moduleIssues}
showAssignee={true}
showDeadline={true}
onIssueClick={handleIssueClick}
emptyMessage="No issues found for the selected filter."
/>
Expand Down
Loading