Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 additions & 0 deletions backend/apps/api/rest/v0/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from apps.api.rest.v0.structured_search import FieldConfig, apply_structured_search
from apps.owasp.models.enums.project import ProjectLevel, ProjectType
from apps.owasp.models.project import Project as ProjectModel
from apps.owasp.models.sponsor import Sponsor as SponsorModel

PROJECT_SEARCH_FIELDS: dict[str, FieldConfig] = {
"name": {
Expand Down Expand Up @@ -55,6 +56,7 @@ class ProjectDetail(ProjectBase):

description: str
leaders: list[Leader]
sponsors: list["ProjectSponsor"] = []

@staticmethod
def resolve_leaders(obj):
Expand All @@ -64,13 +66,39 @@ def resolve_leaders(obj):
for leader in obj.entity_leaders
]

@staticmethod
def resolve_sponsors(obj):
"""Resolve sponsors linked to this project."""
return [
ProjectSponsor(
key=sponsor.key,
name=sponsor.name,
sponsor_type=sponsor.sponsor_type,
image_url=sponsor.image_url,
url=sponsor.url,
description=sponsor.description,
)
for sponsor in obj.sponsors.filter(status=SponsorModel.Status.ACTIVE)
]


class ProjectError(Schema):
"""Project error schema."""

message: str


class ProjectSponsor(Schema):
"""Schema for project sponsors."""

image_url: str
key: str
name: str
sponsor_type: str
url: str
description: str = ""


class ProjectFilter(FilterSchema):
"""Filter for Project."""

Expand Down
70 changes: 70 additions & 0 deletions backend/apps/api/rest/v0/sponsor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from http import HTTPStatus
from typing import Literal

from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.http import HttpRequest
from ninja import Field, FilterSchema, Path, Query, Schema
from ninja.decorators import decorate_view
Expand All @@ -11,6 +13,7 @@

from apps.api.decorators.cache import cache_response
from apps.api.rest.v0.common import ValidationErrorSchema
from apps.common.utils import slugify
from apps.owasp.models.sponsor import Sponsor as SponsorModel

router = RouterPaginated(tags=["Sponsors"])
Expand Down Expand Up @@ -45,6 +48,23 @@ class SponsorError(Schema):
message: str


class SponsorApplicationRequest(Schema):
"""Schema for sponsor application request."""

name: str = Field(..., description="Organization name")
contact_email: str = Field(..., description="Contact email")
website: str | None = Field(None, description="Organization website (optional)")
sponsorship_interest: str = Field(..., description="Message about sponsorship interest")


class SponsorApplicationResponse(Schema):
"""Schema for sponsor application response."""

id: int
name: str
status: str


class SponsorFilter(FilterSchema):
"""Filter for Sponsor."""

Expand Down Expand Up @@ -105,3 +125,53 @@ def get_sponsor(
return sponsor

return Response({"message": "Sponsor not found"}, status=HTTPStatus.NOT_FOUND)


@router.post(
"/applications/",
description="Submit a sponsor application.",
operation_id="create_sponsor_application",
response={
HTTPStatus.BAD_REQUEST: ValidationErrorSchema,
HTTPStatus.CREATED: SponsorApplicationResponse,
},
Comment thread
anurag2787 marked this conversation as resolved.
summary="Create sponsor application",
)
def create_sponsor_application(
request: HttpRequest,
payload: SponsorApplicationRequest,
) -> Response:
"""Create a sponsor application."""
try:
key = slugify(payload.name)

if SponsorModel.objects.filter(key=key).exists():
return Response(
{"message": "A sponsor with this organization name already exists"},
status=HTTPStatus.BAD_REQUEST,
)

sponsor = SponsorModel(
name=payload.name,
key=key,
contact_email=payload.contact_email,
url=payload.website or "",
description=payload.sponsorship_interest,
status=SponsorModel.Status.DRAFT,
sort_name=payload.name,
)
sponsor.save()
Comment thread
anurag2787 marked this conversation as resolved.

return Response(
SponsorApplicationResponse(
id=sponsor.id,
name=sponsor.name,
status=sponsor.status,
),
status=HTTPStatus.CREATED,
)
except (ValueError, ValidationError, IntegrityError) as e:
return Response(
{"message": f"Error creating sponsor application: {e!s}"},
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
status=HTTPStatus.BAD_REQUEST,
)
Comment thread
anurag2787 marked this conversation as resolved.
23 changes: 23 additions & 0 deletions backend/apps/owasp/admin/sponsor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin):
"name",
"sort_name",
"sponsor_type",
"status",
"is_member",
"member_type",
"chapter",
"project",
)
search_fields = (
"name",
"sort_name",
"description",
"contact_email",
)
list_filter = (
"sponsor_type",
"status",
"is_member",
"member_type",
"chapter",
"project",
)
fieldsets = (
(
Expand All @@ -35,6 +42,7 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin):
"name",
"sort_name",
"description",
"contact_email",
)
},
),
Expand All @@ -55,10 +63,25 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin):
"is_member",
"member_type",
"sponsor_type",
"status",
)
},
),
(
"Entity Associations",
{
"fields": (
"chapter",
"project",
),
"description": (
"Optional: Link this sponsor to a specific chapter or project. "
"Leave blank for global sponsors."
),
},
),
)
readonly_fields = ("nest_created_at", "nest_updated_at")


admin.site.register(Sponsor, SponsorAdmin)
7 changes: 7 additions & 0 deletions backend/apps/owasp/api/internal/mutations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""OWASP GraphQL mutations."""

from .sponsor import SponsorMutations


class OwaspMutations(SponsorMutations):
"""OWASP mutations."""
113 changes: 113 additions & 0 deletions backend/apps/owasp/api/internal/mutations/sponsor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""OWASP sponsors GraphQL mutations."""

