Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
231f5c2
added model, command , algolia , sponsors
abhayymishraa Jan 27, 2025
6fddf74
Enhanced command and removed extra indexed from data
abhayymishraa Jan 28, 2025
70a7dd2
Merge branch 'main' into feat/sponsors
abhayymishraa Jan 28, 2025
2262c9d
Added testcases
abhayymishraa Jan 28, 2025
1260440
verified commit
abhayymishraaa Jan 31, 2025
ad7584e
Merge branch 'main' into feat/sponsors
abhayymishraaa Jan 31, 2025
8d3b4f2
verified commit with chnages
abhayymishraa Feb 2, 2025
2df44d4
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 3, 2025
fa9459a
fixed importing
abhayymishraa Feb 3, 2025
f2dabbe
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 3, 2025
d6fe486
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 3, 2025
76e9010
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 4, 2025
5375a5b
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 4, 2025
dde2f69
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 13, 2025
9d0c001
resolved conflict
abhayymishraa Feb 13, 2025
16a758b
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 14, 2025
fc41e3f
fixed testcase
abhayymishraa Feb 14, 2025
2de5fe4
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 20, 2025
c004ae2
fixed makefile
abhayymishraa Feb 20, 2025
965a125
Merge branch 'main' into feat/sponsors
abhayymishraa Feb 22, 2025
874aedc
pre-commit after resolve conflict
abhayymishraa Feb 22, 2025
354149f
migrations
abhayymishraa Feb 22, 2025
8d29ff2
Merge branch 'main' into feat/sponsors
abhayymishraa Mar 3, 2025
3346deb
pre-commmit
abhayymishraa Mar 3, 2025
55103aa
Merge branch 'main' into feat/sponsors
abhayymishraa Mar 3, 2025
689d641
fixed sonaQube warning
abhayymishraa Mar 4, 2025
ad4b842
refractor code removed algolia
abhayymishraa Mar 4, 2025
2afa6e3
updated testcase
abhayymishraa Mar 4, 2025
ef34968
Merge branch 'main' into feat/sponsors
abhayymishraa Mar 4, 2025
818aa74
added moving Logo compoennt
abhayymishraa Mar 4, 2025
7db1f02
backned test fix
abhayymishraa Mar 4, 2025
cfc555d
frontend test case fix
abhayymishraa Mar 4, 2025
2c3eec3
pre-commit
abhayymishraa Mar 4, 2025
930c075
Merge branch 'main' into feat/sponsors
abhayymishraa Mar 4, 2025
5d06b86
fix e2e case
abhayymishraa Mar 4, 2025
b65d610
fix bug
abhayymishraa Mar 4, 2025
fdda190
improvements
abhayymishraa Mar 4, 2025
35fa0bf
Merge branch 'main' into pr/abhayymishraa/630
arkid15r Mar 5, 2025
370e32b
Update code
arkid15r Mar 5, 2025
d9a8317
changes
abhayymishraa Mar 5, 2025
b45db63
increase testcase timing
abhayymishraa Mar 5, 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
7 changes: 6 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ owasp-update-events:
@echo "Getting OWASP events data"
@CMD="python manage.py owasp_update_events" $(MAKE) exec-backend-command

owasp-update-sponsors:
@echo "Getting OWASP sponsors data"
@CMD="python manage.py owasp_update_sponsors" $(MAKE) exec-backend-command

purge-data:
@CMD="python manage.py purge_data" $(MAKE) exec-backend-command

Expand Down Expand Up @@ -136,4 +140,5 @@ update-data: \
owasp-scrape-projects \
github-update-project-related-repositories \
owasp-aggregate-projects \
owasp-update-events
owasp-update-events \
owasp-update-sponsors
32 changes: 32 additions & 0 deletions backend/apps/owasp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements
from apps.owasp.models.snapshot import Snapshot
from apps.owasp.models.sponsors import Sponsor


class GenericEntityAdminMixin:
Expand Down Expand Up @@ -141,10 +142,41 @@ class SnapshotAdmin(admin.ModelAdmin):
)


class SponsorAdmin(admin.ModelAdmin):
"""Admin configuration for Sponsor model."""

list_display = (
"name",
"sort_name",
"sponsor_type",
"is_member",
"member_type",
)

