Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
9 changes: 7 additions & 2 deletions backend/apps/github/graphql/nodes/release.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""GitHub release GraphQL node."""

from graphene import Field
import graphene

from apps.common.graphql.nodes import BaseNode
from apps.github.graphql.nodes.user import UserNode
Expand All @@ -10,7 +10,8 @@
class ReleaseNode(BaseNode):
"""GitHub release node."""

author = Field(UserNode)
author = graphene.Field(UserNode)
project_name = graphene.String()

class Meta:
model = Release
Expand All @@ -21,3 +22,7 @@ class Meta:
"published_at",
"tag_name",
)

def resolve_project_name(self, info):
"""Return project name."""
return self.idx_project
3 changes: 3 additions & 0 deletions backend/apps/owasp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class SnapshotAdmin(admin.ModelAdmin):
"new_users",
)
list_display = (
"title",
"start_at",
"end_at",
"status",
Expand All @@ -133,6 +134,8 @@ class SnapshotAdmin(admin.ModelAdmin):
)
ordering = ("-start_at",)
search_fields = (
"title",
"key",
"status",
"error_message",
)
Expand Down
74 changes: 74 additions & 0 deletions backend/apps/owasp/graphql/nodes/snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""OWASP snapshot GraphQL node."""

import graphene

from apps.github.graphql.nodes.issue import IssueNode
from apps.github.graphql.nodes.release import ReleaseNode
from apps.github.graphql.nodes.user import UserNode
from apps.owasp.graphql.nodes.chapter import ChapterNode
from apps.owasp.graphql.nodes.common import GenericEntityNode
from apps.owasp.graphql.nodes.project import ProjectNode
from apps.owasp.models.snapshot import Snapshot

RECENT_ISSUES_LIMIT = 100


class SnapshotNode(GenericEntityNode):
"""Snapshot node."""

key = graphene.String()
status = graphene.String()
error_message = graphene.String()
new_chapters = graphene.List(ChapterNode)
new_issues = graphene.List(IssueNode)
new_projects = graphene.List(ProjectNode)
new_releases = graphene.List(ReleaseNode)
new_users = graphene.List(UserNode)
updated_at = graphene.DateTime()

class Meta:
model = Snapshot
fields = (
"title",
"created_at",
"start_at",
"status",
"end_at",
"error_message",
)

def resolve_key(self, info):
"""Resolve key."""
return self.key

def resolve_status(self, info):
"""Resolve status."""
return self.status

def resolve_error_message(self, info):
"""Resolve error message."""
return self.error_message

def resolve_new_chapters(self, info):
"""Resolve new chapters."""
return self.new_chapters.all()

def resolve_new_issues(self, info):
"""Resolve recent new issues."""
return self.new_issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT]

def resolve_new_projects(self, info):
"""Resolve recent new projects."""
return self.new_projects.order_by("-created_at")

def resolve_new_releases(self, info):
"""Resolve recent new releases."""
return self.new_releases.order_by("-published_at")

def resolve_new_users(self, info):
"""Resolve recent new users."""
return self.new_users.order_by("-created_at")

def resolve_updated_at(self, info):
"""Resolve updated at."""
return self.updated_at
3 changes: 2 additions & 1 deletion backend/apps/owasp/graphql/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from .committee import CommitteeQuery
from .event import EventQuery
from .project import ProjectQuery
from .snapshot import SnapshotQuery
from .stats import StatsQuery


class OwaspQuery(ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, StatsQuery):
class OwaspQuery(ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, SnapshotQuery, StatsQuery):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Class declaration exceeds line length limit.

The OwaspQuery class declaration now exceeds the maximum line length of 99 characters (currently 100 characters), as flagged by the static analyzer.

Consider reformatting the inheritance list for readability:

-class OwaspQuery(ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, SnapshotQuery, StatsQuery):
+class OwaspQuery(
+    ChapterQuery,
+    CommitteeQuery,
+    EventQuery,
+    ProjectQuery,
+    SnapshotQuery,
+    StatsQuery,
+):
    """OWASP queries."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class OwaspQuery(ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, SnapshotQuery, StatsQuery):
class OwaspQuery(
ChapterQuery,
CommitteeQuery,
EventQuery,
ProjectQuery,
SnapshotQuery,
StatsQuery,
):
"""OWASP queries."""
🧰 Tools
🪛 Ruff (0.8.2)

11-11: Line too long (100 > 99)

(E501)

"""OWASP queries."""
32 changes: 32 additions & 0 deletions backend/apps/owasp/graphql/queries/snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""OWASP snapshot GraphQL queries."""

import graphene

from apps.common.graphql.queries import BaseQuery
from apps.owasp.graphql.nodes.snapshot import SnapshotNode
from apps.owasp.models.snapshot import Snapshot


class SnapshotQuery(BaseQuery):
"""Snapshot queries."""

snapshot = graphene.Field(
SnapshotNode,
key=graphene.String(required=True),
)

recent_snapshots = graphene.List(
SnapshotNode,
limit=graphene.Int(default_value=8),
)

def resolve_snapshot(root, info, key):
"""Resolve snapshot by key."""
try:
return Snapshot.objects.get(key=key)
except Snapshot.DoesNotExist:
return None

