-
-
Notifications
You must be signed in to change notification settings - Fork 629
Feat: Implement Sponsors Program Support #4525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
29c4ba9
684ce40
9b8f00e
a71c02d
b145f66
fa0d855
ce1064a
1162239
b79a091
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |||||||
| from http import HTTPStatus | ||||||||
| from typing import Literal | ||||||||
|
|
||||||||
| from django.db import IntegrityError, transaction | ||||||||
| from django.db.models import Case, IntegerField, Value, When | ||||||||
| from django.http import HttpRequest | ||||||||
| from ninja import Field, FilterSchema, Path, Query, Schema | ||||||||
| from ninja.decorators import decorate_view | ||||||||
|
|
@@ -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"]) | ||||||||
|
|
@@ -19,6 +22,7 @@ | |||||||
| class SponsorBase(Schema): | ||||||||
| """Base schema for Sponsor (used in list endpoints).""" | ||||||||
|
|
||||||||
| description: str | ||||||||
| image_url: str | ||||||||
| key: str | ||||||||
| name: str | ||||||||
|
|
@@ -33,10 +37,10 @@ class Sponsor(SponsorBase): | |||||||
| class SponsorDetail(SponsorBase): | ||||||||
| """Detail schema for Sponsor (used in single item endpoints).""" | ||||||||
|
|
||||||||
| description: str | ||||||||
| is_member: bool | ||||||||
| job_url: str | ||||||||
| member_type: str | ||||||||
| status: str | ||||||||
|
|
||||||||
|
|
||||||||
| class SponsorError(Schema): | ||||||||
|
|
@@ -45,6 +49,22 @@ class SponsorError(Schema): | |||||||
| message: str | ||||||||
|
|
||||||||
|
|
||||||||
| class SponsorApplication(Schema): | ||||||||
| """Schema for sponsor application form submission.""" | ||||||||
|
|
||||||||
| organization_name: str = Field(..., description="Name of the sponsoring organization") | ||||||||
| website: str = Field("", description="Organization website URL") | ||||||||
| contact_email: str = Field(..., description="Contact email address") | ||||||||
| message: str = Field("", description="Sponsorship interest or message") | ||||||||
|
|
||||||||
|
|
||||||||
| class SponsorApplicationResponse(Schema): | ||||||||
| """Response schema for sponsor application.""" | ||||||||
|
|
||||||||
| message: str | ||||||||
| key: str | ||||||||
|
|
||||||||
|
|
||||||||
| class SponsorFilter(FilterSchema): | ||||||||
| """Filter for Sponsor.""" | ||||||||
|
|
||||||||
|
|
@@ -63,6 +83,11 @@ class SponsorFilter(FilterSchema): | |||||||
| example="Silver", | ||||||||
| ) | ||||||||
|
|
||||||||
| status: str | None = Field( | ||||||||
| None, | ||||||||
| description="Filter by sponsor status (draft, active, archived). Defaults to active.", | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| @router.get( | ||||||||
| "/", | ||||||||
|
|
@@ -81,7 +106,91 @@ def list_sponsors( | |||||||
| ), | ||||||||
| ) -> list[Sponsor]: | ||||||||
| """Get sponsors.""" | ||||||||
| return filters.filter(SponsorModel.objects.order_by(ordering or "name")) | ||||||||
| qs = SponsorModel.objects.order_by(ordering or "name") | ||||||||
| if filters.status is None: | ||||||||
| qs = qs.filter(status=SponsorModel.SponsorStatus.ACTIVE) | ||||||||
|
|
||||||||
| return filters.filter(qs) | ||||||||
|
|
||||||||
|
|
||||||||
| @router.get( | ||||||||
| "/nest", | ||||||||
| description="Retrieve active OWASP Nest sponsors for external integrations.", | ||||||||
| operation_id="list_nest_sponsors", | ||||||||
| response=list[Sponsor], | ||||||||
| summary="List Nest sponsors", | ||||||||
| ) | ||||||||
| @decorate_view(cache_response()) | ||||||||
| def list_nest_sponsors( | ||||||||
| request: HttpRequest, | ||||||||
| ) -> list[Sponsor]: | ||||||||
| """Get active Nest sponsors for external integrations (GitHub Actions, dashboards, etc.).""" | ||||||||
| tier_order = Case( | ||||||||
| When(sponsor_type=SponsorModel.SponsorType.DIAMOND, then=Value(1)), | ||||||||
| When(sponsor_type=SponsorModel.SponsorType.PLATINUM, then=Value(2)), | ||||||||
| When(sponsor_type=SponsorModel.SponsorType.GOLD, then=Value(3)), | ||||||||
| When(sponsor_type=SponsorModel.SponsorType.SILVER, then=Value(4)), | ||||||||
| When(sponsor_type=SponsorModel.SponsorType.SUPPORTER, then=Value(5)), | ||||||||
| default=Value(6), | ||||||||
| output_field=IntegerField(), | ||||||||
| ) | ||||||||
| return list( | ||||||||
| SponsorModel.objects.filter(status=SponsorModel.SponsorStatus.ACTIVE) | ||||||||
|
||||||||
| SponsorModel.objects.filter(status=SponsorModel.SponsorStatus.ACTIVE) | |
| SponsorModel.objects.filter(status=SponsorModel.SponsorStatus.ACTIVE) | |
| .exclude(sponsor_type=SponsorModel.SponsorType.NOT_SPONSOR) |
Copilot
AI
Apr 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The /api/v0 Ninja API is configured with auth=ApiKey() (see backend/apps/api/rest/v0/__init__.py:41-48), so this POST endpoint will return 401 in production unless you explicitly override auth for this operation/router. Since the frontend sponsor application form calls this endpoint without an API key, consider setting auth=None for this route (and adding appropriate anti-abuse controls like throttling/CAPTCHA) or proxying the request through a server-side endpoint that can attach credentials safely.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| @strawberry_django.type( | ||
| Sponsor, | ||
| fields=[ | ||
| "description", | ||
| "image_url", | ||
| "name", | ||
| "sponsor_type", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Generated manually for sponsor status, email, and entity FK fields. | ||
|
|
||
| import django.db.models.deletion | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("owasp", "0072_project_project_name_gin_idx_and_more"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="sponsor", | ||
| name="status", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("draft", "Draft"), | ||
| ("active", "Active"), | ||
| ("archived", "Archived"), | ||
| ], | ||
| default="active", | ||
| max_length=20, | ||
| verbose_name="Status", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="sponsor", | ||
| name="contact_email", | ||
| field=models.EmailField( | ||
| blank=True, | ||
| default="", | ||
| max_length=254, | ||
| verbose_name="Contact Email", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="sponsor", | ||
| name="chapter", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| related_name="sponsors", | ||
| to="owasp.chapter", | ||
| verbose_name="Chapter", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="sponsor", | ||
| name="project", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| related_name="sponsors", | ||
| to="owasp.project", | ||
| verbose_name="Project", | ||
| ), | ||
| ), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue/requirements specify a REST endpoint like
/api/v0/projects/nest/sponsors, but this PR introduces/api/v0/sponsors/nest. If external integrations are expected to follow the documented project-scoped route, consider adding the endpoint under the Projects router (or adding an alias/redirect) to avoid breaking the intended API contract.