-
-
Notifications
You must be signed in to change notification settings - Fork 288
Improved the "/sponsors" command for slack #630
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
Changes from 36 commits
231f5c2
6fddf74
70a7dd2
2262c9d
1260440
ad7584e
8d3b4f2
2df44d4
fa9459a
f2dabbe
d6fe486
76e9010
5375a5b
dde2f69
9d0c001
16a758b
fc41e3f
2de5fe4
c004ae2
965a125
874aedc
354149f
8d29ff2
3346deb
55103aa
689d641
ad4b842
2afa6e3
ef34968
818aa74
7db1f02
cfc555d
2c3eec3
930c075
5d06b86
b65d610
fdda190
35fa0bf
370e32b
d9a8317
b45db63
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 |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| """OWASP app constants.""" | ||
|
|
||
| OWASP_ORGANIZATION_NAME = "OWASP" | ||
| OWASP_ORGANIZATION_DATA_URL = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main" |
| 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", | ||
| ) |
| 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() |
| 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]) | ||
| 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", | ||
| }, | ||
| ), | ||
| ] |
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Properties for human-readable types might throw exceptions. The 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @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}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 "" |
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.
🛠️ 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:
Note: This assumes the
bulk_savemethod 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