import logging
from typing import Optional

import strawberry
from django.db.utils import IntegrityError

from apps.common.utils import slugify
from apps.owasp.api.internal.nodes.sponsor import SponsorNode
from apps.owasp.models.sponsor import Sponsor

logger = logging.getLogger(__name__)


@strawberry.type
class CreateSponsorApplicationResult:
"""Result of creating a sponsor application."""

ok: bool
sponsor: Optional[SponsorNode] = None
code: Optional[str] = None
message: Optional[str] = None


@strawberry.type
class SponsorMutations:
"""Sponsor mutations."""

@strawberry.mutation
def create_sponsor_application(
self,
name: str,
contact_email: str,
sponsorship_interest: str,
website: Optional[str] = None,
) -> CreateSponsorApplicationResult:
"""Create a sponsor application.

Args:
name: Organization name
contact_email: Contact email address
sponsorship_interest: Message about sponsorship interest
website: Organization website (optional)

Returns:
CreateSponsorApplicationResult with sponsor application status
"""
try:
if not name or not name.strip():
return CreateSponsorApplicationResult(
ok=False,
code="INVALID_NAME",
message="Organization name is required",
)

if not contact_email or not contact_email.strip():
return CreateSponsorApplicationResult(
ok=False,
code="INVALID_EMAIL",
message="Contact email is required",
)

if not sponsorship_interest or not sponsorship_interest.strip():
return CreateSponsorApplicationResult(
ok=False,
code="INVALID_INTEREST",
message="Sponsorship interest message is required",
)

key = slugify(name.strip())

if Sponsor.objects.filter(key=key).exists():
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
return CreateSponsorApplicationResult(
ok=False,
code="DUPLICATE",
message="A sponsor with this organization name already exists",
)

sponsor = Sponsor(
name=name.strip(),
key=key,
contact_email=contact_email.strip(),
url=website.strip() if website else "",
description=sponsorship_interest.strip(),
status=Sponsor.Status.DRAFT,
sort_name=name.strip(),
)
sponsor.save()

Comment thread
anurag2787 marked this conversation as resolved.
Outdated
logger.info(f"Sponsor application created: {sponsor.id} - {sponsor.name}")

return CreateSponsorApplicationResult(
ok=True,
sponsor=sponsor,
code="SUCCESS",
message="Sponsor application submitted successfully",
)

except IntegrityError as err:
logger.warning(f"Error creating sponsor application: {err}")
return CreateSponsorApplicationResult(
ok=False,
code="ERROR",
message="Error submitting sponsor application",
)
except Exception as err:
logger.error(f"Unexpected error creating sponsor application: {err}")
return CreateSponsorApplicationResult(
ok=False,
code="ERROR",
message="An unexpected error occurred",
)
7 changes: 7 additions & 0 deletions backend/apps/owasp/api/internal/nodes/chapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

from apps.core.utils.index import deep_camelize
from apps.owasp.api.internal.nodes.common import GenericEntityNode
from apps.owasp.api.internal.nodes.sponsor import SponsorNode
from apps.owasp.models.chapter import Chapter
from apps.owasp.models.sponsor import Sponsor


@strawberry.type
Expand Down Expand Up @@ -61,3 +63,8 @@ def key(self, root: Chapter) -> str:
def suggested_location(self, root: Chapter) -> str | None:
"""Resolve suggested location."""
return root.idx_suggested_location

@strawberry_django.field(prefetch_related=["sponsors"])
def sponsors(self, root: Chapter) -> list[SponsorNode]:
"""Resolve active sponsors for this chapter."""
return root.sponsors.filter(status=Sponsor.Status.ACTIVE).order_by("name")
7 changes: 7 additions & 0 deletions backend/apps/owasp/api/internal/nodes/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from apps.owasp.api.internal.nodes.project_health_metrics import (
ProjectHealthMetricsNode,
)
from apps.owasp.api.internal.nodes.sponsor import SponsorNode
from apps.owasp.models.project import Project
from apps.owasp.models.sponsor import Sponsor

RECENT_ISSUES_LIMIT = 5
RECENT_RELEASES_LIMIT = 5
Expand Down Expand Up @@ -131,6 +133,11 @@ def repositories_count(self, root: Project) -> int:
"""Resolve repositories count."""
return root.idx_repositories_count

@strawberry_django.field(prefetch_related=["sponsors"])
def sponsors(self, root: Project) -> list[SponsorNode]:
"""Resolve active sponsors for this project."""
return root.sponsors.filter(status=Sponsor.Status.ACTIVE).order_by("name")

@strawberry_django.field
def topics(self, root: Project) -> list[str]:
"""Resolve topics."""
Expand Down
4 changes: 4 additions & 0 deletions backend/apps/owasp/api/internal/nodes/sponsor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
@strawberry_django.type(
Sponsor,
fields=[
"description",
"id",
"image_url",
"key",
"name",
"sponsor_type",
"status",
"url",
],
)
Expand Down
2 changes: 1 addition & 1 deletion backend/apps/owasp/api/internal/queries/sponsor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SponsorQuery:
def sponsors(self) -> list[SponsorNode]:
"""Resolve sponsors."""
return sorted(
Sponsor.objects.all(),
Sponsor.objects.filter(status=Sponsor.Status.ACTIVE),
key=lambda x: {
Sponsor.SponsorType.DIAMOND: 1,
Sponsor.SponsorType.PLATINUM: 2,
Expand Down
Loading