search_fields = (
"name",
"sort_name",
"description",
)

list_filter = (
"sponsor_type",
"is_member",
"member_type",
)

fieldsets = (
("Basic Information", {"fields": ("name", "sort_name", "description")}),
("URLs and Images", {"fields": ("url", "job_url", "image_url")}),
("Status", {"fields": ("is_member", "member_type", "sponsor_type")}),
)


admin.site.register(Chapter, ChapterAdmin)
admin.site.register(Committee, CommitteeAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(ProjectHealthMetrics)
admin.site.register(ProjectHealthRequirements)
admin.site.register(Snapshot, SnapshotAdmin)
admin.site.register(Sponsor, SponsorAdmin)
1 change: 1 addition & 0 deletions backend/apps/owasp/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""OWASP app constants."""

OWASP_ORGANIZATION_NAME = "OWASP"
OWASP_ORGANIZATION_DATA_URL = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main"
15 changes: 15 additions & 0 deletions backend/apps/owasp/graphql/nodes/sponsors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""OWASP sponsors GraphQL node."""

from apps.common.graphql.nodes import BaseNode
from apps.owasp.models.sponsors import Sponsor


class SponsorNode(BaseNode):
"""Sponsor node."""

class Meta:
model = Sponsor
fields = (
"name",
"image_url",
)
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 @@ -5,10 +5,11 @@
from .event import EventQuery
from .project import ProjectQuery
from .snapshot import SnapshotQuery
from .sponsors import SponsorQuery
from .stats import StatsQuery


class OwaspQuery(
ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, SnapshotQuery, StatsQuery
ChapterQuery, CommitteeQuery, EventQuery, ProjectQuery, SnapshotQuery, SponsorQuery, StatsQuery
):
"""OWASP queries."""
17 changes: 17 additions & 0 deletions backend/apps/owasp/graphql/queries/sponsors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""OWASP sponsors GraphQL queries."""

import graphene

from apps.common.graphql.queries import BaseQuery
from apps.owasp.graphql.nodes.sponsors import SponsorNode
from apps.owasp.models.sponsors import Sponsor


class SponsorQuery(BaseQuery):
"""Sponsor queries."""

sponsors = graphene.List(SponsorNode)

def resolve_sponsors(root, info):
"""Resolve sponsors."""
return Sponsor.objects.all()
20 changes: 20 additions & 0 deletions backend/apps/owasp/management/commands/owasp_update_sponsors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""A command to add OWASP sponsors data."""

import yaml
from django.core.management.base import BaseCommand

from apps.github.utils import get_repository_file_content
from apps.owasp.models.sponsors import Sponsor


class Command(BaseCommand):
help = "Import sponsors from the provided YAML file"

def handle(self, *args, **kwargs):
data = yaml.safe_load(
get_repository_file_content(
"https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/corp_members.yml"
).expandtabs()
)

Sponsor.bulk_save([Sponsor.update_data(sponsor_data) for sponsor_data in data])
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement transaction management and improve feedback.

The current implementation doesn't provide feedback about how many sponsors were processed, and it lacks transaction management for database consistency.

Implement transaction management and add detailed feedback:

-    Sponsor.bulk_save([Sponsor.update_data(sponsor_data) for sponsor_data in data])
+    from django.db import transaction
+    
+    sponsors_data = [Sponsor.update_data(sponsor_data) for sponsor_data in data]
+    
+    with transaction.atomic():
+        created_count, updated_count = Sponsor.bulk_save(sponsors_data)
+        
+    self.stdout.write(
+        self.style.SUCCESS(
+            f"Successfully processed {len(sponsors_data)} sponsors: "
+            f"{created_count} created, {updated_count} updated"
+        )
+    )

Note: This assumes the bulk_save method returns a tuple of (created_count, updated_count). If it doesn't, you'll need to modify the Sponsor model's method to track and return these counts.

📝 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
Sponsor.bulk_save([Sponsor.update_data(sponsor_data) for sponsor_data in data])
from django.db import transaction
sponsors_data = [Sponsor.update_data(sponsor_data) for sponsor_data in data]
with transaction.atomic():
created_count, updated_count = Sponsor.bulk_save(sponsors_data)
self.stdout.write(
self.style.SUCCESS(
f"Successfully processed {len(sponsors_data)} sponsors: "
f"{created_count} created, {updated_count} updated"
)
)

85 changes: 85 additions & 0 deletions backend/apps/owasp/migrations/0022_sponsor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Generated by Django 5.1.5 on 2025-03-04 15:28

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0021_alter_snapshot_key"),
]