def resolve_recent_snapshots(root, info, limit):
"""Resolve recent snapshots."""
return Snapshot.objects.order_by("-created_at")[:limit]
10 changes: 6 additions & 4 deletions backend/apps/owasp/migrations/0015_snapshot.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Generated by Django 5.1.6 on 2025-02-22 18:37
# Generated by Django 5.1.6 on 2025-02-27 05:18

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("github", "0015_alter_release_author"),
("github", "0016_user_is_bot"),
("owasp", "0014_project_custom_tags"),
]

Expand All @@ -19,6 +19,10 @@ class Migration(migrations.Migration):
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("title", models.CharField(default="", max_length=255)),
("key", models.CharField(max_length=7, unique=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("start_at", models.DateTimeField()),
("end_at", models.DateTimeField()),
(
Expand All @@ -34,8 +38,6 @@ class Migration(migrations.Migration):
max_length=10,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("error_message", models.TextField(blank=True, default="")),
(
"new_chapters",
Expand Down
10 changes: 10 additions & 0 deletions backend/apps/owasp/models/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""OWASP app snapshot models."""

from django.db import models
from django.utils.timezone import now


class Snapshot(models.Model):
Expand All @@ -16,6 +17,9 @@ class Status(models.TextChoices):
COMPLETED = "completed", "Completed"
ERROR = "error", "Error"

title = models.CharField(max_length=255, default="")
key = models.CharField(max_length=7, unique=True) # Format: YYYY-mm

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand All @@ -31,6 +35,12 @@ class Status(models.TextChoices):
new_releases = models.ManyToManyField("github.Release", related_name="snapshots", blank=True)
new_users = models.ManyToManyField("github.User", related_name="snapshots", blank=True)

def save(self, *args, **kwargs):
"""Automatically set the key in YYYY-mm format before saving."""
if not self.key:
self.key = now().strftime("%Y-%m")
super().save(*args, **kwargs)

def __str__(self):
"""Return a string representation of the snapshot."""
return f"Snapshot {self.start_at} to {self.end_at} ({self.status})"
7 changes: 7 additions & 0 deletions backend/tests/owasp/models/snapshot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def setUp(self):
"""Set up a mocked snapshot object."""
self.snapshot = MagicMock(spec=Snapshot) # Mock entire model
self.snapshot.id = 1 # Set an ID to avoid ManyToMany errors
self.snapshot.title = "Mock Snapshot Title"
self.snapshot.key = "2025-02"
self.snapshot.start_at = "2025-02-21"
self.snapshot.end_at = "2025-02-21"
self.snapshot.status = Snapshot.Status.PROCESSING
Expand All @@ -27,3 +29,8 @@ def test_mocked_many_to_many_relations(self):
"""Test ManyToMany relationships using mocks."""
self.snapshot.new_chapters.set(["Mock Chapter"])
self.snapshot.new_chapters.set.assert_called_once_with(["Mock Chapter"])

def test_snapshot_attributes(self):
"""Test that title and key are correctly assigned."""
assert self.snapshot.title == "Mock Snapshot Title"
assert self.snapshot.key == "2025-02"
16 changes: 9 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
Home,
ProjectsPage,
CommitteesPage,
ChapterDetailsPage,
ChaptersPage,
CommitteeDetailsPage,
CommitteesPage,
ContributePage,
Home,
ProjectDetailsPage,
CommitteeDetailsPage,
ChapterDetailsPage,
UsersPage,
UserDetailsPage,
ProjectsPage,
RepositoryDetailsPage,
SnapshotDetailsPage,
UserDetailsPage,
UsersPage,
} from 'pages'
import { useEffect } from 'react'
import { Routes, Route, useLocation } from 'react-router-dom'
Expand Down Expand Up @@ -43,6 +44,7 @@ function App() {
<Route path="/committees/:committeeKey" element={<CommitteeDetailsPage />}></Route>
<Route path="/chapters" element={<ChaptersPage />}></Route>
<Route path="/chapters/:chapterKey" element={<ChapterDetailsPage />}></Route>
<Route path="/community/snapshot/:id" element={<SnapshotDetailsPage />}></Route>
<Route path="/community/users" element={<UsersPage />}></Route>
<Route path="/community/users/:userKey" element={<UserDetailsPage />}></Route>
<Route path="*" element={<ErrorDisplay {...ERROR_CONFIGS['404']} />} />
Expand Down
61 changes: 61 additions & 0 deletions frontend/src/api/queries/snapshotQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { gql } from '@apollo/client'

export const GET_SNAPSHOT_DETAILS = gql`
query GetSnapshotDetails($key: String!) {
snapshot(key: $key) {
title
key
updatedAt
createdAt
startAt
endAt
status
errorMessage
newReleases {
name
publishedAt
tagName
projectName
}
newProjects {
key
name
summary
starsCount
forksCount
contributorsCount
level
isActive
repositoriesCount
topContributors {
avatarUrl
contributionsCount
login
name
}
}
newChapters {
key
name
createdAt
suggestedLocation
region
summary
topContributors {
avatarUrl
contributionsCount
login
name
}
updatedAt
url
relatedUrls
geoLocation {
lat
lng
}
isActive
}
}
}
`
Loading