-
-
Notifications
You must be signed in to change notification settings - Fork 647
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 2 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 |
|---|---|---|
|
|
@@ -11,6 +11,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 +20,7 @@ | |
| class SponsorBase(Schema): | ||
| """Base schema for Sponsor (used in list endpoints).""" | ||
|
|
||
| description: str | ||
| image_url: str | ||
| key: str | ||
| name: str | ||
|
|
@@ -33,10 +35,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 +47,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 +81,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 +104,74 @@ 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.).""" | ||
| return list( | ||
| SponsorModel.objects.filter(status=SponsorModel.SponsorStatus.ACTIVE).order_by( | ||
| "sponsor_type", "name" | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/apply", | ||
| description="Submit a sponsor application. Creates a new sponsor record with draft status.", | ||
| operation_id="apply_sponsor", | ||
| response={ | ||
| HTTPStatus.BAD_REQUEST: ValidationErrorSchema, | ||
| HTTPStatus.CREATED: SponsorApplicationResponse, | ||
| }, | ||
| summary="Apply to become a sponsor", | ||
| ) | ||
| def apply_sponsor( | ||
| request: HttpRequest, | ||
| payload: SponsorApplication, | ||
| ) -> Response: | ||
| """Submit a sponsor application.""" | ||
|
Comment on lines
+144
to
+158
|
||
| key = slugify(payload.organization_name) | ||
|
|
||
| if SponsorModel.objects.filter(key=key).exists(): | ||
| return Response( | ||
| {"message": "A sponsor application with this organization name already exists."}, | ||
| status=HTTPStatus.BAD_REQUEST, | ||
| ) | ||
|
|
||
| sponsor = SponsorModel( | ||
| contact_email=payload.contact_email, | ||
| description=payload.message, | ||
| key=key, | ||
| name=payload.organization_name, | ||
| sort_name=payload.organization_name, | ||
| status=SponsorModel.SponsorStatus.DRAFT, | ||
| url=payload.website, | ||
| ) | ||
| sponsor.save() | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return Response( | ||
| { | ||
| "message": "Sponsor application submitted successfully. " | ||
| "It will be reviewed by the OWASP team.", | ||
| "key": key, | ||
| }, | ||
| status=HTTPStatus.CREATED, | ||
| ) | ||
|
|
||
|
|
||
| @router.get( | ||
|
|
||
| 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.