operations = [
migrations.CreateModel(
name="Sponsor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"description",
models.TextField(blank=True, verbose_name="Description"),
),
(
"key",
models.CharField(max_length=100, unique=True, verbose_name="Key"),
),
("name", models.CharField(max_length=255, verbose_name="Name")),
(
"sort_name",
models.CharField(max_length=255, verbose_name="Sort Name"),
),
("url", models.URLField(blank=True, verbose_name="Website URL")),
("job_url", models.URLField(blank=True, verbose_name="Job URL")),
(
"image_url",
models.CharField(blank=True, max_length=255, verbose_name="Image Path"),
),
(
"is_member",
models.BooleanField(default=False, verbose_name="Is Corporate Sponsor"),
),
(
"member_type",
models.CharField(
blank=True,
choices=[
("Platinum", "Platinum"),
("Gold", "Gold"),
("Silver", "Silver"),
],
default="Silver",
max_length=20,
verbose_name="Member Type",
),
),
(
"sponsor_type",
models.CharField(
choices=[
("Diamond", "Diamond"),
("Platinum", "Platinum"),
("Gold", "Gold"),
("Silver", "Silver"),
("Supporter", "Supporter"),
("Not a Sponsor", "Not Sponsor"),
],
default="Not a Sponsor",
max_length=20,
verbose_name="Sponsor Type",
),
),
],
options={
"verbose_name_plural": "Sponsors",
"db_table": "owasp_sponsors",
},
),
]
134 changes: 134 additions & 0 deletions backend/apps/owasp/models/sponsors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""OWASP app sponsor models."""

from django.db import models

from apps.common.models import BulkSaveModel, TimestampedModel
from apps.common.utils import slugify
from apps.github.utils import normalize_url
from apps.owasp.constants import OWASP_ORGANIZATION_DATA_URL


class Sponsor(BulkSaveModel, TimestampedModel):
"""Sponsor model."""

objects = models.Manager()

class Meta:
db_table = "owasp_sponsors"
verbose_name_plural = "Sponsors"

class SponsorType(models.TextChoices):
DIAMOND = "Diamond"
PLATINUM = "Platinum"
GOLD = "Gold"
SILVER = "Silver"
SUPPORTER = "Supporter"
NOT_SPONSOR = "Not a Sponsor"

class MemberType(models.TextChoices):
PLATINUM = "Platinum"
GOLD = "Gold"
SILVER = "Silver"

# Basic information
description = models.TextField(verbose_name="Description", blank=True)
key = models.CharField(verbose_name="Key", max_length=100, unique=True)
name = models.CharField(verbose_name="Name", max_length=255)
sort_name = models.CharField(verbose_name="Sort Name", max_length=255)

# URLs and images
url = models.URLField(verbose_name="Website URL", blank=True)
job_url = models.URLField(verbose_name="Job URL", blank=True)
image_url = models.CharField(verbose_name="Image Path", max_length=255, blank=True)

# Status fields
is_member = models.BooleanField(verbose_name="Is Corporate Sponsor", default=False)
member_type = models.CharField(
verbose_name="Member Type",
max_length=20,
choices=MemberType.choices,
default=MemberType.SILVER,
blank=True,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider aligning default behavior of is_member and member_type.

Currently, is_member defaults to False, but member_type defaults to MemberType.SILVER. These defaults seem inconsistent - if not a member, the member type shouldn't be set to SILVER.

is_member = models.BooleanField(verbose_name="Is Corporate Sponsor", default=False)
member_type = models.CharField(
    verbose_name="Member Type",
    max_length=20,
    choices=MemberType.choices,
-   default=MemberType.SILVER,
+   default="",
    blank=True,
)

Or better yet, make the member_type field conditional on is_member:

def save(self, *args, **kwargs):
    """Override save method to ensure data consistency."""
    if not self.is_member:
        self.member_type = ""
    super().save(*args, **kwargs)

sponsor_type = models.CharField(
verbose_name="Sponsor Type",
max_length=20,
choices=SponsorType.choices,
default=SponsorType.NOT_SPONSOR,
)

def __str__(self):
"""Sponsor human readable representation."""
return f"{self.name}"

@property
def readable_member_type(self):
"""Get human-readable member type."""
return self.MemberType(self.member_type).label

@property
def readable_sponsor_type(self):
"""Get human-readable sponsor type."""
return self.SponsorType(self.sponsor_type).label

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Properties for human-readable types might throw exceptions.

The readable_member_type and readable_sponsor_type properties don't handle the case where the field might be blank or have an invalid value. This could lead to exceptions at runtime.

Add error handling to prevent exceptions:

@property
def readable_member_type(self):
    """Get human-readable member type."""
-   return self.MemberType(self.member_type).label
+   try:
+       return self.MemberType(self.member_type).label if self.member_type else ""
+   except ValueError:
+       return ""

@property
def readable_sponsor_type(self):
    """Get human-readable sponsor type."""
-   return self.SponsorType(self.sponsor_type).label
+   try:
+       return self.SponsorType(self.sponsor_type).label
+   except ValueError:
+       return ""
📝 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
@property
def readable_member_type(self):
"""Get human-readable member type."""
return self.MemberType(self.member_type).label
@property
def readable_sponsor_type(self):
"""Get human-readable sponsor type."""
return self.SponsorType(self.sponsor_type).label
@property
def readable_member_type(self):
"""Get human-readable member type."""
try:
return self.MemberType(self.member_type).label if self.member_type else ""
except ValueError:
return ""
@property
def readable_sponsor_type(self):
"""Get human-readable sponsor type."""
try:
return self.SponsorType(self.sponsor_type).label
except ValueError:
return ""

@staticmethod
def bulk_save(sponsors, fields=None):
"""Bulk save sponsors."""
BulkSaveModel.bulk_save(Sponsor, sponsors, fields=fields)

@staticmethod
def update_data(data, save=True):
"""Update sponsor data."""
key = slugify(data["name"])
try:
sponsor = Sponsor.objects.get(key=key)
except Sponsor.DoesNotExist:
sponsor = Sponsor(key=key)

sponsor.from_dict(data)

if save:
sponsor.save()

return sponsor

def from_dict(self, data):
"""Update instance based on the dict data."""
image_path = data.get("image", "").lstrip("/")
image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Image URL construction should handle empty paths.

The current image URL construction doesn't handle the case where image_path is empty, which could result in an invalid URL.

- image_path = data.get("image", "").lstrip("/")
- image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}"
+ image_path = data.get("image", "").lstrip("/")
+ image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}" if image_path else ""
📝 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
image_path = data.get("image", "").lstrip("/")
image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}"
image_path = data.get("image", "").lstrip("/")
image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}" if image_path else ""

sponsor_key = str(data.get("sponsor", "-1"))
member_key = str(data.get("membertype", "4"))

member_type_mapping = {
"2": self.MemberType.PLATINUM,
"3": self.MemberType.GOLD,
"4": self.MemberType.SILVER,
}

sponsor_type_mapping = {
"1": self.SponsorType.DIAMOND,
"2": self.SponsorType.PLATINUM,
"3": self.SponsorType.GOLD,
"4": self.SponsorType.SILVER,
"5": self.SponsorType.SUPPORTER,
"-1": self.SponsorType.NOT_SPONSOR,
}

sponsor_type_label = sponsor_type_mapping.get(sponsor_key, self.SponsorType.NOT_SPONSOR)
member_type_label = member_type_mapping.get(member_key, self.MemberType.SILVER)

fields = {
"name": data.get("name", ""),
"sort_name": data.get("sortname", "").capitalize(),
"description": data.get("description", ""),
"url": normalize_url(data.get("url", "")) or "",
"job_url": normalize_url(data.get("job_url", "")) or "",
"image_url": image_url,
"is_member": bool(data.get("member", False)),
"sponsor_type": sponsor_type_label,
"member_type": member_type_label,
}

for key, value in fields.items():
setattr(self, key, value)
Loading