Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions backend/apps/owasp/api/internal/nodes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
class GenericEntityNode:
"""Base node class for OWASP entities with common fields and resolvers."""

@strawberry.field
def leaders_logins(self) -> list[str]:
"""Resolve leaders logins."""
return [leader.login for leader in self.leaders.all()]
Copy link
Collaborator

@kasya kasya Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rudransh-shrivastava were you able to see any Leaders data on the frontend? Checking this, and I don't think this resolver returns anything - it's just an empty list all the time. And that's because there's no self.leaders.all()

Copy link
Collaborator

@kasya kasya Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, feel free to use http://localhost:8000/graphql/ to see data that is returned by graphQL request while working on updating queries - this is super helpful!
Screenshot 2025-07-25 at 6 42 18 PM

Copy link
Collaborator Author

@rudransh-shrivastava rudransh-shrivastava Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @kasya , you're right that there are no leaders.
There are two fields in the OWASP database model

  1. leaders_raw
  2. leaders

since leaders maps to github.User, I was using this field to access leader's logins/usernames.
The leaders_raw field is a list of strings like ["Rudransh Shrivastava"].

leaders for projects or chapters do not have any data in the database -- but for leaders_raw there is.

If i take a random project for example:
This is the leaders_raw field.
image

This is the leaders field - which is empty.
image

I'm not sure why leaders is empty. Initially I assumed it was an issue locally. I was able to set leaders manually and test out things.


@strawberry.field
def leaders(self) -> list[str]:
"""Resolve leaders."""
Expand Down
4 changes: 4 additions & 0 deletions frontend/__tests__/e2e/pages/ChapterDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ test.describe('Chapter Details Page', () => {
await page.getByRole('button', { name: 'Show less' }).click()
await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible()
})

test('should have a leaders list block', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible()
})
})
4 changes: 4 additions & 0 deletions frontend/__tests__/e2e/pages/ProjectDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@ test.describe('Project Details Page', () => {
await expect(page.getByText('Forks Trend')).toBeVisible()
await expect(page.getByText('Days Since Last Commit and Release')).toBeVisible()
})

test('should have a leaders list block', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible()
})
})
113 changes: 113 additions & 0 deletions frontend/__tests__/unit/components/LeadersListBlock.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { MockedProvider } from '@apollo/client/testing'
import { render, screen, fireEvent } from '@testing-library/react'
import { useRouter } from 'next/navigation'
import { GET_LEADER_DATA } from 'server/queries/userQueries'
import LeadersListBlock from 'components/LeadersListBlock'

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}))

const mockLeaders = {
leader1: 'Role 1',
leader2: 'Role 2',
}

const mocks = [
{
request: {
query: GET_LEADER_DATA,
variables: { key: 'leader1' },
},
result: {
data: {
user: {
login: 'leader1',
avatarUrl: 'https://avatars.githubusercontent.com/u/1',
company: 'Company 1',
location: 'Location 1',
name: 'Leader One',
},
},
},
},
{
request: {
query: GET_LEADER_DATA,
variables: { key: 'leader2' },
},
result: {
data: {
user: {
login: 'leader2',
avatarUrl: 'https://avatars.githubusercontent.com/u/2',
company: 'Company 2',
location: 'Location 2',
name: 'Leader Two',
},
},
},
},
]

describe('LeadersListBlock', () => {
const push = jest.fn()
beforeEach(() => {
;(useRouter as jest.Mock).mockReturnValue({ push })
})

test('renders loading state initially', () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<LeadersListBlock leaders={mockLeaders} />
</MockedProvider>
)

expect(screen.getByText('Loading leader1...')).toBeInTheDocument()
expect(screen.getByText('Loading leader2...')).toBeInTheDocument()
})

test('renders user cards on successful data fetch', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<LeadersListBlock leaders={mockLeaders} />
</MockedProvider>
)

expect(await screen.findByText('Leader One')).toBeInTheDocument()
expect(await screen.findByText('Leader Two')).toBeInTheDocument()
})

test('navigates to the correct user profile on button click', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<LeadersListBlock leaders={mockLeaders} />
</MockedProvider>
)

const viewProfileButton = await screen.findAllByText('View Profile')
fireEvent.click(viewProfileButton[0])

expect(push).toHaveBeenCalledWith('/members/leader1')
})

test('renders an error message if the query fails', async () => {
const errorMocks = [
{
request: {
query: GET_LEADER_DATA,
variables: { key: 'leader1' },
},
error: new Error('An error occurred'),
},
]

render(
<MockedProvider mocks={errorMocks} addTypename={false}>
<LeadersListBlock leaders={{ leader1: 'Role 1' }} />
</MockedProvider>
)

expect(await screen.findByText("Error loading leader1's data")).toBeInTheDocument()
})
})
1 change: 1 addition & 0 deletions frontend/__tests__/unit/data/mockChapterDetailsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const mockChapterDetailsData = {
},
establishedYear: 2020,
key: 'test-chapter',
leadersLogins: ['leader1', 'leader2'],
},
topContributors: Array.from({ length: 15 }, (_, i) => ({
avatarUrl: `https://avatars.githubusercontent.com/avatar${i + 1}.jpg`,
Expand Down
1 change: 1 addition & 0 deletions frontend/__tests__/unit/data/mockProjectDetailsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const mockProjectDetailsData = {
key: 'example-project',
languages: ['Python', 'GraphQL', 'JavaScript'],
leaders: ['alice', 'bob'],
leadersLogins: ['alice', 'bob'],
level: 'Lab',
name: 'Test Project',
recentIssues: [
Expand Down
54 changes: 4 additions & 50 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Tooltip } from '@heroui/tooltip'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { GET_PROJECT_METADATA, GET_TOP_CONTRIBUTORS } from 'server/queries/projectQueries'
import { GET_LEADER_DATA } from 'server/queries/userQueries'
import type { Contributor } from 'types/contributor'
import type { Project } from 'types/project'
import type { User } from 'types/user'
import { aboutText, technologies } from 'utils/aboutData'
import { capitalize } from 'utils/capitalize'
import AnchorTitle from 'components/AnchorTitle'
import AnimatedCounter from 'components/AnimatedCounter'
import LeadersListBlock from 'components/LeadersListBlock'
import LoadingSpinner from 'components/LoadingSpinner'
import Markdown from 'components/MarkdownWrapper'
import SecondaryCard from 'components/SecondaryCard'
import TopContributorsList from 'components/TopContributorsList'
import UserCard from 'components/UserCard'

const leaders = {
arkid15r: 'CCSP, CISSP, CSSLP',
Expand Down Expand Up @@ -112,15 +108,9 @@ const About = () => {
))}
</SecondaryCard>

<SecondaryCard icon={faArrowUpRightFromSquare} title={<AnchorTitle title="Leaders" />}>
<div className="flex w-full flex-col items-center justify-around overflow-hidden md:flex-row">
{Object.keys(leaders).map((username) => (
<div key={username}>
<LeaderData username={username} />
</div>
))}
</div>
</SecondaryCard>
{leaders && (
<LeadersListBlock leaders={leaders} icon={faArrowUpRightFromSquare} label="Leaders" />
)}

{topContributors && (
<TopContributorsList
Expand Down Expand Up @@ -241,40 +231,4 @@ const About = () => {
)
}

const LeaderData = ({ username }: { username: string }) => {
const { data, loading, error } = useQuery(GET_LEADER_DATA, {
variables: { key: username },
})
const router = useRouter()

if (loading) return <p>Loading {username}...</p>
if (error) return <p>Error loading {username}'s data</p>

const user = data?.user

if (!user) {
return <p>No data available for {username}</p>
}

const handleButtonClick = (user: User) => {
router.push(`/members/${user.login}`)
}

return (
<UserCard
avatar={user.avatarUrl}
button={{
icon: <FontAwesomeIconWrapper icon="fa-solid fa-right-to-bracket" />,
label: 'View Profile',
onclick: () => handleButtonClick(user),
}}
className="h-64 w-40 bg-inherit"
company={user.company}
description={leaders[user.login]}
location={user.location}
name={user.name || username}
/>
)
}

export default About
1 change: 1 addition & 0 deletions frontend/src/app/chapters/[chapterKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default function ChapterDetailsPage() {
socialLinks={chapter.relatedUrls}
summary={chapter.summary}
title={chapter.name}
leadersLogins={chapter.leadersLogins}
topContributors={topContributors}
type="chapter"
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/projects/[projectKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const ProjectDetailsPage = () => {
stats={projectStats}
summary={project.summary}
title={project.name}
leadersLogins={project.leadersLogins}
topContributors={topContributors}
topics={project.topics}
type="project"
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
faTags,
faUsers,
faRectangleList,
faArrowUpRightFromSquare,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import type { DetailsCardProps } from 'types/card'
import { LeadersListBlockProps } from 'types/leaders'
import { capitalize } from 'utils/capitalize'
import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials'
import { getSocialIcon } from 'utils/urlIconMappings'
Expand All @@ -19,6 +21,7 @@ import ChapterMapWrapper from 'components/ChapterMapWrapper'
import HealthMetrics from 'components/HealthMetrics'
import InfoBlock from 'components/InfoBlock'
import LeadersList from 'components/LeadersList'
import LeadersListBlock from 'components/LeadersListBlock'
import MetricsScoreCircle from 'components/MetricsScoreCircle'
import Milestones from 'components/Milestones'
import RecentIssues from 'components/RecentIssues'
Expand Down Expand Up @@ -50,6 +53,7 @@ const DetailsCard = ({
stats,
summary,
title,
leadersLogins,
topContributors,
topics,
type,
Expand Down Expand Up @@ -171,6 +175,16 @@ const DetailsCard = ({
)}
</div>
)}
{leadersLogins && leadersLogins.length > 0 && (
<LeadersListBlock
icon={faArrowUpRightFromSquare}
label="Leaders"
leaders={leadersLogins.reduce((acc, login) => {
acc[login] = ''
return acc
}, {} as LeadersListBlockProps)}
/>
)}
{topContributors && (
<TopContributorsList
contributors={topContributors}
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/LeadersListBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client'
import { useQuery } from '@apollo/client'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import { useRouter } from 'next/navigation'
import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import { GET_LEADER_DATA } from 'server/queries/userQueries'
import { LeadersListBlockProps } from 'types/leaders'
import { User } from 'types/user'
import AnchorTitle from 'components/AnchorTitle'
import SecondaryCard from 'components/SecondaryCard'
import UserCard from 'components/UserCard'

const LeadersListBlock = ({
leaders,
label = 'Leaders',
icon,
}: {
leaders: LeadersListBlockProps
label?: string
icon?: IconProp
}) => {
return (
<SecondaryCard icon={icon} title={<AnchorTitle title={label} />}>
<div className="flex w-full flex-col items-center justify-around overflow-hidden md:flex-row">
{Object.keys(leaders).map((username) => (
<div key={username}>
<LeaderData username={username} leaders={leaders} />
</div>
))}
</div>
</SecondaryCard>
)
}

const LeaderData = ({
username,
leaders,
}: {
username: string
leaders: LeadersListBlockProps
}) => {
const { data, loading, error } = useQuery(GET_LEADER_DATA, {
variables: { key: username },
})
const router = useRouter()

if (loading) return <p>Loading {username}...</p>
if (error) return <p>Error loading {username}'s data</p>

const user = data?.user

if (!user) {
return <p>No data available for {username}</p>
}

const handleButtonClick = (user: User) => {
router.push(`/members/${user.login}`)
}

return (
<UserCard
avatar={user.avatarUrl}
button={{
icon: <FontAwesomeIconWrapper icon="fa-solid fa-right-to-bracket" />,
label: 'View Profile',
onclick: () => handleButtonClick(user),
}}
className="w-42 h-64 bg-inherit"
company={user.company}
description={leaders[user.login]}
location={user.location}
name={user.name || username}
/>
)
}

export default LeadersListBlock
1 change: 1 addition & 0 deletions frontend/src/server/queries/chapterQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const GET_CHAPTER_DATA = gql`
summary
updatedAt
url
leadersLogins
}
topContributors(chapter: $key) {
avatarUrl
Expand Down
Loading