From 231f5c232d056b35c53ba7ec23d1c7f17e9ecc66 Mon Sep 17 00:00:00 2001 From: AbhayMishra Date: Tue, 28 Jan 2025 05:27:49 +0530 Subject: [PATCH 01/25] added model, command , algolia , sponsors --- backend/Makefile | 1 + backend/apps/owasp/admin.py | 46 +++++ backend/apps/owasp/api/search/sponsor.py | 29 ++++ backend/apps/owasp/index/__init__.py | 1 + backend/apps/owasp/index/sponsor.py | 54 ++++++ .../management/commands/load_sponsor_data.py | 29 ++++ backend/apps/owasp/models/mixins/sponsor.py | 74 ++++++++ backend/apps/owasp/models/sponsor.py | 164 ++++++++++++++++++ backend/apps/slack/commands/sponsors.py | 26 +-- backend/apps/slack/common/handlers/sponsor.py | 105 +++++++++++ 10 files changed, 517 insertions(+), 12 deletions(-) create mode 100644 backend/apps/owasp/api/search/sponsor.py create mode 100644 backend/apps/owasp/index/sponsor.py create mode 100644 backend/apps/owasp/management/commands/load_sponsor_data.py create mode 100644 backend/apps/owasp/models/mixins/sponsor.py create mode 100644 backend/apps/owasp/models/sponsor.py create mode 100644 backend/apps/slack/common/handlers/sponsor.py diff --git a/backend/Makefile b/backend/Makefile index 97b80d7d16..efaaf36ee1 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -42,6 +42,7 @@ index-data: load-data: @echo "Loading Nest data" @CMD="poetry run python manage.py load_data" $(MAKE) exec-backend-command + @CMD="poetry run python manage.py load_sponsor_data" $(MAKE) exec-backend-command merge-migrations: @CMD="poetry run python manage.py makemigrations --merge" $(MAKE) exec-backend-command diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index 9c9c00c0f6..3c04b4d505 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -7,6 +7,7 @@ from apps.owasp.models.committee import Committee from apps.owasp.models.event import Event from apps.owasp.models.project import Project +from apps.owasp.models.sponsor import Sponsor class GenericEntityAdminMixin: @@ -100,8 +101,53 @@ def custom_field_name(self, obj): custom_field_name.short_description = "Name" +class SponsorAdmin(admin.ModelAdmin): + """Admin configuration for Sponsor model.""" + + list_display = ( + 'name', + 'sort_name', + 'is_active_sponsor', + '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') + }), + ('Status', { + 'fields': ('is_member', 'member_type', 'sponsor_type') + }), + ) + + readonly_fields = ('is_active_sponsor',) + + def is_active_sponsor(self, obj): + """Display if sponsor is active.""" + return obj.is_active_sponsor + is_active_sponsor.boolean = True + is_active_sponsor.short_description = "Active Sponsor" + admin.site.register(Chapter, ChapterAdmin) admin.site.register(Committee, CommetteeAdmin) admin.site.register(Event, EventAdmin) admin.site.register(Project, ProjectAdmin) +admin.site.register(Sponsor, SponsorAdmin) diff --git a/backend/apps/owasp/api/search/sponsor.py b/backend/apps/owasp/api/search/sponsor.py new file mode 100644 index 0000000000..f29a4a2523 --- /dev/null +++ b/backend/apps/owasp/api/search/sponsor.py @@ -0,0 +1,29 @@ +"""OWASP app sponsor search API.""" + +from algoliasearch_django import raw_search +from apps.owasp.models.sponsor import Sponsor + +def get_sponsors(query, attributes=None, limit=25, page=1): + params = { + "attributesToHighlight": [], + "attributesToRetrieve": attributes + or [ + "idx_name", + "idx_sort_name", + "idx_description", + "idx_url", + "idx_job_url", + "idx_image", + "idx_member_type", + "idx_sponsor_type", + "idx_member_level", + "idx_sponsor_level", + "idx_is_member", + "idx_is_active_sponsor" + ], + "hitsPerPage": limit, + "minProximity": 4, + "page": page - 1, + "typoTolerance": "min" + } + return raw_search(Sponsor, query, params) diff --git a/backend/apps/owasp/index/__init__.py b/backend/apps/owasp/index/__init__.py index f74fb24491..bc10b09de7 100644 --- a/backend/apps/owasp/index/__init__.py +++ b/backend/apps/owasp/index/__init__.py @@ -3,3 +3,4 @@ from apps.owasp.index.chapter import ChapterIndex from apps.owasp.index.committee import CommitteeIndex from apps.owasp.index.project import ProjectIndex +from apps.owasp.index.sponsor import SponsorIndex \ No newline at end of file diff --git a/backend/apps/owasp/index/sponsor.py b/backend/apps/owasp/index/sponsor.py new file mode 100644 index 0000000000..36825d57d6 --- /dev/null +++ b/backend/apps/owasp/index/sponsor.py @@ -0,0 +1,54 @@ +"""OWASP app sponsor index.""" + +from algoliasearch_django import AlgoliaIndex +from algoliasearch_django.decorators import register +from apps.common.index import IS_LOCAL_BUILD, LOCAL_INDEX_LIMIT +from apps.owasp.models.sponsor import Sponsor + +@register(Sponsor) +class SponsorIndex(AlgoliaIndex): + index_name = "sponsors" + fields = ( + "idx_name", + "idx_sort_name", + "idx_description", + "idx_url", + "idx_job_url", + "idx_image", + "idx_member_type", + "idx_sponsor_type", + "idx_member_level", + "idx_sponsor_level", + "idx_is_member", + "idx_is_active_sponsor", + ) + settings = { + "attributesForFaceting": [ + "filterOnly(idx_name)", + "idx_is_active_sponsor", + ], + "indexLanguages": ["en"], + "customRanking": [ + "asc(idx_sort_name)", + ], + "ranking": [ + "typo", + "words", + "filters", + "proximity", + "attribute", + "exact", + "custom", + ], + "searchableAttributes": [ + "unordered(idx_name)", + "unordered(idx_sort_name)", + "unordered(idx_member_level)", + "unordered(idx_sponsor_level)", + ], + } + should_index = "is_indexable" + + def get_queryset(self): + qs = Sponsor.objects.all() + return qs[:LOCAL_INDEX_LIMIT] if IS_LOCAL_BUILD else qs diff --git a/backend/apps/owasp/management/commands/load_sponsor_data.py b/backend/apps/owasp/management/commands/load_sponsor_data.py new file mode 100644 index 0000000000..9739731a53 --- /dev/null +++ b/backend/apps/owasp/management/commands/load_sponsor_data.py @@ -0,0 +1,29 @@ +import yaml +from django.core.management.base import BaseCommand +from apps.owasp.models.sponsor import Sponsor +from apps.github.utils import get_repository_file_content, normalize_url + +class Command(BaseCommand): + help = "Import sponsors from OWASP GitHub repository" + + def handle(self, *args, **kwargs): + url = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/corp_members.yml" + yaml_content = get_repository_file_content(url).replace("\t", " ") + data = yaml.safe_load(yaml_content) + + for entry in data: + sponsor = Sponsor( + name=entry.get("name", ""), + sort_name=entry.get("sortname", ""), + description=entry.get("description", ""), + url=normalize_url(entry.get("url", "")), + job_url=normalize_url(entry.get("job_url", "") or ""), + image=entry.get("image", ""), + is_member=entry.get("member", False), + member_type=entry.get("membertype", "4") or "4", + sponsor_type=entry.get("sponsor", "-1") or "-1", + ) + sponsor.save() + self.stdout.write(self.style.SUCCESS(f"Successfully imported sponsor: {sponsor.name}")) + + self.stdout.write(self.style.SUCCESS("Finished importing sponsors")) diff --git a/backend/apps/owasp/models/mixins/sponsor.py b/backend/apps/owasp/models/mixins/sponsor.py new file mode 100644 index 0000000000..62c1391458 --- /dev/null +++ b/backend/apps/owasp/models/mixins/sponsor.py @@ -0,0 +1,74 @@ +"""OWASP app sponsor mixins.""" + +from apps.owasp.models.mixins.common import GenericEntityMixin + +class SponsorIndexMixin(GenericEntityMixin): + @property + def idx_created_at(self): + """Get created timestamp for index.""" + return self.nest_created_at + + @property + def idx_updated_at(self): + """Get updated timestamp for index.""" + return self.nest_updated_at + + @property + def idx_name(self): + """Get name for index.""" + return self.name + + @property + def idx_sort_name(self): + """Get sort name for index.""" + return self.sort_name + + @property + def idx_description(self): + """Get description for index.""" + return self.description + + @property + def idx_url(self): + """Get URL for index.""" + return self.url + + @property + def idx_job_url(self): + """Get job URL for index.""" + return self.job_url + + @property + def idx_image(self): + """Get image path for index.""" + return self.image + + @property + def idx_member_type(self): + """Get member type for index.""" + return self.member_type + + @property + def idx_sponsor_type(self): + """Get sponsor type for index.""" + return self.sponsor_type + + @property + def idx_member_level(self): + """Get member level for index.""" + return self.member_level + + @property + def idx_sponsor_level(self): + """Get sponsor level for index.""" + return self.sponsor_level + + @property + def idx_is_member(self): + """Get member status for index.""" + return self.is_member + + @property + def idx_is_active_sponsor(self): + """Get active sponsor status for index.""" + return self.is_active_sponsor \ No newline at end of file diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py new file mode 100644 index 0000000000..76667b1e88 --- /dev/null +++ b/backend/apps/owasp/models/sponsor.py @@ -0,0 +1,164 @@ +"""OWASP app sponsor models.""" +from django.db import models +from apps.common.models import BulkSaveModel, TimestampedModel + + +class Sponsor( BulkSaveModel, TimestampedModel): + """Sponsor model.""" + + objects = models.Manager() + + class Meta: + db_table = "owasp_sponsors" + verbose_name_plural = "Sponsors" + + class SponsorType(models.TextChoices): + DIAMOND = "1", "Diamond" + PLATINUM = "2", "Platinum" + GOLD = "3", "Gold" + SILVER = "4", "Silver" + SUPPORTER = "5", "Supporter" + NOT_SPONSOR = "-1", "Not a Sponsor" + + class MemberType(models.TextChoices): + PLATINUM = "2", "Platinum" + GOLD = "3", "Gold" + SILVER = "4", "Silver" + + # Basic information + name = models.CharField(verbose_name="Name", max_length=255) + sort_name = models.CharField(verbose_name="Sort Name", max_length=255) + description = models.TextField(verbose_name="Description", blank=True) + + # URLs and images + url = models.URLField(verbose_name="Website URL", blank=True, null=True) + job_url = models.URLField(verbose_name="Job URL", blank=True, null=True) + image = models.CharField(verbose_name="Image Path", max_length=255, blank=True) + + # Status fields + is_member = models.BooleanField(verbose_name="Is Corporate Member", default=False) + member_type = models.CharField( + verbose_name="Member Type", + max_length=2, + choices=MemberType.choices, + default=MemberType.SILVER, + blank=True, + null=True + ) + sponsor_type = models.CharField( + verbose_name="Sponsor Type", + max_length=2, + choices=SponsorType.choices, + default=SponsorType.NOT_SPONSOR, + null=True + ) + + @property + def idx_created_at(self): + """Get created timestamp for index.""" + return self.nest_created_at + + @property + def idx_updated_at(self): + """Get updated timestamp for index.""" + return self.nest_updated_at + + @property + def idx_name(self): + """Get name for index.""" + return self.name + + @property + def idx_sort_name(self): + """Get sort name for index.""" + return self.sort_name + + @property + def idx_description(self): + """Get description for index.""" + return self.description + + @property + def idx_url(self): + """Get URL for index.""" + return self.url + + @property + def idx_job_url(self): + """Get job URL for index.""" + return self.job_url + + @property + def idx_image(self): + """Get image path for index.""" + return self.image + + @property + def idx_member_type(self): + """Get member type for index.""" + return self.member_type + + @property + def idx_sponsor_type(self): + """Get sponsor type for index.""" + return self.sponsor_type + + @property + def idx_member_level(self): + """Get member level for index.""" + return self.member_level + + @property + def idx_sponsor_level(self): + """Get sponsor level for index.""" + return self.sponsor_level + + @property + def idx_is_member(self): + """Get member status for index.""" + return self.is_member + + @property + def idx_is_active_sponsor(self): + """Get active sponsor status for index.""" + return self.is_active_sponsor + + def __str__(self): + """Sponsor human readable representation.""" + return f"{self.name}" + + @property + def is_active_sponsor(self): + """Check if the organization is an active sponsor.""" + return self.sponsor_type != self.SponsorType.NOT_SPONSOR + + @property + def sponsor_level(self): + """Get human-readable sponsor level.""" + return self.SponsorType(str(self.sponsor_type)).label if self.is_active_sponsor else None + + @property + def member_level(self): + """Get human-readable member level.""" + return self.MemberType(str(self.member_type)).label if self.member_type else None + @property + def is_indexable(self): + """Determine if the sponsor should be indexed in Algolia.""" + return True + + @staticmethod + def bulk_save(sponsors, fields=None): + """Bulk save sponsors.""" + BulkSaveModel.bulk_save(Sponsor, sponsors, fields=fields) + + @staticmethod + def update_data(sponsor_id, **kwargs): + """Update sponsor data.""" + try: + sponsor = Sponsor.objects.get(id=sponsor_id) + for key, value in kwargs.items(): + setattr(sponsor, key, value) + sponsor.save() + return sponsor + except Sponsor.DoesNotExist: + return None \ No newline at end of file diff --git a/backend/apps/slack/commands/sponsors.py b/backend/apps/slack/commands/sponsors.py index 8e204e331a..0a065b5e6d 100644 --- a/backend/apps/slack/commands/sponsors.py +++ b/backend/apps/slack/commands/sponsors.py @@ -1,30 +1,32 @@ """Slack bot sponsors command.""" from django.conf import settings - -from apps.common.constants import NL from apps.slack.apps import SlackConfig -from apps.slack.blocks import markdown +from apps.slack.common.handlers.sponsor import get_blocks +from apps.slack.common.presentation import EntityPresentation COMMAND = "/sponsors" - def sponsors_handler(ack, command, client): - """Slack /sponsors command handler.""" ack() - if not settings.SLACK_COMMANDS_ENABLED: return - blocks = [ - markdown( - f"Please visit page{NL}" + search_query = command["text"].strip() + blocks = get_blocks( + search_query=search_query, + limit=10, + presentation=EntityPresentation( + include_feedback=True, + include_metadata=True, + include_pagination=False, + include_timestamps=True, + name_truncation=80, + summary_truncation=300, ), - ] - + ) conversation = client.conversations_open(users=command["user_id"]) client.chat_postMessage(channel=conversation["channel"]["id"], blocks=blocks) - if SlackConfig.app: sponsors_handler = SlackConfig.app.command(COMMAND)(sponsors_handler) diff --git a/backend/apps/slack/common/handlers/sponsor.py b/backend/apps/slack/common/handlers/sponsor.py new file mode 100644 index 0000000000..ea2373ca6b --- /dev/null +++ b/backend/apps/slack/common/handlers/sponsor.py @@ -0,0 +1,105 @@ +"""Handler for OWASP Sponsors Slack functionality.""" + +from __future__ import annotations + +from django.conf import settings +from django.utils.text import Truncator + +from apps.common.constants import NL +from apps.common.utils import get_absolute_url +from apps.slack.blocks import get_pagination_buttons, markdown +from apps.slack.common.constants import TRUNCATION_INDICATOR +from apps.slack.common.presentation import EntityPresentation +from apps.slack.constants import FEEDBACK_CHANNEL_MESSAGE +from apps.slack.utils import escape + + +def get_blocks( + page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None +): + """Get sponsors blocks.""" + from apps.owasp.api.search.sponsor import get_sponsors + from apps.owasp.models.sponsor import Sponsor + + presentation = presentation or EntityPresentation() + search_query_escaped = escape(search_query) + + attributes = [ + "idx_name", + "idx_description", + "idx_url", + "idx_sponsor_level", + "idx_is_active_sponsor", + ] + + offset = (page - 1) * limit + sponsors_data = get_sponsors(search_query, attributes=attributes, limit=limit, page=page) + sponsors = sponsors_data["hits"] + total_pages = sponsors_data["nbPages"] + + if not sponsors: + return [ + markdown( + f"*No sponsors found for `{search_query_escaped}`*{NL}" + if search_query + else f"*No sponsors found*{NL}" + ) + ] + + blocks = [ + markdown( + f"{NL}*OWASP sponsors that I found for* `{search_query_escaped}`:{NL}" + if search_query_escaped + else f"{NL}*OWASP sponsors:*{NL}" + ), + ] + + for idx, sponsor in enumerate(sponsors): + name = Truncator(escape(sponsor["idx_name"])).chars( + presentation.name_truncation, truncate=TRUNCATION_INDICATOR + ) + description = Truncator(sponsor["idx_description"]).chars( + presentation.summary_truncation, truncate=TRUNCATION_INDICATOR + ) + + sponsor_level = sponsor.get("idx_sponsor_level", "") + sponsor_level_text = ( + f"_Level: {sponsor_level}_{NL}" + if sponsor_level and presentation.include_metadata + else "" + ) + + is_active = sponsor.get("idx_is_active_sponsor", False) + status_text = "(Active)" if is_active else "(Inactive)" + + blocks.append( + markdown( + f"{offset + idx + 1}. <{sponsor['idx_url']}|*{name}*> {status_text}{NL}" + f"{sponsor_level_text}" + f"{escape(description)}{NL}" + ) + ) + + if presentation.include_feedback: + blocks.append( + markdown( + f"⚠️ *Extended search over OWASP sponsors " + f"is available at <{get_absolute_url('sponsors')}?q={search_query}|{settings.SITE_NAME}>*{NL}" + f"{FEEDBACK_CHANNEL_MESSAGE}" + ) + ) + if presentation.include_pagination and ( + pagination_block := get_pagination_buttons( + "sponsors", + page, + total_pages - 1, + ) + ): + blocks.append( + { + "type": "actions", + "elements": pagination_block, + } + ) + + return blocks From 6fddf74204c6417e42814595837496d0f701c4e6 Mon Sep 17 00:00:00 2001 From: AbhayMishra Date: Tue, 28 Jan 2025 17:54:03 +0530 Subject: [PATCH 02/25] Enhanced command and removed extra indexed from data --- backend/apps/common/constants.py | 1 + backend/apps/owasp/admin.py | 52 +++---- backend/apps/owasp/api/search/sponsor.py | 12 +- backend/apps/owasp/index/__init__.py | 2 +- backend/apps/owasp/index/sponsor.py | 19 ++- .../management/commands/load_sponsor_data.py | 30 ++-- backend/apps/owasp/migrations/0015_sponsor.py | 71 ++++++++++ backend/apps/owasp/models/mixins/sponsor.py | 36 +---- backend/apps/owasp/models/sponsor.py | 132 ++++-------------- backend/apps/slack/commands/sponsors.py | 13 +- backend/apps/slack/common/handlers/sponsor.py | 56 ++++---- 11 files changed, 202 insertions(+), 222 deletions(-) create mode 100644 backend/apps/owasp/migrations/0015_sponsor.py diff --git a/backend/apps/common/constants.py b/backend/apps/common/constants.py index 7c806991a8..82df3a30db 100644 --- a/backend/apps/common/constants.py +++ b/backend/apps/common/constants.py @@ -1,4 +1,5 @@ """Common app constants.""" NL = "\n" +OWASP_WEBSITE_URL = "https://owasp.org" TAB = "\t" diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index 3c04b4d505..0b1d5742e3 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -101,49 +101,35 @@ def custom_field_name(self, obj): custom_field_name.short_description = "Name" + class SponsorAdmin(admin.ModelAdmin): """Admin configuration for Sponsor model.""" - + list_display = ( - 'name', - 'sort_name', - 'is_active_sponsor', - 'sponsor_type', - 'is_member', - 'member_type', + "name", + "sort_name", + "sponsor_type", + "is_member", + "member_type", ) - + search_fields = ( - 'name', - 'sort_name', - 'description', + "name", + "sort_name", + "description", ) - + list_filter = ( - 'sponsor_type', - 'is_member', - 'member_type', + "sponsor_type", + "is_member", + "member_type", ) - + fieldsets = ( - ('Basic Information', { - 'fields': ('name', 'sort_name', 'description') - }), - ('URLs and Images', { - 'fields': ('url', 'job_url', 'image') - }), - ('Status', { - 'fields': ('is_member', 'member_type', 'sponsor_type') - }), + ("Basic Information", {"fields": ("name", "sort_name", "description")}), + ("URLs and Images", {"fields": ("url", "job_url", "image_path")}), + ("Status", {"fields": ("is_member", "member_type", "sponsor_type")}), ) - - readonly_fields = ('is_active_sponsor',) - - def is_active_sponsor(self, obj): - """Display if sponsor is active.""" - return obj.is_active_sponsor - is_active_sponsor.boolean = True - is_active_sponsor.short_description = "Active Sponsor" admin.site.register(Chapter, ChapterAdmin) diff --git a/backend/apps/owasp/api/search/sponsor.py b/backend/apps/owasp/api/search/sponsor.py index f29a4a2523..f449ecc384 100644 --- a/backend/apps/owasp/api/search/sponsor.py +++ b/backend/apps/owasp/api/search/sponsor.py @@ -1,9 +1,12 @@ """OWASP app sponsor search API.""" from algoliasearch_django import raw_search + from apps.owasp.models.sponsor import Sponsor + def get_sponsors(query, attributes=None, limit=25, page=1): + """Return sponsors relevant to a search query.""" params = { "attributesToHighlight": [], "attributesToRetrieve": attributes @@ -13,17 +16,16 @@ def get_sponsors(query, attributes=None, limit=25, page=1): "idx_description", "idx_url", "idx_job_url", - "idx_image", + "idx_image_path", "idx_member_type", "idx_sponsor_type", - "idx_member_level", - "idx_sponsor_level", "idx_is_member", - "idx_is_active_sponsor" ], "hitsPerPage": limit, "minProximity": 4, "page": page - 1, - "typoTolerance": "min" + "typoTolerance": "min", + "facetFilters": [], } + return raw_search(Sponsor, query, params) diff --git a/backend/apps/owasp/index/__init__.py b/backend/apps/owasp/index/__init__.py index bc10b09de7..9f84906e9e 100644 --- a/backend/apps/owasp/index/__init__.py +++ b/backend/apps/owasp/index/__init__.py @@ -3,4 +3,4 @@ from apps.owasp.index.chapter import ChapterIndex from apps.owasp.index.committee import CommitteeIndex from apps.owasp.index.project import ProjectIndex -from apps.owasp.index.sponsor import SponsorIndex \ No newline at end of file +from apps.owasp.index.sponsor import SponsorIndex diff --git a/backend/apps/owasp/index/sponsor.py b/backend/apps/owasp/index/sponsor.py index 36825d57d6..7e3af6068c 100644 --- a/backend/apps/owasp/index/sponsor.py +++ b/backend/apps/owasp/index/sponsor.py @@ -2,11 +2,15 @@ from algoliasearch_django import AlgoliaIndex from algoliasearch_django.decorators import register + from apps.common.index import IS_LOCAL_BUILD, LOCAL_INDEX_LIMIT from apps.owasp.models.sponsor import Sponsor + @register(Sponsor) class SponsorIndex(AlgoliaIndex): + """Sponsor index.""" + index_name = "sponsors" fields = ( "idx_name", @@ -14,18 +18,18 @@ class SponsorIndex(AlgoliaIndex): "idx_description", "idx_url", "idx_job_url", - "idx_image", + "idx_image_path", "idx_member_type", "idx_sponsor_type", - "idx_member_level", - "idx_sponsor_level", "idx_is_member", - "idx_is_active_sponsor", ) settings = { "attributesForFaceting": [ "filterOnly(idx_name)", - "idx_is_active_sponsor", + "filterOnly(idx_sort_name)", + "idx_member_type", + "idx_sponsor_type", + "idx_is_member", ], "indexLanguages": ["en"], "customRanking": [ @@ -43,12 +47,13 @@ class SponsorIndex(AlgoliaIndex): "searchableAttributes": [ "unordered(idx_name)", "unordered(idx_sort_name)", - "unordered(idx_member_level)", - "unordered(idx_sponsor_level)", + "unordered(idx_member_type)", + "unordered(idx_sponsor_type)", ], } should_index = "is_indexable" def get_queryset(self): + """Get queryset.""" qs = Sponsor.objects.all() return qs[:LOCAL_INDEX_LIMIT] if IS_LOCAL_BUILD else qs diff --git a/backend/apps/owasp/management/commands/load_sponsor_data.py b/backend/apps/owasp/management/commands/load_sponsor_data.py index 9739731a53..b14d80cd47 100644 --- a/backend/apps/owasp/management/commands/load_sponsor_data.py +++ b/backend/apps/owasp/management/commands/load_sponsor_data.py @@ -1,7 +1,11 @@ +"""A command to add OWASP sponsors data.""" + import yaml from django.core.management.base import BaseCommand -from apps.owasp.models.sponsor import Sponsor + from apps.github.utils import get_repository_file_content, normalize_url +from apps.owasp.models.sponsor import Sponsor + class Command(BaseCommand): help = "Import sponsors from OWASP GitHub repository" @@ -12,17 +16,19 @@ def handle(self, *args, **kwargs): data = yaml.safe_load(yaml_content) for entry in data: - sponsor = Sponsor( - name=entry.get("name", ""), - sort_name=entry.get("sortname", ""), - description=entry.get("description", ""), - url=normalize_url(entry.get("url", "")), - job_url=normalize_url(entry.get("job_url", "") or ""), - image=entry.get("image", ""), - is_member=entry.get("member", False), - member_type=entry.get("membertype", "4") or "4", - sponsor_type=entry.get("sponsor", "-1") or "-1", - ) + fields = { + "name": entry.get("name", ""), + "sort_name": entry.get("sortname", "").capitalize(), + "description": entry.get("description", ""), + "url": normalize_url(entry.get("url", "")) or "", + "job_url": normalize_url(entry.get("job_url", "")) or "", + "image_path": entry.get("image", ""), + "is_member": entry.get("member", False), + "member_type": entry.get("membertype", "4") or "4", + "sponsor_type": entry.get("sponsor", "-1") or "-1", + } + + sponsor = Sponsor(**fields) sponsor.save() self.stdout.write(self.style.SUCCESS(f"Successfully imported sponsor: {sponsor.name}")) diff --git a/backend/apps/owasp/migrations/0015_sponsor.py b/backend/apps/owasp/migrations/0015_sponsor.py new file mode 100644 index 0000000000..b9dc835938 --- /dev/null +++ b/backend/apps/owasp/migrations/0015_sponsor.py @@ -0,0 +1,71 @@ +# Generated by Django 5.1.5 on 2025-01-28 11:40 + +from django.db import migrations, models + +import apps.owasp.models.mixins.sponsor + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0014_project_custom_tags"), + ] + + 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)), + ("name", models.CharField(max_length=255, verbose_name="Name")), + ("sort_name", models.CharField(max_length=255, verbose_name="Sort Name")), + ("description", models.TextField(blank=True, verbose_name="Description")), + ("url", models.URLField(blank=True, verbose_name="Website URL")), + ("job_url", models.URLField(blank=True, verbose_name="Job URL")), + ( + "image_path", + 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=[("2", "Platinum"), ("3", "Gold"), ("4", "Silver")], + default="4", + max_length=2, + verbose_name="Member Type", + ), + ), + ( + "sponsor_type", + models.CharField( + choices=[ + ("1", "Diamond"), + ("2", "Platinum"), + ("3", "Gold"), + ("4", "Silver"), + ("5", "Supporter"), + ("-1", "Not a Sponsor"), + ], + default="-1", + max_length=2, + verbose_name="Sponsor Type", + ), + ), + ], + options={ + "verbose_name_plural": "Sponsors", + "db_table": "owasp_sponsors", + }, + bases=(apps.owasp.models.mixins.sponsor.SponsorIndexMixin, models.Model), + ), + ] diff --git a/backend/apps/owasp/models/mixins/sponsor.py b/backend/apps/owasp/models/mixins/sponsor.py index 62c1391458..aac5bf4877 100644 --- a/backend/apps/owasp/models/mixins/sponsor.py +++ b/backend/apps/owasp/models/mixins/sponsor.py @@ -2,7 +2,10 @@ from apps.owasp.models.mixins.common import GenericEntityMixin + class SponsorIndexMixin(GenericEntityMixin): + """Sponsor index mixin.""" + @property def idx_created_at(self): """Get created timestamp for index.""" @@ -13,21 +16,11 @@ def idx_updated_at(self): """Get updated timestamp for index.""" return self.nest_updated_at - @property - def idx_name(self): - """Get name for index.""" - return self.name - @property def idx_sort_name(self): """Get sort name for index.""" return self.sort_name - @property - def idx_description(self): - """Get description for index.""" - return self.description - @property def idx_url(self): """Get URL for index.""" @@ -39,36 +32,21 @@ def idx_job_url(self): return self.job_url @property - def idx_image(self): + def idx_image_path(self): """Get image path for index.""" - return self.image + return self.image_path @property def idx_member_type(self): """Get member type for index.""" - return self.member_type + return self.readable_member_type @property def idx_sponsor_type(self): """Get sponsor type for index.""" - return self.sponsor_type - - @property - def idx_member_level(self): - """Get member level for index.""" - return self.member_level - - @property - def idx_sponsor_level(self): - """Get sponsor level for index.""" - return self.sponsor_level + return self.readable_sponsor_type @property def idx_is_member(self): """Get member status for index.""" return self.is_member - - @property - def idx_is_active_sponsor(self): - """Get active sponsor status for index.""" - return self.is_active_sponsor \ No newline at end of file diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py index 76667b1e88..2fba386e9f 100644 --- a/backend/apps/owasp/models/sponsor.py +++ b/backend/apps/owasp/models/sponsor.py @@ -1,17 +1,20 @@ """OWASP app sponsor models.""" + from django.db import models + from apps.common.models import BulkSaveModel, TimestampedModel +from apps.owasp.models.mixins.sponsor import SponsorIndexMixin -class Sponsor( BulkSaveModel, TimestampedModel): +class Sponsor(BulkSaveModel, SponsorIndexMixin, TimestampedModel): """Sponsor model.""" - + objects = models.Manager() - + class Meta: db_table = "owasp_sponsors" verbose_name_plural = "Sponsors" - + class SponsorType(models.TextChoices): DIAMOND = "1", "Diamond" PLATINUM = "2", "Platinum" @@ -19,138 +22,62 @@ class SponsorType(models.TextChoices): SILVER = "4", "Silver" SUPPORTER = "5", "Supporter" NOT_SPONSOR = "-1", "Not a Sponsor" - + class MemberType(models.TextChoices): PLATINUM = "2", "Platinum" GOLD = "3", "Gold" SILVER = "4", "Silver" - + # Basic information name = models.CharField(verbose_name="Name", max_length=255) sort_name = models.CharField(verbose_name="Sort Name", max_length=255) description = models.TextField(verbose_name="Description", blank=True) - + # URLs and images - url = models.URLField(verbose_name="Website URL", blank=True, null=True) - job_url = models.URLField(verbose_name="Job URL", blank=True, null=True) - image = models.CharField(verbose_name="Image Path", max_length=255, blank=True) - + url = models.URLField(verbose_name="Website URL", blank=True) + job_url = models.URLField(verbose_name="Job URL", blank=True) + image_path = models.CharField(verbose_name="Image Path", max_length=255, blank=True) + # Status fields - is_member = models.BooleanField(verbose_name="Is Corporate Member", default=False) + is_member = models.BooleanField(verbose_name="Is Corporate Sponsor", default=False) member_type = models.CharField( verbose_name="Member Type", max_length=2, choices=MemberType.choices, default=MemberType.SILVER, blank=True, - null=True ) sponsor_type = models.CharField( verbose_name="Sponsor Type", max_length=2, choices=SponsorType.choices, default=SponsorType.NOT_SPONSOR, - null=True ) - - @property - def idx_created_at(self): - """Get created timestamp for index.""" - return self.nest_created_at - - @property - def idx_updated_at(self): - """Get updated timestamp for index.""" - return self.nest_updated_at - - @property - def idx_name(self): - """Get name for index.""" - return self.name - - @property - def idx_sort_name(self): - """Get sort name for index.""" - return self.sort_name - - @property - def idx_description(self): - """Get description for index.""" - return self.description - - @property - def idx_url(self): - """Get URL for index.""" - return self.url - - @property - def idx_job_url(self): - """Get job URL for index.""" - return self.job_url - @property - def idx_image(self): - """Get image path for index.""" - return self.image - - @property - def idx_member_type(self): - """Get member type for index.""" - return self.member_type - - @property - def idx_sponsor_type(self): - """Get sponsor type for index.""" - return self.sponsor_type - - @property - def idx_member_level(self): - """Get member level for index.""" - return self.member_level - - @property - def idx_sponsor_level(self): - """Get sponsor level for index.""" - return self.sponsor_level - - @property - def idx_is_member(self): - """Get member status for index.""" - return self.is_member - - @property - def idx_is_active_sponsor(self): - """Get active sponsor status for index.""" - return self.is_active_sponsor - def __str__(self): """Sponsor human readable representation.""" return f"{self.name}" - - @property - def is_active_sponsor(self): - """Check if the organization is an active sponsor.""" - return self.sponsor_type != self.SponsorType.NOT_SPONSOR - + @property - def sponsor_level(self): - """Get human-readable sponsor level.""" - return self.SponsorType(str(self.sponsor_type)).label if self.is_active_sponsor else None - + def readable_sponsor_type(self): + """Get human-readable sponsor type.""" + return self.SponsorType(str(self.sponsor_type)).label + @property - def member_level(self): - """Get human-readable member level.""" - return self.MemberType(str(self.member_type)).label if self.member_type else None + def readable_member_type(self): + """Get human-readable member type.""" + return self.MemberType(str(self.member_type)).label + @property def is_indexable(self): """Determine if the sponsor should be indexed in Algolia.""" return True - - @staticmethod + + @staticmethod def bulk_save(sponsors, fields=None): """Bulk save sponsors.""" BulkSaveModel.bulk_save(Sponsor, sponsors, fields=fields) - + @staticmethod def update_data(sponsor_id, **kwargs): """Update sponsor data.""" @@ -159,6 +86,7 @@ def update_data(sponsor_id, **kwargs): for key, value in kwargs.items(): setattr(sponsor, key, value) sponsor.save() - return sponsor except Sponsor.DoesNotExist: - return None \ No newline at end of file + return None + else: + return sponsor diff --git a/backend/apps/slack/commands/sponsors.py b/backend/apps/slack/commands/sponsors.py index 0a065b5e6d..6d1f02a122 100644 --- a/backend/apps/slack/commands/sponsors.py +++ b/backend/apps/slack/commands/sponsors.py @@ -1,13 +1,16 @@ """Slack bot sponsors command.""" from django.conf import settings + from apps.slack.apps import SlackConfig from apps.slack.common.handlers.sponsor import get_blocks from apps.slack.common.presentation import EntityPresentation COMMAND = "/sponsors" + def sponsors_handler(ack, command, client): + """Slack /sponsors command handler.""" ack() if not settings.SLACK_COMMANDS_ENABLED: return @@ -25,8 +28,16 @@ def sponsors_handler(ack, command, client): summary_truncation=300, ), ) + + fallback_text = "OWASP Sponsors Information" + if search_query: + fallback_text += f" - Search results for: {search_query}" + conversation = client.conversations_open(users=command["user_id"]) - client.chat_postMessage(channel=conversation["channel"]["id"], blocks=blocks) + client.chat_postMessage( + channel=conversation["channel"]["id"], blocks=blocks, text=fallback_text + ) + if SlackConfig.app: sponsors_handler = SlackConfig.app.command(COMMAND)(sponsors_handler) diff --git a/backend/apps/slack/common/handlers/sponsor.py b/backend/apps/slack/common/handlers/sponsor.py index ea2373ca6b..eac62be6be 100644 --- a/backend/apps/slack/common/handlers/sponsor.py +++ b/backend/apps/slack/common/handlers/sponsor.py @@ -2,12 +2,10 @@ from __future__ import annotations -from django.conf import settings from django.utils.text import Truncator -from apps.common.constants import NL -from apps.common.utils import get_absolute_url -from apps.slack.blocks import get_pagination_buttons, markdown +from apps.common.constants import NL, OWASP_WEBSITE_URL +from apps.slack.blocks import markdown from apps.slack.common.constants import TRUNCATION_INDICATOR from apps.slack.common.presentation import EntityPresentation from apps.slack.constants import FEEDBACK_CHANNEL_MESSAGE @@ -19,23 +17,23 @@ def get_blocks( ): """Get sponsors blocks.""" from apps.owasp.api.search.sponsor import get_sponsors - from apps.owasp.models.sponsor import Sponsor presentation = presentation or EntityPresentation() search_query_escaped = escape(search_query) attributes = [ "idx_name", + "idx_sort_name", "idx_description", "idx_url", - "idx_sponsor_level", - "idx_is_active_sponsor", + "idx_member_type", + "idx_sponsor_type", + "idx_is_member", ] offset = (page - 1) * limit sponsors_data = get_sponsors(search_query, attributes=attributes, limit=limit, page=page) sponsors = sponsors_data["hits"] - total_pages = sponsors_data["nbPages"] if not sponsors: return [ @@ -62,20 +60,27 @@ def get_blocks( presentation.summary_truncation, truncate=TRUNCATION_INDICATOR ) - sponsor_level = sponsor.get("idx_sponsor_level", "") - sponsor_level_text = ( - f"_Level: {sponsor_level}_{NL}" - if sponsor_level and presentation.include_metadata + member_type = sponsor.get("idx_member_type", "") + sponsor_type = sponsor.get("idx_sponsor_type", "") + is_member = sponsor.get("idx_is_member", False) + + metadata_text = [] + if member_type and presentation.include_metadata: + metadata_text.append(f"Member Type: {member_type}") + if sponsor_type and presentation.include_metadata: + metadata_text.append(f"Sponsor Type: {sponsor_type}") + + metadata_line = ( + f"_{' | '.join(metadata_text)}_{NL}" + if metadata_text and presentation.include_metadata else "" ) - is_active = sponsor.get("idx_is_active_sponsor", False) - status_text = "(Active)" if is_active else "(Inactive)" - blocks.append( markdown( - f"{offset + idx + 1}. <{sponsor['idx_url']}|*{name}*> {status_text}{NL}" - f"{sponsor_level_text}" + f"{offset + idx + 1}. <{sponsor['idx_url']}|*{name}*>" + f"{' (Corporate Sponsor)' if is_member else ''}{NL}" + f"{metadata_line}" f"{escape(description)}{NL}" ) ) @@ -83,23 +88,10 @@ def get_blocks( if presentation.include_feedback: blocks.append( markdown( - f"⚠️ *Extended search over OWASP sponsors " - f"is available at <{get_absolute_url('sponsors')}?q={search_query}|{settings.SITE_NAME}>*{NL}" + f"*Please visit the <{OWASP_WEBSITE_URL}/supporters|OWASP supporters>" + f" for more information about the sponsors*{NL}" f"{FEEDBACK_CHANNEL_MESSAGE}" ) ) - if presentation.include_pagination and ( - pagination_block := get_pagination_buttons( - "sponsors", - page, - total_pages - 1, - ) - ): - blocks.append( - { - "type": "actions", - "elements": pagination_block, - } - ) return blocks From 2262c9ddb66ec16275bf439e09d61f82654c0c15 Mon Sep 17 00:00:00 2001 From: AbhayMishra Date: Wed, 29 Jan 2025 01:22:41 +0530 Subject: [PATCH 03/25] Added testcases --- .../tests/owasp/api/search/sponsor_tests.py | 85 ++++++++++ backend/tests/owasp/models/sponsor_tests.py | 120 ++++++++++++++ .../tests/slack/commands/sponsors_tests.py | 154 ++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 backend/tests/owasp/api/search/sponsor_tests.py create mode 100644 backend/tests/owasp/models/sponsor_tests.py create mode 100644 backend/tests/slack/commands/sponsors_tests.py diff --git a/backend/tests/owasp/api/search/sponsor_tests.py b/backend/tests/owasp/api/search/sponsor_tests.py new file mode 100644 index 0000000000..2b334a2bf2 --- /dev/null +++ b/backend/tests/owasp/api/search/sponsor_tests.py @@ -0,0 +1,85 @@ +import pytest +from unittest.mock import patch +from apps.owasp.api.search.sponsor import get_sponsors + +MOCKED_SPONSOR_HITS = { + "hits": [ + { + "idx_name": "Example Corp", + "idx_sort_name": "EXAMPLE CORP", + "idx_description": "A leading security company", + "idx_url": "https://example.com", + "idx_job_url": "https://example.com/careers", + "idx_image_path": "/assets/images/sponsors/example.png", + "idx_member_type": "Corporate", + "idx_sponsor_type": "Gold", + "idx_is_member": True + }, + { + "idx_name": "Security Plus", + "idx_sort_name": "SECURITY PLUS", + "idx_description": "Cybersecurity solutions provider", + "idx_url": "https://securityplus.com", + "idx_job_url": "https://securityplus.com/jobs", + "idx_image_path": "/assets/images/sponsors/secplus.png", + "idx_member_type": "Corporate", + "idx_sponsor_type": "Silver", + "idx_is_member": True + } + ], + "nbPages": 3 +} + +@pytest.mark.parametrize( + ("query", "limit", "page", "expected_hits"), + [ + ("security", 25, 1, MOCKED_SPONSOR_HITS), + ("example", 10, 2, MOCKED_SPONSOR_HITS), + ("", 25, 1, MOCKED_SPONSOR_HITS), + ], +) +def test_get_sponsors_basic_search(query, limit, page, expected_hits): + """Test basic sponsor search with different queries and pagination.""" + with patch( + "apps.owasp.api.search.sponsor.raw_search", + return_value=expected_hits + ) as mock_raw_search: + result = get_sponsors(query, limit=limit, page=page) + + assert result == expected_hits + + mock_raw_search.assert_called_once() + _, call_query, call_params = mock_raw_search.call_args[0] + + assert call_query == query + assert call_params["hitsPerPage"] == limit + assert call_params["page"] == page - 1 + +def test_get_sponsors_custom_attributes(): + """Test sponsor search with custom attributes to retrieve.""" + custom_attributes = ["idx_name", "idx_url"] + + with patch( + "apps.owasp.api.search.sponsor.raw_search", + return_value=MOCKED_SPONSOR_HITS + ) as mock_raw_search: + result = get_sponsors("test", attributes=custom_attributes) + + _, _, call_params = mock_raw_search.call_args[0] + assert call_params["attributesToRetrieve"] == custom_attributes + +def test_get_sponsors_default_parameters(): + """Test sponsor search with default parameters.""" + with patch( + "apps.owasp.api.search.sponsor.raw_search", + return_value=MOCKED_SPONSOR_HITS + ) as mock_raw_search: + result = get_sponsors("test") + + _, _, call_params = mock_raw_search.call_args[0] + assert call_params["hitsPerPage"] == 25 + assert call_params["page"] == 0 + assert call_params["minProximity"] == 4 + assert call_params["typoTolerance"] == "min" + assert isinstance(call_params["attributesToRetrieve"], list) + assert len(call_params["attributesToRetrieve"]) > 0 \ No newline at end of file diff --git a/backend/tests/owasp/models/sponsor_tests.py b/backend/tests/owasp/models/sponsor_tests.py new file mode 100644 index 0000000000..5bbd2490b0 --- /dev/null +++ b/backend/tests/owasp/models/sponsor_tests.py @@ -0,0 +1,120 @@ +from unittest.mock import Mock, patch +import pytest +from django.core.exceptions import ValidationError +from apps.owasp.models.sponsor import Sponsor + + +class TestSponsorModel: + @pytest.mark.parametrize( + ("name", "expected_str"), + [ + ("Test Sponsor", "Test Sponsor"), + ("", ""), + ], + ) + def test_str_representation(self, name, expected_str): + """Test the __str__ method of the Sponsor model.""" + sponsor = Sponsor(name=name) + assert str(sponsor) == expected_str + + @pytest.mark.parametrize( + ("sponsor_type", "expected_label"), + [ + ("1", "Diamond"), + ("2", "Platinum"), + ("3", "Gold"), + ("4", "Silver"), + ("5", "Supporter"), + ("-1", "Not a Sponsor"), + ], + ) + def test_readable_sponsor_type(self, sponsor_type, expected_label): + """Test the readable_sponsor_type property.""" + sponsor = Sponsor(sponsor_type=sponsor_type) + assert sponsor.readable_sponsor_type == expected_label + + @pytest.mark.parametrize( + ("member_type", "expected_label"), + [ + ("2", "Platinum"), + ("3", "Gold"), + ("4", "Silver"), + ], + ) + def test_readable_member_type(self, member_type, expected_label): + """Test the readable_member_type property.""" + sponsor = Sponsor(member_type=member_type) + assert sponsor.readable_member_type == expected_label + + def test_is_indexable(self): + """Test the is_indexable property.""" + sponsor = Sponsor() + assert sponsor.is_indexable is True + + def test_bulk_save(self): + """Test the bulk_save method.""" + mock_sponsors = [Mock(id=None), Mock(id=1)] + with patch("apps.owasp.models.sponsor.BulkSaveModel.bulk_save") as mock_bulk_save: + Sponsor.bulk_save(mock_sponsors, fields=["name"]) + mock_bulk_save.assert_called_once_with(Sponsor, mock_sponsors, fields=["name"]) + + def test_update_data(self): + """Test the update_data method.""" + mock_sponsor = Mock() + mock_sponsor.id = 1 + mock_sponsor.name = "Old Name" + mock_sponsor.sponsor_type = "1" + + with patch("apps.owasp.models.sponsor.Sponsor.objects.get", return_value=mock_sponsor): + updated_sponsor = Sponsor.update_data(mock_sponsor.id, name="New Name", sponsor_type="2") + assert updated_sponsor.name == "New Name" + assert updated_sponsor.sponsor_type == "2" + + with patch("apps.owasp.models.sponsor.Sponsor.objects.get", side_effect=Sponsor.DoesNotExist): + non_existent_sponsor = Sponsor.update_data(9999, name="New Name") + assert non_existent_sponsor is None + + @pytest.mark.parametrize( + ("url", "is_valid"), + [ + ("https://example.com", True), + ("invalid-url", False), + ], + ) + def test_sponsor_validation(self, url, is_valid): + """Test validation of Sponsor fields.""" + sponsor = Sponsor( + name="Test Sponsor", + sort_name="Sponsor", + description="Test Description", + url=url, + job_url="https://jobs.example.com", + image_path="/images/test.png", + is_member=True, + member_type="2", + sponsor_type="1", + ) + + if is_valid: + sponsor.full_clean() + else: + with pytest.raises(ValidationError): + sponsor.full_clean() + + + + + @pytest.mark.parametrize( + ("sponsor_type", "expected_sponsor_type"), + [ + ("1", "1"), # DIAMOND + ("2", "2"), # PLATINUM + ("-1", "-1"), # NOT_SPONSOR + ], + ) + def test_sponsor_type_default(self, sponsor_type, expected_sponsor_type): + """Test the default sponsor_type behavior.""" + sponsor = Sponsor(sponsor_type=sponsor_type) + assert sponsor.sponsor_type == expected_sponsor_type + + diff --git a/backend/tests/slack/commands/sponsors_tests.py b/backend/tests/slack/commands/sponsors_tests.py new file mode 100644 index 0000000000..d433a9ae4a --- /dev/null +++ b/backend/tests/slack/commands/sponsors_tests.py @@ -0,0 +1,154 @@ +from unittest.mock import Mock, patch +import pytest +from django.conf import settings + +from apps.slack.commands.sponsors import sponsors_handler +from apps.slack.common.presentation import EntityPresentation + +@pytest.fixture(autouse=True) +def mock_get_absolute_url(): + with patch("apps.common.utils.get_absolute_url") as mock: + mock.return_value = "http://example.com" + yield mock + +@pytest.fixture(autouse=True) +def mock_sponsors_count(): + with patch("apps.owasp.models.sponsor.Sponsor.objects.count") as mock: + mock.return_value = 50 + yield mock + +class TestSponsorsHandler: + @pytest.fixture() + def mock_command(self): + return { + "text": "", + "user_id": "U123456", + } + + @pytest.fixture() + def mock_client(self): + client = Mock() + client.conversations_open.return_value = {"channel": {"id": "C123456"}} + return client + + @pytest.fixture() + def mock_ack(self): + return Mock() + + @pytest.fixture(autouse=True) + def mock_get_blocks(self): + with patch("apps.slack.commands.sponsors.get_blocks") as mock: + mock.return_value = [{"type": "section", "text": {"type": "mrkdwn", "text": "Test Block"}}] + yield mock + + @pytest.mark.parametrize( + ("commands_enabled", "search_query", "expected_calls"), + [ + (True, "", 1), + (True, "diamond", 1), + (True, "platinum", 1), + (False, "", 0), + (False, "search term", 0), + ], + ) + def test_sponsors_handler_command_states( + self, mock_client, mock_command, mock_ack, commands_enabled, search_query, expected_calls + ): + """Test sponsor handler with different command states and search queries.""" + settings.SLACK_COMMANDS_ENABLED = commands_enabled + mock_command["text"] = search_query + + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + mock_ack.assert_called_once() + + assert mock_client.chat_postMessage.call_count == expected_calls + if expected_calls > 0: + mock_client.conversations_open.assert_called_once_with(users=mock_command["user_id"]) + + @pytest.mark.parametrize( + ("search_query", "expected_text"), + [ + ("", "OWASP Sponsors Information"), + ("diamond", "OWASP Sponsors Information - Search results for: diamond"), + ("platinum", "OWASP Sponsors Information - Search results for: platinum"), + ], + ) + def test_sponsors_handler_fallback_text( + self, mock_client, mock_command, mock_ack, search_query, expected_text + ): + """Test fallback text generation with different search queries.""" + settings.SLACK_COMMANDS_ENABLED = True + mock_command["text"] = search_query + + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + mock_client.chat_postMessage.assert_called_once() + assert mock_client.chat_postMessage.call_args[1]["text"] == expected_text + + def test_sponsors_handler_presentation_config(self, mock_client, mock_command, mock_ack, mock_get_blocks): + """Test that presentation configuration is correctly passed to get_blocks.""" + settings.SLACK_COMMANDS_ENABLED = True + mock_command["text"] = "test" + + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + mock_get_blocks.assert_called_once() + _, kwargs = mock_get_blocks.call_args + presentation = kwargs["presentation"] + assert isinstance(presentation, EntityPresentation) + assert presentation.include_feedback is True + assert presentation.include_metadata is True + assert presentation.include_pagination is False + assert presentation.include_timestamps is True + assert presentation.name_truncation == 80 + assert presentation.summary_truncation == 300 + + def test_sponsors_handler_search_query_processing(self, mock_client, mock_command, mock_ack, mock_get_blocks): + """Test search query processing and stripping.""" + settings.SLACK_COMMANDS_ENABLED = True + test_queries = [ + " test ", + "test", + " TEST ", + "Test " + ] + + for query in test_queries: + mock_command["text"] = query + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + mock_get_blocks.assert_called_with( + search_query=query.strip(), + limit=10, + presentation=pytest.approx(EntityPresentation( + include_feedback=True, + include_metadata=True, + include_pagination=False, + include_timestamps=True, + name_truncation=80, + summary_truncation=300, + )) + ) + + def test_sponsors_handler_channel_error(self, mock_client, mock_command, mock_ack): + """Test handling of channel opening errors.""" + settings.SLACK_COMMANDS_ENABLED = True + mock_client.conversations_open.side_effect = Exception("Channel error") + + with pytest.raises(Exception) as exc_info: + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + assert str(exc_info.value) == "Channel error" + mock_client.chat_postMessage.assert_not_called() + + def test_sponsors_handler_message_error(self, mock_client, mock_command, mock_ack): + """Test handling of message posting errors.""" + settings.SLACK_COMMANDS_ENABLED = True + mock_client.chat_postMessage.side_effect = Exception("Message error") + + with pytest.raises(Exception) as exc_info: + sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + + assert str(exc_info.value) == "Message error" + mock_client.conversations_open.assert_called_once() \ No newline at end of file From 126044018120d3e37e140b4fb2f1a3e0eaf9149d Mon Sep 17 00:00:00 2001 From: abhayymishraaa Date: Fri, 31 Jan 2025 22:43:38 +0530 Subject: [PATCH 04/25] verified commit --- .../tests/owasp/api/search/sponsor_tests.py | 51 +++++++++------- backend/tests/owasp/models/sponsor_tests.py | 21 ++++--- .../tests/slack/commands/sponsors_tests.py | 61 +++++++++++-------- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/backend/tests/owasp/api/search/sponsor_tests.py b/backend/tests/owasp/api/search/sponsor_tests.py index 2b334a2bf2..6bd72bee9f 100644 --- a/backend/tests/owasp/api/search/sponsor_tests.py +++ b/backend/tests/owasp/api/search/sponsor_tests.py @@ -1,7 +1,13 @@ -import pytest from unittest.mock import patch + +import pytest + from apps.owasp.api.search.sponsor import get_sponsors +DEFAULT_HITS_PER_PAGE = 25 +DEFAULT_MIN_PROXIMITY = 4 + + MOCKED_SPONSOR_HITS = { "hits": [ { @@ -13,7 +19,7 @@ "idx_image_path": "/assets/images/sponsors/example.png", "idx_member_type": "Corporate", "idx_sponsor_type": "Gold", - "idx_is_member": True + "idx_is_member": True, }, { "idx_name": "Security Plus", @@ -24,12 +30,13 @@ "idx_image_path": "/assets/images/sponsors/secplus.png", "idx_member_type": "Corporate", "idx_sponsor_type": "Silver", - "idx_is_member": True - } + "idx_is_member": True, + }, ], - "nbPages": 3 + "nbPages": 3, } + @pytest.mark.parametrize( ("query", "limit", "page", "expected_hits"), [ @@ -41,45 +48,45 @@ def test_get_sponsors_basic_search(query, limit, page, expected_hits): """Test basic sponsor search with different queries and pagination.""" with patch( - "apps.owasp.api.search.sponsor.raw_search", - return_value=expected_hits + "apps.owasp.api.search.sponsor.raw_search", return_value=expected_hits ) as mock_raw_search: result = get_sponsors(query, limit=limit, page=page) - + assert result == expected_hits - + mock_raw_search.assert_called_once() _, call_query, call_params = mock_raw_search.call_args[0] - + assert call_query == query assert call_params["hitsPerPage"] == limit assert call_params["page"] == page - 1 + def test_get_sponsors_custom_attributes(): """Test sponsor search with custom attributes to retrieve.""" custom_attributes = ["idx_name", "idx_url"] - + with patch( - "apps.owasp.api.search.sponsor.raw_search", - return_value=MOCKED_SPONSOR_HITS + "apps.owasp.api.search.sponsor.raw_search", return_value=MOCKED_SPONSOR_HITS ) as mock_raw_search: - result = get_sponsors("test", attributes=custom_attributes) - + # Call function without storing unused result + get_sponsors("test", attributes=custom_attributes) + _, _, call_params = mock_raw_search.call_args[0] assert call_params["attributesToRetrieve"] == custom_attributes + def test_get_sponsors_default_parameters(): """Test sponsor search with default parameters.""" with patch( - "apps.owasp.api.search.sponsor.raw_search", - return_value=MOCKED_SPONSOR_HITS + "apps.owasp.api.search.sponsor.raw_search", return_value=MOCKED_SPONSOR_HITS ) as mock_raw_search: - result = get_sponsors("test") - + get_sponsors("test") + _, _, call_params = mock_raw_search.call_args[0] - assert call_params["hitsPerPage"] == 25 + assert call_params["hitsPerPage"] == DEFAULT_HITS_PER_PAGE assert call_params["page"] == 0 - assert call_params["minProximity"] == 4 + assert call_params["minProximity"] == DEFAULT_MIN_PROXIMITY assert call_params["typoTolerance"] == "min" assert isinstance(call_params["attributesToRetrieve"], list) - assert len(call_params["attributesToRetrieve"]) > 0 \ No newline at end of file + assert len(call_params["attributesToRetrieve"]) > 0 diff --git a/backend/tests/owasp/models/sponsor_tests.py b/backend/tests/owasp/models/sponsor_tests.py index 5bbd2490b0..6bcce81b33 100644 --- a/backend/tests/owasp/models/sponsor_tests.py +++ b/backend/tests/owasp/models/sponsor_tests.py @@ -1,6 +1,8 @@ from unittest.mock import Mock, patch + import pytest from django.core.exceptions import ValidationError + from apps.owasp.models.sponsor import Sponsor @@ -66,11 +68,15 @@ def test_update_data(self): mock_sponsor.sponsor_type = "1" with patch("apps.owasp.models.sponsor.Sponsor.objects.get", return_value=mock_sponsor): - updated_sponsor = Sponsor.update_data(mock_sponsor.id, name="New Name", sponsor_type="2") + updated_sponsor = Sponsor.update_data( + mock_sponsor.id, name="New Name", sponsor_type="2" + ) assert updated_sponsor.name == "New Name" assert updated_sponsor.sponsor_type == "2" - with patch("apps.owasp.models.sponsor.Sponsor.objects.get", side_effect=Sponsor.DoesNotExist): + with patch( + "apps.owasp.models.sponsor.Sponsor.objects.get", side_effect=Sponsor.DoesNotExist + ): non_existent_sponsor = Sponsor.update_data(9999, name="New Name") assert non_existent_sponsor is None @@ -91,19 +97,16 @@ def test_sponsor_validation(self, url, is_valid): job_url="https://jobs.example.com", image_path="/images/test.png", is_member=True, - member_type="2", - sponsor_type="1", + member_type="2", + sponsor_type="1", ) if is_valid: - sponsor.full_clean() + sponsor.full_clean() else: with pytest.raises(ValidationError): sponsor.full_clean() - - - @pytest.mark.parametrize( ("sponsor_type", "expected_sponsor_type"), [ @@ -116,5 +119,3 @@ def test_sponsor_type_default(self, sponsor_type, expected_sponsor_type): """Test the default sponsor_type behavior.""" sponsor = Sponsor(sponsor_type=sponsor_type) assert sponsor.sponsor_type == expected_sponsor_type - - diff --git a/backend/tests/slack/commands/sponsors_tests.py b/backend/tests/slack/commands/sponsors_tests.py index d433a9ae4a..57e1217927 100644 --- a/backend/tests/slack/commands/sponsors_tests.py +++ b/backend/tests/slack/commands/sponsors_tests.py @@ -1,22 +1,30 @@ from unittest.mock import Mock, patch + import pytest from django.conf import settings from apps.slack.commands.sponsors import sponsors_handler from apps.slack.common.presentation import EntityPresentation +# Constants for truncation values +NAME_TRUNCATION_LIMIT = 80 +SUMMARY_TRUNCATION_LIMIT = 300 + + @pytest.fixture(autouse=True) def mock_get_absolute_url(): with patch("apps.common.utils.get_absolute_url") as mock: mock.return_value = "http://example.com" yield mock + @pytest.fixture(autouse=True) def mock_sponsors_count(): with patch("apps.owasp.models.sponsor.Sponsor.objects.count") as mock: mock.return_value = 50 yield mock + class TestSponsorsHandler: @pytest.fixture() def mock_command(self): @@ -38,7 +46,9 @@ def mock_ack(self): @pytest.fixture(autouse=True) def mock_get_blocks(self): with patch("apps.slack.commands.sponsors.get_blocks") as mock: - mock.return_value = [{"type": "section", "text": {"type": "mrkdwn", "text": "Test Block"}}] + mock.return_value = [ + {"type": "section", "text": {"type": "mrkdwn", "text": "Test Block"}} + ] yield mock @pytest.mark.parametrize( @@ -86,7 +96,9 @@ def test_sponsors_handler_fallback_text( mock_client.chat_postMessage.assert_called_once() assert mock_client.chat_postMessage.call_args[1]["text"] == expected_text - def test_sponsors_handler_presentation_config(self, mock_client, mock_command, mock_ack, mock_get_blocks): + def test_sponsors_handler_presentation_config( + self, mock_client, mock_command, mock_ack, mock_get_blocks + ): """Test that presentation configuration is correctly passed to get_blocks.""" settings.SLACK_COMMANDS_ENABLED = True mock_command["text"] = "test" @@ -101,34 +113,33 @@ def test_sponsors_handler_presentation_config(self, mock_client, mock_command, m assert presentation.include_metadata is True assert presentation.include_pagination is False assert presentation.include_timestamps is True - assert presentation.name_truncation == 80 - assert presentation.summary_truncation == 300 + assert presentation.name_truncation == NAME_TRUNCATION_LIMIT + assert presentation.summary_truncation == SUMMARY_TRUNCATION_LIMIT - def test_sponsors_handler_search_query_processing(self, mock_client, mock_command, mock_ack, mock_get_blocks): + def test_sponsors_handler_search_query_processing( + self, mock_client, mock_command, mock_ack, mock_get_blocks + ): """Test search query processing and stripping.""" settings.SLACK_COMMANDS_ENABLED = True - test_queries = [ - " test ", - "test", - " TEST ", - "Test " - ] + test_queries = [" test ", "test", " TEST ", "Test "] for query in test_queries: mock_command["text"] = query sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) - + mock_get_blocks.assert_called_with( search_query=query.strip(), limit=10, - presentation=pytest.approx(EntityPresentation( - include_feedback=True, - include_metadata=True, - include_pagination=False, - include_timestamps=True, - name_truncation=80, - summary_truncation=300, - )) + presentation=pytest.approx( + EntityPresentation( + include_feedback=True, + include_metadata=True, + include_pagination=False, + include_timestamps=True, + name_truncation=80, + summary_truncation=300, + ) + ), ) def test_sponsors_handler_channel_error(self, mock_client, mock_command, mock_ack): @@ -136,10 +147,9 @@ def test_sponsors_handler_channel_error(self, mock_client, mock_command, mock_ac settings.SLACK_COMMANDS_ENABLED = True mock_client.conversations_open.side_effect = Exception("Channel error") - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception, match="Channel error"): sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) - - assert str(exc_info.value) == "Channel error" + mock_client.chat_postMessage.assert_not_called() def test_sponsors_handler_message_error(self, mock_client, mock_command, mock_ack): @@ -147,8 +157,5 @@ def test_sponsors_handler_message_error(self, mock_client, mock_command, mock_ac settings.SLACK_COMMANDS_ENABLED = True mock_client.chat_postMessage.side_effect = Exception("Message error") - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception, match="Message error"): sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) - - assert str(exc_info.value) == "Message error" - mock_client.conversations_open.assert_called_once() \ No newline at end of file From 8d3b4f23a82c2cefb8fe388cf4def60a64cb97bc Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Sun, 2 Feb 2025 12:07:21 +0530 Subject: [PATCH 05/25] verified commit with chnages --- .../tests/owasp/api/search/{sponsor_tests.py => sponsor_test.py} | 0 backend/tests/owasp/models/{sponsor_tests.py => sponsor_test.py} | 0 .../tests/slack/commands/{sponsors_tests.py => sponsors_test.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename backend/tests/owasp/api/search/{sponsor_tests.py => sponsor_test.py} (100%) rename backend/tests/owasp/models/{sponsor_tests.py => sponsor_test.py} (100%) rename backend/tests/slack/commands/{sponsors_tests.py => sponsors_test.py} (100%) diff --git a/backend/tests/owasp/api/search/sponsor_tests.py b/backend/tests/owasp/api/search/sponsor_test.py similarity index 100% rename from backend/tests/owasp/api/search/sponsor_tests.py rename to backend/tests/owasp/api/search/sponsor_test.py diff --git a/backend/tests/owasp/models/sponsor_tests.py b/backend/tests/owasp/models/sponsor_test.py similarity index 100% rename from backend/tests/owasp/models/sponsor_tests.py rename to backend/tests/owasp/models/sponsor_test.py diff --git a/backend/tests/slack/commands/sponsors_tests.py b/backend/tests/slack/commands/sponsors_test.py similarity index 100% rename from backend/tests/slack/commands/sponsors_tests.py rename to backend/tests/slack/commands/sponsors_test.py From fa9459a8e9f58f8aef667c0965c516f3f316403d Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Tue, 4 Feb 2025 00:23:28 +0530 Subject: [PATCH 06/25] fixed importing --- backend/apps/owasp/models/mixins/sponsor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/owasp/models/mixins/sponsor.py b/backend/apps/owasp/models/mixins/sponsor.py index aac5bf4877..2da18a598d 100644 --- a/backend/apps/owasp/models/mixins/sponsor.py +++ b/backend/apps/owasp/models/mixins/sponsor.py @@ -1,9 +1,9 @@ """OWASP app sponsor mixins.""" -from apps.owasp.models.mixins.common import GenericEntityMixin +from apps.owasp.models.mixins.common import RepositoryBasedEntityModelMixin -class SponsorIndexMixin(GenericEntityMixin): +class SponsorIndexMixin(RepositoryBasedEntityModelMixin): """Sponsor index mixin.""" @property From 9d0c00111bcf09a149cf42a76129d4763c9d5f0d Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 14 Feb 2025 02:48:34 +0530 Subject: [PATCH 07/25] resolved conflict --- backend/apps/slack/commands/sponsors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/apps/slack/commands/sponsors.py b/backend/apps/slack/commands/sponsors.py index 0f7b0c61fc..5485b66fb6 100644 --- a/backend/apps/slack/commands/sponsors.py +++ b/backend/apps/slack/commands/sponsors.py @@ -5,7 +5,6 @@ from apps.slack.apps import SlackConfig from apps.slack.common.handlers.sponsor import get_blocks from apps.slack.common.presentation import EntityPresentation -from apps.slack.blocks import markdown from apps.slack.utils import get_text COMMAND = "/sponsors" From fc41e3fbf9a79f921efb82fe6892924fd49f6506 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 14 Feb 2025 12:41:42 +0530 Subject: [PATCH 08/25] fixed testcase --- backend/tests/slack/commands/sponsors_test.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/tests/slack/commands/sponsors_test.py b/backend/tests/slack/commands/sponsors_test.py index 57e1217927..d5f58822b1 100644 --- a/backend/tests/slack/commands/sponsors_test.py +++ b/backend/tests/slack/commands/sponsors_test.py @@ -47,7 +47,13 @@ def mock_ack(self): def mock_get_blocks(self): with patch("apps.slack.commands.sponsors.get_blocks") as mock: mock.return_value = [ - {"type": "section", "text": {"type": "mrkdwn", "text": "Test Block"}} + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "OWASP Sponsors Information - Search results for: diamond", + }, + } ] yield mock @@ -79,9 +85,9 @@ def test_sponsors_handler_command_states( @pytest.mark.parametrize( ("search_query", "expected_text"), [ - ("", "OWASP Sponsors Information"), + ("", "OWASP Sponsors Information - Search results for: diamond"), ("diamond", "OWASP Sponsors Information - Search results for: diamond"), - ("platinum", "OWASP Sponsors Information - Search results for: platinum"), + ("platinum", "OWASP Sponsors Information - Search results for: diamond"), ], ) def test_sponsors_handler_fallback_text( From c004ae2ea439df70fff897df2fde83b4319b05b7 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 21 Feb 2025 00:54:45 +0530 Subject: [PATCH 09/25] fixed makefile --- backend/Makefile | 8 ++++++-- .../{load_sponsor_data.py => owasp_update_sponsors.py} | 0 2 files changed, 6 insertions(+), 2 deletions(-) rename backend/apps/owasp/management/commands/{load_sponsor_data.py => owasp_update_sponsors.py} (100%) diff --git a/backend/Makefile b/backend/Makefile index 4537c4c706..a3d1f31cc5 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -44,7 +44,6 @@ index-data: load-data: @echo "Loading Nest data" @CMD="poetry run python manage.py load_data" $(MAKE) exec-backend-command - @CMD="poetry run python manage.py load_sponsor_data" $(MAKE) exec-backend-command merge-migrations: @CMD="poetry run python manage.py makemigrations --merge" $(MAKE) exec-backend-command @@ -83,6 +82,10 @@ owasp-scrape-projects: @echo "Scraping OWASP site projects data" @CMD="poetry run python manage.py owasp_scrape_projects" $(MAKE) exec-backend-command +owasp-update-sponsors: + @echo "Getting OWASP sponsors data" + @CMD="poetry run python manage.py owasp_update_sponsors" $(MAKE) exec-backend-command + poetry-update: @CMD="poetry update" $(MAKE) exec-backend-command @@ -113,4 +116,5 @@ update-data: \ owasp-scrape-committees \ owasp-scrape-projects \ github-update-project-related-repositories \ - owasp-aggregate-projects + owasp-aggregate-projects \ + owasp-update-sponsors diff --git a/backend/apps/owasp/management/commands/load_sponsor_data.py b/backend/apps/owasp/management/commands/owasp_update_sponsors.py similarity index 100% rename from backend/apps/owasp/management/commands/load_sponsor_data.py rename to backend/apps/owasp/management/commands/owasp_update_sponsors.py From 874aedc1d288ed244d3e651a4daf16e71d87757a Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Sun, 23 Feb 2025 03:11:28 +0530 Subject: [PATCH 10/25] pre-commit after resolve conflict --- backend/apps/owasp/admin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index 6a8a2f65ed..ccdf9258b3 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -7,8 +7,8 @@ from apps.owasp.models.committee import Committee from apps.owasp.models.event import Event from apps.owasp.models.project import Project -from apps.owasp.models.sponsor import Sponsor from apps.owasp.models.snapshot import Snapshot +from apps.owasp.models.sponsor import Sponsor class GenericEntityAdminMixin: @@ -102,7 +102,7 @@ def custom_field_name(self, obj): custom_field_name.short_description = "Name" - + class SnapshotAdmin(admin.ModelAdmin): autocomplete_fields = ( "new_chapters", @@ -128,8 +128,8 @@ class SnapshotAdmin(admin.ModelAdmin): "status", "error_message", ) - - + + class SponsorAdmin(admin.ModelAdmin): """Admin configuration for Sponsor model.""" @@ -159,9 +159,10 @@ class SponsorAdmin(admin.ModelAdmin): ("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(Snapshot, SnapshotAdmin) -admin.site.register(Sponsor, SponsorAdmin) \ No newline at end of file +admin.site.register(Sponsor, SponsorAdmin) From 354149f7c01410c79c4824cec5fb21de0c19277d Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Sun, 23 Feb 2025 03:25:50 +0530 Subject: [PATCH 11/25] migrations --- .../{0015_sponsor.py => 0016_sponsor.py} | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) rename backend/apps/owasp/migrations/{0015_sponsor.py => 0016_sponsor.py} (80%) diff --git a/backend/apps/owasp/migrations/0015_sponsor.py b/backend/apps/owasp/migrations/0016_sponsor.py similarity index 80% rename from backend/apps/owasp/migrations/0015_sponsor.py rename to backend/apps/owasp/migrations/0016_sponsor.py index b9dc835938..104f70fa7e 100644 --- a/backend/apps/owasp/migrations/0015_sponsor.py +++ b/backend/apps/owasp/migrations/0016_sponsor.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.5 on 2025-01-28 11:40 +# Generated by Django 5.1.5 on 2025-02-22 21:48 from django.db import migrations, models @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("owasp", "0014_project_custom_tags"), + ("owasp", "0015_snapshot"), ] operations = [ @@ -17,14 +17,23 @@ class Migration(migrations.Migration): ( "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + 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)), ("name", models.CharField(max_length=255, verbose_name="Name")), - ("sort_name", models.CharField(max_length=255, verbose_name="Sort Name")), - ("description", models.TextField(blank=True, verbose_name="Description")), + ( + "sort_name", + models.CharField(max_length=255, verbose_name="Sort Name"), + ), + ( + "description", + models.TextField(blank=True, verbose_name="Description"), + ), ("url", models.URLField(blank=True, verbose_name="Website URL")), ("job_url", models.URLField(blank=True, verbose_name="Job URL")), ( From 3346debe024bc0f746970d977e828b4d74611e15 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Mon, 3 Mar 2025 22:52:47 +0530 Subject: [PATCH 12/25] pre-commmit --- backend/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Makefile b/backend/Makefile index 4768bfa45e..7731affb5a 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -101,7 +101,7 @@ owasp-scrape-projects: 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 From 689d64123af90285ff31fdd92a0e667ae47c7fb6 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Tue, 4 Mar 2025 10:39:46 +0530 Subject: [PATCH 13/25] fixed sonaQube warning --- backend/tests/slack/commands/sponsors_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/slack/commands/sponsors_test.py b/backend/tests/slack/commands/sponsors_test.py index d5f58822b1..1263cca13f 100644 --- a/backend/tests/slack/commands/sponsors_test.py +++ b/backend/tests/slack/commands/sponsors_test.py @@ -14,7 +14,7 @@ @pytest.fixture(autouse=True) def mock_get_absolute_url(): with patch("apps.common.utils.get_absolute_url") as mock: - mock.return_value = "http://example.com" + mock.return_value = "https://example.com" yield mock From ad4b8423580c5834541eb3bd072455c2d3f36083 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Tue, 4 Mar 2025 16:09:16 +0530 Subject: [PATCH 14/25] refractor code removed algolia --- backend/apps/owasp/admin.py | 2 +- backend/apps/owasp/api/search/sponsor.py | 31 ---- backend/apps/owasp/index/__init__.py | 1 - backend/apps/owasp/index/sponsor.py | 59 -------- .../commands/owasp_update_sponsors.py | 33 ++--- .../{0016_sponsor.py => 0022_sponsor.py} | 45 +++--- backend/apps/owasp/models/mixins/sponsor.py | 52 ------- backend/apps/owasp/models/sponsor.py | 92 ------------ backend/apps/owasp/models/sponsors.py | 135 ++++++++++++++++++ backend/apps/slack/commands/sponsors.py | 51 ++++--- backend/apps/slack/common/handlers/sponsor.py | 97 ------------- backend/apps/slack/utils.py | 16 ++- .../tests/owasp/api/search/sponsor_test.py | 92 ------------ .../{sponsor_test.py => sponsors_test.py} | 3 +- 14 files changed, 218 insertions(+), 491 deletions(-) delete mode 100644 backend/apps/owasp/api/search/sponsor.py delete mode 100644 backend/apps/owasp/index/sponsor.py rename backend/apps/owasp/migrations/{0016_sponsor.py => 0022_sponsor.py} (69%) delete mode 100644 backend/apps/owasp/models/mixins/sponsor.py delete mode 100644 backend/apps/owasp/models/sponsor.py create mode 100644 backend/apps/owasp/models/sponsors.py delete mode 100644 backend/apps/slack/common/handlers/sponsor.py delete mode 100644 backend/tests/owasp/api/search/sponsor_test.py rename backend/tests/owasp/models/{sponsor_test.py => sponsors_test.py} (98%) diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index e008d9f795..bdda36b04a 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -10,7 +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.sponsor import Sponsor +from apps.owasp.models.sponsors import Sponsor class GenericEntityAdminMixin: diff --git a/backend/apps/owasp/api/search/sponsor.py b/backend/apps/owasp/api/search/sponsor.py deleted file mode 100644 index f449ecc384..0000000000 --- a/backend/apps/owasp/api/search/sponsor.py +++ /dev/null @@ -1,31 +0,0 @@ -"""OWASP app sponsor search API.""" - -from algoliasearch_django import raw_search - -from apps.owasp.models.sponsor import Sponsor - - -def get_sponsors(query, attributes=None, limit=25, page=1): - """Return sponsors relevant to a search query.""" - params = { - "attributesToHighlight": [], - "attributesToRetrieve": attributes - or [ - "idx_name", - "idx_sort_name", - "idx_description", - "idx_url", - "idx_job_url", - "idx_image_path", - "idx_member_type", - "idx_sponsor_type", - "idx_is_member", - ], - "hitsPerPage": limit, - "minProximity": 4, - "page": page - 1, - "typoTolerance": "min", - "facetFilters": [], - } - - return raw_search(Sponsor, query, params) diff --git a/backend/apps/owasp/index/__init__.py b/backend/apps/owasp/index/__init__.py index 9f84906e9e..f74fb24491 100644 --- a/backend/apps/owasp/index/__init__.py +++ b/backend/apps/owasp/index/__init__.py @@ -3,4 +3,3 @@ from apps.owasp.index.chapter import ChapterIndex from apps.owasp.index.committee import CommitteeIndex from apps.owasp.index.project import ProjectIndex -from apps.owasp.index.sponsor import SponsorIndex diff --git a/backend/apps/owasp/index/sponsor.py b/backend/apps/owasp/index/sponsor.py deleted file mode 100644 index 7e3af6068c..0000000000 --- a/backend/apps/owasp/index/sponsor.py +++ /dev/null @@ -1,59 +0,0 @@ -"""OWASP app sponsor index.""" - -from algoliasearch_django import AlgoliaIndex -from algoliasearch_django.decorators import register - -from apps.common.index import IS_LOCAL_BUILD, LOCAL_INDEX_LIMIT -from apps.owasp.models.sponsor import Sponsor - - -@register(Sponsor) -class SponsorIndex(AlgoliaIndex): - """Sponsor index.""" - - index_name = "sponsors" - fields = ( - "idx_name", - "idx_sort_name", - "idx_description", - "idx_url", - "idx_job_url", - "idx_image_path", - "idx_member_type", - "idx_sponsor_type", - "idx_is_member", - ) - settings = { - "attributesForFaceting": [ - "filterOnly(idx_name)", - "filterOnly(idx_sort_name)", - "idx_member_type", - "idx_sponsor_type", - "idx_is_member", - ], - "indexLanguages": ["en"], - "customRanking": [ - "asc(idx_sort_name)", - ], - "ranking": [ - "typo", - "words", - "filters", - "proximity", - "attribute", - "exact", - "custom", - ], - "searchableAttributes": [ - "unordered(idx_name)", - "unordered(idx_sort_name)", - "unordered(idx_member_type)", - "unordered(idx_sponsor_type)", - ], - } - should_index = "is_indexable" - - def get_queryset(self): - """Get queryset.""" - qs = Sponsor.objects.all() - return qs[:LOCAL_INDEX_LIMIT] if IS_LOCAL_BUILD else qs diff --git a/backend/apps/owasp/management/commands/owasp_update_sponsors.py b/backend/apps/owasp/management/commands/owasp_update_sponsors.py index b14d80cd47..0d9dfb96f2 100644 --- a/backend/apps/owasp/management/commands/owasp_update_sponsors.py +++ b/backend/apps/owasp/management/commands/owasp_update_sponsors.py @@ -3,33 +3,18 @@ import yaml from django.core.management.base import BaseCommand -from apps.github.utils import get_repository_file_content, normalize_url -from apps.owasp.models.sponsor import Sponsor +from apps.github.utils import get_repository_file_content +from apps.owasp.models.sponsors import Sponsor class Command(BaseCommand): - help = "Import sponsors from OWASP GitHub repository" + help = "Import sponsors from the provided YAML file" def handle(self, *args, **kwargs): - url = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/corp_members.yml" - yaml_content = get_repository_file_content(url).replace("\t", " ") - data = yaml.safe_load(yaml_content) + data = yaml.safe_load( + get_repository_file_content( + "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/corp_members.yml" + ).expandtabs() + ) - for entry in data: - fields = { - "name": entry.get("name", ""), - "sort_name": entry.get("sortname", "").capitalize(), - "description": entry.get("description", ""), - "url": normalize_url(entry.get("url", "")) or "", - "job_url": normalize_url(entry.get("job_url", "")) or "", - "image_path": entry.get("image", ""), - "is_member": entry.get("member", False), - "member_type": entry.get("membertype", "4") or "4", - "sponsor_type": entry.get("sponsor", "-1") or "-1", - } - - sponsor = Sponsor(**fields) - sponsor.save() - self.stdout.write(self.style.SUCCESS(f"Successfully imported sponsor: {sponsor.name}")) - - self.stdout.write(self.style.SUCCESS("Finished importing sponsors")) + Sponsor.bulk_save([Sponsor.update_data(sponsor_data) for sponsor_data in data]) diff --git a/backend/apps/owasp/migrations/0016_sponsor.py b/backend/apps/owasp/migrations/0022_sponsor.py similarity index 69% rename from backend/apps/owasp/migrations/0016_sponsor.py rename to backend/apps/owasp/migrations/0022_sponsor.py index 104f70fa7e..78c2449263 100644 --- a/backend/apps/owasp/migrations/0016_sponsor.py +++ b/backend/apps/owasp/migrations/0022_sponsor.py @@ -1,13 +1,11 @@ -# Generated by Django 5.1.5 on 2025-02-22 21:48 +# Generated by Django 5.1.5 on 2025-03-04 10:37 from django.db import migrations, models -import apps.owasp.models.mixins.sponsor - class Migration(migrations.Migration): dependencies = [ - ("owasp", "0015_snapshot"), + ("owasp", "0021_alter_snapshot_key"), ] operations = [ @@ -25,15 +23,19 @@ class Migration(migrations.Migration): ), ("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"), ), - ( - "description", - models.TextField(blank=True, verbose_name="Description"), - ), ("url", models.URLField(blank=True, verbose_name="Website URL")), ("job_url", models.URLField(blank=True, verbose_name="Job URL")), ( @@ -48,9 +50,13 @@ class Migration(migrations.Migration): "member_type", models.CharField( blank=True, - choices=[("2", "Platinum"), ("3", "Gold"), ("4", "Silver")], - default="4", - max_length=2, + choices=[ + ("Platinum", "Platinum"), + ("Gold", "Gold"), + ("Silver", "Silver"), + ], + default="Silver", + max_length=20, verbose_name="Member Type", ), ), @@ -58,15 +64,15 @@ class Migration(migrations.Migration): "sponsor_type", models.CharField( choices=[ - ("1", "Diamond"), - ("2", "Platinum"), - ("3", "Gold"), - ("4", "Silver"), - ("5", "Supporter"), - ("-1", "Not a Sponsor"), + ("Diamond", "Diamond"), + ("Platinum", "Platinum"), + ("Gold", "Gold"), + ("Silver", "Silver"), + ("Supporter", "Supporter"), + ("Not a Sponsor", "Not Sponsor"), ], - default="-1", - max_length=2, + default="Not a Sponsor", + max_length=20, verbose_name="Sponsor Type", ), ), @@ -75,6 +81,5 @@ class Migration(migrations.Migration): "verbose_name_plural": "Sponsors", "db_table": "owasp_sponsors", }, - bases=(apps.owasp.models.mixins.sponsor.SponsorIndexMixin, models.Model), ), ] diff --git a/backend/apps/owasp/models/mixins/sponsor.py b/backend/apps/owasp/models/mixins/sponsor.py deleted file mode 100644 index 2da18a598d..0000000000 --- a/backend/apps/owasp/models/mixins/sponsor.py +++ /dev/null @@ -1,52 +0,0 @@ -"""OWASP app sponsor mixins.""" - -from apps.owasp.models.mixins.common import RepositoryBasedEntityModelMixin - - -class SponsorIndexMixin(RepositoryBasedEntityModelMixin): - """Sponsor index mixin.""" - - @property - def idx_created_at(self): - """Get created timestamp for index.""" - return self.nest_created_at - - @property - def idx_updated_at(self): - """Get updated timestamp for index.""" - return self.nest_updated_at - - @property - def idx_sort_name(self): - """Get sort name for index.""" - return self.sort_name - - @property - def idx_url(self): - """Get URL for index.""" - return self.url - - @property - def idx_job_url(self): - """Get job URL for index.""" - return self.job_url - - @property - def idx_image_path(self): - """Get image path for index.""" - return self.image_path - - @property - def idx_member_type(self): - """Get member type for index.""" - return self.readable_member_type - - @property - def idx_sponsor_type(self): - """Get sponsor type for index.""" - return self.readable_sponsor_type - - @property - def idx_is_member(self): - """Get member status for index.""" - return self.is_member diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py deleted file mode 100644 index 2fba386e9f..0000000000 --- a/backend/apps/owasp/models/sponsor.py +++ /dev/null @@ -1,92 +0,0 @@ -"""OWASP app sponsor models.""" - -from django.db import models - -from apps.common.models import BulkSaveModel, TimestampedModel -from apps.owasp.models.mixins.sponsor import SponsorIndexMixin - - -class Sponsor(BulkSaveModel, SponsorIndexMixin, TimestampedModel): - """Sponsor model.""" - - objects = models.Manager() - - class Meta: - db_table = "owasp_sponsors" - verbose_name_plural = "Sponsors" - - class SponsorType(models.TextChoices): - DIAMOND = "1", "Diamond" - PLATINUM = "2", "Platinum" - GOLD = "3", "Gold" - SILVER = "4", "Silver" - SUPPORTER = "5", "Supporter" - NOT_SPONSOR = "-1", "Not a Sponsor" - - class MemberType(models.TextChoices): - PLATINUM = "2", "Platinum" - GOLD = "3", "Gold" - SILVER = "4", "Silver" - - # Basic information - name = models.CharField(verbose_name="Name", max_length=255) - sort_name = models.CharField(verbose_name="Sort Name", max_length=255) - description = models.TextField(verbose_name="Description", blank=True) - - # URLs and images - url = models.URLField(verbose_name="Website URL", blank=True) - job_url = models.URLField(verbose_name="Job URL", blank=True) - image_path = 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=2, - choices=MemberType.choices, - default=MemberType.SILVER, - blank=True, - ) - sponsor_type = models.CharField( - verbose_name="Sponsor Type", - max_length=2, - choices=SponsorType.choices, - default=SponsorType.NOT_SPONSOR, - ) - - def __str__(self): - """Sponsor human readable representation.""" - return f"{self.name}" - - @property - def readable_sponsor_type(self): - """Get human-readable sponsor type.""" - return self.SponsorType(str(self.sponsor_type)).label - - @property - def readable_member_type(self): - """Get human-readable member type.""" - return self.MemberType(str(self.member_type)).label - - @property - def is_indexable(self): - """Determine if the sponsor should be indexed in Algolia.""" - return True - - @staticmethod - def bulk_save(sponsors, fields=None): - """Bulk save sponsors.""" - BulkSaveModel.bulk_save(Sponsor, sponsors, fields=fields) - - @staticmethod - def update_data(sponsor_id, **kwargs): - """Update sponsor data.""" - try: - sponsor = Sponsor.objects.get(id=sponsor_id) - for key, value in kwargs.items(): - setattr(sponsor, key, value) - sponsor.save() - except Sponsor.DoesNotExist: - return None - else: - return sponsor diff --git a/backend/apps/owasp/models/sponsors.py b/backend/apps/owasp/models/sponsors.py new file mode 100644 index 0000000000..b1041dc5f9 --- /dev/null +++ b/backend/apps/owasp/models/sponsors.py @@ -0,0 +1,135 @@ +"""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 + + +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_path = 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, + ) + 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_sponsor_type(self): + """Get human-readable sponsor type.""" + return self.SponsorType(str(self.sponsor_type)).label + + @property + def readable_member_type(self): + """Get human-readable member type.""" + return self.MemberType(str(self.member_type)).label + + @property + def is_indexable(self): + """Determine if the sponsor should be indexed in Algolia.""" + return True + + @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.""" + 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, + } + + member_type_mapping = { + "2": self.MemberType.PLATINUM, + "3": self.MemberType.GOLD, + "4": self.MemberType.SILVER, + } + + sponsor_type_label = sponsor_type_mapping.get( + data.get("sponsor", "-1") or "-1", self.SponsorType.NOT_SPONSOR + ) + member_type_label = member_type_mapping.get( + data.get("membertype", "4") or "4", 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_path": data.get("image", ""), + "is_member": data.get("member", False), + "sponsor_type": sponsor_type_label, + "member_type": member_type_label, + } + + for key, value in fields.items(): + setattr(self, key, value) diff --git a/backend/apps/slack/commands/sponsors.py b/backend/apps/slack/commands/sponsors.py index 5485b66fb6..90229ed505 100644 --- a/backend/apps/slack/commands/sponsors.py +++ b/backend/apps/slack/commands/sponsors.py @@ -2,10 +2,11 @@ from django.conf import settings +from apps.common.constants import NL, OWASP_WEBSITE_URL from apps.slack.apps import SlackConfig -from apps.slack.common.handlers.sponsor import get_blocks -from apps.slack.common.presentation import EntityPresentation -from apps.slack.utils import get_text +from apps.slack.blocks import markdown +from apps.slack.constants import FEEDBACK_CHANNEL_MESSAGE +from apps.slack.utils import get_sponsors_data, get_text COMMAND = "/sponsors" @@ -16,23 +17,35 @@ def sponsors_handler(ack, command, client): if not settings.SLACK_COMMANDS_ENABLED: return - search_query = command["text"].strip() - blocks = get_blocks( - search_query=search_query, - limit=10, - presentation=EntityPresentation( - include_feedback=True, - include_metadata=True, - include_pagination=False, - include_timestamps=True, - name_truncation=80, - summary_truncation=300, - ), - ) + sponsors = get_sponsors_data() + if not sponsors: + client.chat_postMessage( + channel=command["user_id"], text="Failed to get OWASP sponsor data." + ) + return + + blocks = [] + blocks.append(markdown("*OWASP Sponsors:*")) + + for idx, sponsor in enumerate(sponsors, start=1): + if sponsor.url: + block_text = f"*{idx}. <{sponsor.url}|{sponsor.name}>*{NL}" + else: + block_text = f"*{idx}. {sponsor.name}*{NL}" - fallback_text = "OWASP Sponsors Information" - if search_query: - fallback_text += f" - Search results for: {search_query}" + block_text += f"Member Type: {sponsor.member_type}{NL}" + block_text += f"{sponsor.description}{NL}" + + blocks.append(markdown(block_text)) + + blocks.append({"type": "divider"}) + blocks.append( + markdown( + f"* Please visit the <{OWASP_WEBSITE_URL}/supporters|OWASP supporters>" + f" for more information about the sponsors*{NL}" + f"{FEEDBACK_CHANNEL_MESSAGE}" + ) + ) conversation = client.conversations_open(users=command["user_id"]) client.chat_postMessage( diff --git a/backend/apps/slack/common/handlers/sponsor.py b/backend/apps/slack/common/handlers/sponsor.py deleted file mode 100644 index eac62be6be..0000000000 --- a/backend/apps/slack/common/handlers/sponsor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Handler for OWASP Sponsors Slack functionality.""" - -from __future__ import annotations - -from django.utils.text import Truncator - -from apps.common.constants import NL, OWASP_WEBSITE_URL -from apps.slack.blocks import markdown -from apps.slack.common.constants import TRUNCATION_INDICATOR -from apps.slack.common.presentation import EntityPresentation -from apps.slack.constants import FEEDBACK_CHANNEL_MESSAGE -from apps.slack.utils import escape - - -def get_blocks( - page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None -): - """Get sponsors blocks.""" - from apps.owasp.api.search.sponsor import get_sponsors - - presentation = presentation or EntityPresentation() - search_query_escaped = escape(search_query) - - attributes = [ - "idx_name", - "idx_sort_name", - "idx_description", - "idx_url", - "idx_member_type", - "idx_sponsor_type", - "idx_is_member", - ] - - offset = (page - 1) * limit - sponsors_data = get_sponsors(search_query, attributes=attributes, limit=limit, page=page) - sponsors = sponsors_data["hits"] - - if not sponsors: - return [ - markdown( - f"*No sponsors found for `{search_query_escaped}`*{NL}" - if search_query - else f"*No sponsors found*{NL}" - ) - ] - - blocks = [ - markdown( - f"{NL}*OWASP sponsors that I found for* `{search_query_escaped}`:{NL}" - if search_query_escaped - else f"{NL}*OWASP sponsors:*{NL}" - ), - ] - - for idx, sponsor in enumerate(sponsors): - name = Truncator(escape(sponsor["idx_name"])).chars( - presentation.name_truncation, truncate=TRUNCATION_INDICATOR - ) - description = Truncator(sponsor["idx_description"]).chars( - presentation.summary_truncation, truncate=TRUNCATION_INDICATOR - ) - - member_type = sponsor.get("idx_member_type", "") - sponsor_type = sponsor.get("idx_sponsor_type", "") - is_member = sponsor.get("idx_is_member", False) - - metadata_text = [] - if member_type and presentation.include_metadata: - metadata_text.append(f"Member Type: {member_type}") - if sponsor_type and presentation.include_metadata: - metadata_text.append(f"Sponsor Type: {sponsor_type}") - - metadata_line = ( - f"_{' | '.join(metadata_text)}_{NL}" - if metadata_text and presentation.include_metadata - else "" - ) - - blocks.append( - markdown( - f"{offset + idx + 1}. <{sponsor['idx_url']}|*{name}*>" - f"{' (Corporate Sponsor)' if is_member else ''}{NL}" - f"{metadata_line}" - f"{escape(description)}{NL}" - ) - ) - - if presentation.include_feedback: - blocks.append( - markdown( - f"*Please visit the <{OWASP_WEBSITE_URL}/supporters|OWASP supporters>" - f" for more information about the sponsors*{NL}" - f"{FEEDBACK_CHANNEL_MESSAGE}" - ) - ) - - return blocks diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 84ddbed382..35198ee27c 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -85,7 +85,7 @@ def get_staff_data(timeout=30): def get_events_data(): - """Get raw events data via GraphQL.""" + """Get raw events data via Database.""" from apps.owasp.models.event import Event try: @@ -95,6 +95,20 @@ def get_events_data(): return None +MAX_SPONSORS = 10 + + +def get_sponsors_data(): + """Get raw sponsors data via Database.""" + from apps.owasp.models.sponsors import Sponsor + + try: + return Sponsor.objects.all()[:MAX_SPONSORS] + except Exception as e: + logger.exception("Failed to fetch sponsors data via database", extra={"error": str(e)}) + return None + + def get_text(blocks): """Convert blocks to plain text.""" text = [] diff --git a/backend/tests/owasp/api/search/sponsor_test.py b/backend/tests/owasp/api/search/sponsor_test.py deleted file mode 100644 index 6bd72bee9f..0000000000 --- a/backend/tests/owasp/api/search/sponsor_test.py +++ /dev/null @@ -1,92 +0,0 @@ -from unittest.mock import patch - -import pytest - -from apps.owasp.api.search.sponsor import get_sponsors - -DEFAULT_HITS_PER_PAGE = 25 -DEFAULT_MIN_PROXIMITY = 4 - - -MOCKED_SPONSOR_HITS = { - "hits": [ - { - "idx_name": "Example Corp", - "idx_sort_name": "EXAMPLE CORP", - "idx_description": "A leading security company", - "idx_url": "https://example.com", - "idx_job_url": "https://example.com/careers", - "idx_image_path": "/assets/images/sponsors/example.png", - "idx_member_type": "Corporate", - "idx_sponsor_type": "Gold", - "idx_is_member": True, - }, - { - "idx_name": "Security Plus", - "idx_sort_name": "SECURITY PLUS", - "idx_description": "Cybersecurity solutions provider", - "idx_url": "https://securityplus.com", - "idx_job_url": "https://securityplus.com/jobs", - "idx_image_path": "/assets/images/sponsors/secplus.png", - "idx_member_type": "Corporate", - "idx_sponsor_type": "Silver", - "idx_is_member": True, - }, - ], - "nbPages": 3, -} - - -@pytest.mark.parametrize( - ("query", "limit", "page", "expected_hits"), - [ - ("security", 25, 1, MOCKED_SPONSOR_HITS), - ("example", 10, 2, MOCKED_SPONSOR_HITS), - ("", 25, 1, MOCKED_SPONSOR_HITS), - ], -) -def test_get_sponsors_basic_search(query, limit, page, expected_hits): - """Test basic sponsor search with different queries and pagination.""" - with patch( - "apps.owasp.api.search.sponsor.raw_search", return_value=expected_hits - ) as mock_raw_search: - result = get_sponsors(query, limit=limit, page=page) - - assert result == expected_hits - - mock_raw_search.assert_called_once() - _, call_query, call_params = mock_raw_search.call_args[0] - - assert call_query == query - assert call_params["hitsPerPage"] == limit - assert call_params["page"] == page - 1 - - -def test_get_sponsors_custom_attributes(): - """Test sponsor search with custom attributes to retrieve.""" - custom_attributes = ["idx_name", "idx_url"] - - with patch( - "apps.owasp.api.search.sponsor.raw_search", return_value=MOCKED_SPONSOR_HITS - ) as mock_raw_search: - # Call function without storing unused result - get_sponsors("test", attributes=custom_attributes) - - _, _, call_params = mock_raw_search.call_args[0] - assert call_params["attributesToRetrieve"] == custom_attributes - - -def test_get_sponsors_default_parameters(): - """Test sponsor search with default parameters.""" - with patch( - "apps.owasp.api.search.sponsor.raw_search", return_value=MOCKED_SPONSOR_HITS - ) as mock_raw_search: - get_sponsors("test") - - _, _, call_params = mock_raw_search.call_args[0] - assert call_params["hitsPerPage"] == DEFAULT_HITS_PER_PAGE - assert call_params["page"] == 0 - assert call_params["minProximity"] == DEFAULT_MIN_PROXIMITY - assert call_params["typoTolerance"] == "min" - assert isinstance(call_params["attributesToRetrieve"], list) - assert len(call_params["attributesToRetrieve"]) > 0 diff --git a/backend/tests/owasp/models/sponsor_test.py b/backend/tests/owasp/models/sponsors_test.py similarity index 98% rename from backend/tests/owasp/models/sponsor_test.py rename to backend/tests/owasp/models/sponsors_test.py index 6bcce81b33..d2bca01215 100644 --- a/backend/tests/owasp/models/sponsor_test.py +++ b/backend/tests/owasp/models/sponsors_test.py @@ -1,10 +1,9 @@ from unittest.mock import Mock, patch import pytest +from backend.apps.owasp.models.sponsors import Sponsor from django.core.exceptions import ValidationError -from apps.owasp.models.sponsor import Sponsor - class TestSponsorModel: @pytest.mark.parametrize( From 2afa6e3c6fbc17999cbc3b756a6cfd1041586a45 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Tue, 4 Mar 2025 17:24:56 +0530 Subject: [PATCH 15/25] updated testcase --- backend/tests/owasp/models/sponsors_test.py | 92 +++----- backend/tests/slack/commands/sponsors_test.py | 209 +++++++----------- 2 files changed, 110 insertions(+), 191 deletions(-) diff --git a/backend/tests/owasp/models/sponsors_test.py b/backend/tests/owasp/models/sponsors_test.py index d2bca01215..7b958e2c53 100644 --- a/backend/tests/owasp/models/sponsors_test.py +++ b/backend/tests/owasp/models/sponsors_test.py @@ -1,8 +1,8 @@ from unittest.mock import Mock, patch import pytest -from backend.apps.owasp.models.sponsors import Sponsor -from django.core.exceptions import ValidationError + +from apps.owasp.models.sponsors import Sponsor class TestSponsorModel: @@ -21,12 +21,12 @@ def test_str_representation(self, name, expected_str): @pytest.mark.parametrize( ("sponsor_type", "expected_label"), [ - ("1", "Diamond"), - ("2", "Platinum"), - ("3", "Gold"), - ("4", "Silver"), - ("5", "Supporter"), - ("-1", "Not a Sponsor"), + ("Diamond", "Diamond"), + ("Platinum", "Platinum"), + ("Gold", "Gold"), + ("Silver", "Silver"), + ("Supporter", "Supporter"), + ("Not a Sponsor", "Not Sponsor"), ], ) def test_readable_sponsor_type(self, sponsor_type, expected_label): @@ -37,9 +37,9 @@ def test_readable_sponsor_type(self, sponsor_type, expected_label): @pytest.mark.parametrize( ("member_type", "expected_label"), [ - ("2", "Platinum"), - ("3", "Gold"), - ("4", "Silver"), + ("Platinum", "Platinum"), + ("Gold", "Gold"), + ("Silver", "Silver"), ], ) def test_readable_member_type(self, member_type, expected_label): @@ -55,66 +55,34 @@ def test_is_indexable(self): def test_bulk_save(self): """Test the bulk_save method.""" mock_sponsors = [Mock(id=None), Mock(id=1)] - with patch("apps.owasp.models.sponsor.BulkSaveModel.bulk_save") as mock_bulk_save: + with patch("apps.owasp.models.sponsors.BulkSaveModel.bulk_save") as mock_bulk_save: Sponsor.bulk_save(mock_sponsors, fields=["name"]) mock_bulk_save.assert_called_once_with(Sponsor, mock_sponsors, fields=["name"]) - def test_update_data(self): - """Test the update_data method.""" - mock_sponsor = Mock() - mock_sponsor.id = 1 - mock_sponsor.name = "Old Name" - mock_sponsor.sponsor_type = "1" - - with patch("apps.owasp.models.sponsor.Sponsor.objects.get", return_value=mock_sponsor): - updated_sponsor = Sponsor.update_data( - mock_sponsor.id, name="New Name", sponsor_type="2" - ) - assert updated_sponsor.name == "New Name" - assert updated_sponsor.sponsor_type == "2" - - with patch( - "apps.owasp.models.sponsor.Sponsor.objects.get", side_effect=Sponsor.DoesNotExist - ): - non_existent_sponsor = Sponsor.update_data(9999, name="New Name") - assert non_existent_sponsor is None - @pytest.mark.parametrize( - ("url", "is_valid"), + ("sponsor_type_value", "expected_sponsor_type"), [ - ("https://example.com", True), - ("invalid-url", False), + ("1", "Diamond"), + ("2", "Platinum"), + ("-1", "Not a Sponsor"), ], ) - def test_sponsor_validation(self, url, is_valid): - """Test validation of Sponsor fields.""" - sponsor = Sponsor( - name="Test Sponsor", - sort_name="Sponsor", - description="Test Description", - url=url, - job_url="https://jobs.example.com", - image_path="/images/test.png", - is_member=True, - member_type="2", - sponsor_type="1", - ) - - if is_valid: - sponsor.full_clean() - else: - with pytest.raises(ValidationError): - sponsor.full_clean() + def test_from_dict_sponsor_type_mapping(self, sponsor_type_value, expected_sponsor_type): + """Test the from_dict method for sponsor_type mapping.""" + sponsor = Sponsor() + sponsor.from_dict({"sponsor": sponsor_type_value}) + assert sponsor.sponsor_type == expected_sponsor_type @pytest.mark.parametrize( - ("sponsor_type", "expected_sponsor_type"), + ("member_type_value", "expected_member_type"), [ - ("1", "1"), # DIAMOND - ("2", "2"), # PLATINUM - ("-1", "-1"), # NOT_SPONSOR + ("2", "Platinum"), # "2" maps to "Platinum" + ("3", "Gold"), # "3" maps to "Gold" + ("4", "Silver"), # "4" maps to "Silver" ], ) - def test_sponsor_type_default(self, sponsor_type, expected_sponsor_type): - """Test the default sponsor_type behavior.""" - sponsor = Sponsor(sponsor_type=sponsor_type) - assert sponsor.sponsor_type == expected_sponsor_type + def test_from_dict_member_type_mapping(self, member_type_value, expected_member_type): + """Test the from_dict method for member_type mapping.""" + sponsor = Sponsor() + sponsor.from_dict({"membertype": member_type_value}) + assert sponsor.member_type == expected_member_type diff --git a/backend/tests/slack/commands/sponsors_test.py b/backend/tests/slack/commands/sponsors_test.py index 1263cca13f..f464b1ee8b 100644 --- a/backend/tests/slack/commands/sponsors_test.py +++ b/backend/tests/slack/commands/sponsors_test.py @@ -1,167 +1,118 @@ -from unittest.mock import Mock, patch +"""Test sponsors command handler.""" + +from unittest.mock import MagicMock, patch import pytest from django.conf import settings from apps.slack.commands.sponsors import sponsors_handler -from apps.slack.common.presentation import EntityPresentation - -# Constants for truncation values -NAME_TRUNCATION_LIMIT = 80 -SUMMARY_TRUNCATION_LIMIT = 300 -@pytest.fixture(autouse=True) -def mock_get_absolute_url(): - with patch("apps.common.utils.get_absolute_url") as mock: - mock.return_value = "https://example.com" - yield mock +class MockSponsor: + def __init__(self, name, member_type, description, url): + self.name = name + self.member_type = member_type + self.description = description + self.url = url -@pytest.fixture(autouse=True) -def mock_sponsors_count(): - with patch("apps.owasp.models.sponsor.Sponsor.objects.count") as mock: - mock.return_value = 50 - yield mock +mock_sponsors = [ + MockSponsor( + name="Example Sponsor 1", + member_type="Platinum", + description="A top-tier sponsor.", + url="https://example.com/sponsor1", + ), + MockSponsor( + name="Example Sponsor 2", + member_type="Gold", + description="A mid-tier sponsor.", + url="https://example.com/sponsor2", + ), +] class TestSponsorsHandler: + """Test sponsors command handler.""" + @pytest.fixture() - def mock_command(self): + def mock_slack_command(self): return { - "text": "", "user_id": "U123456", } @pytest.fixture() - def mock_client(self): - client = Mock() + def mock_slack_client(self): + client = MagicMock() client.conversations_open.return_value = {"channel": {"id": "C123456"}} return client - @pytest.fixture() - def mock_ack(self): - return Mock() - - @pytest.fixture(autouse=True) - def mock_get_blocks(self): - with patch("apps.slack.commands.sponsors.get_blocks") as mock: - mock.return_value = [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "OWASP Sponsors Information - Search results for: diamond", - }, - } - ] - yield mock - @pytest.mark.parametrize( - ("commands_enabled", "search_query", "expected_calls"), + ("commands_enabled", "has_sponsors_data", "expected_header"), [ - (True, "", 1), - (True, "diamond", 1), - (True, "platinum", 1), - (False, "", 0), - (False, "search term", 0), + (False, True, None), + (True, True, "*OWASP Sponsors:*"), + (True, False, "*OWASP Sponsors:*"), ], ) - def test_sponsors_handler_command_states( - self, mock_client, mock_command, mock_ack, commands_enabled, search_query, expected_calls + @patch("apps.slack.commands.sponsors.get_sponsors_data") + def test_handler_responses( + self, + mock_get_sponsors_data, + commands_enabled, + has_sponsors_data, + expected_header, + mock_slack_client, + mock_slack_command, ): - """Test sponsor handler with different command states and search queries.""" + """Test handler responses.""" settings.SLACK_COMMANDS_ENABLED = commands_enabled - mock_command["text"] = search_query - - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + mock_get_sponsors_data.return_value = mock_sponsors if has_sponsors_data else [] - mock_ack.assert_called_once() + sponsors_handler(ack=MagicMock(), command=mock_slack_command, client=mock_slack_client) - assert mock_client.chat_postMessage.call_count == expected_calls - if expected_calls > 0: - mock_client.conversations_open.assert_called_once_with(users=mock_command["user_id"]) + if not commands_enabled: + mock_slack_client.conversations_open.assert_not_called() + mock_slack_client.chat_postMessage.assert_not_called() + return - @pytest.mark.parametrize( - ("search_query", "expected_text"), - [ - ("", "OWASP Sponsors Information - Search results for: diamond"), - ("diamond", "OWASP Sponsors Information - Search results for: diamond"), - ("platinum", "OWASP Sponsors Information - Search results for: diamond"), - ], - ) - def test_sponsors_handler_fallback_text( - self, mock_client, mock_command, mock_ack, search_query, expected_text - ): - """Test fallback text generation with different search queries.""" - settings.SLACK_COMMANDS_ENABLED = True - mock_command["text"] = search_query + if not has_sponsors_data: + mock_slack_client.chat_postMessage.assert_called_once_with( + channel=mock_slack_command["user_id"], + text="Failed to get OWASP sponsor data.", + ) + mock_slack_client.conversations_open.assert_not_called() + return - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + mock_slack_client.conversations_open.assert_called_once_with( + users=mock_slack_command["user_id"] + ) - mock_client.chat_postMessage.assert_called_once() - assert mock_client.chat_postMessage.call_args[1]["text"] == expected_text + blocks = mock_slack_client.chat_postMessage.call_args[1]["blocks"] - def test_sponsors_handler_presentation_config( - self, mock_client, mock_command, mock_ack, mock_get_blocks - ): - """Test that presentation configuration is correctly passed to get_blocks.""" - settings.SLACK_COMMANDS_ENABLED = True - mock_command["text"] = "test" - - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) - - mock_get_blocks.assert_called_once() - _, kwargs = mock_get_blocks.call_args - presentation = kwargs["presentation"] - assert isinstance(presentation, EntityPresentation) - assert presentation.include_feedback is True - assert presentation.include_metadata is True - assert presentation.include_pagination is False - assert presentation.include_timestamps is True - assert presentation.name_truncation == NAME_TRUNCATION_LIMIT - assert presentation.summary_truncation == SUMMARY_TRUNCATION_LIMIT - - def test_sponsors_handler_search_query_processing( - self, mock_client, mock_command, mock_ack, mock_get_blocks - ): - """Test search query processing and stripping.""" - settings.SLACK_COMMANDS_ENABLED = True - test_queries = [" test ", "test", " TEST ", "Test "] - - for query in test_queries: - mock_command["text"] = query - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) - - mock_get_blocks.assert_called_with( - search_query=query.strip(), - limit=10, - presentation=pytest.approx( - EntityPresentation( - include_feedback=True, - include_metadata=True, - include_pagination=False, - include_timestamps=True, - name_truncation=80, - summary_truncation=300, - ) - ), - ) + assert blocks[0]["text"]["text"] == expected_header - def test_sponsors_handler_channel_error(self, mock_client, mock_command, mock_ack): - """Test handling of channel opening errors.""" - settings.SLACK_COMMANDS_ENABLED = True - mock_client.conversations_open.side_effect = Exception("Channel error") + if has_sponsors_data: + current_block = 1 - with pytest.raises(Exception, match="Channel error"): - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + sponsor_block = blocks[current_block]["text"]["text"] + assert "*1. *" in sponsor_block + assert "Member Type: Platinum" in sponsor_block + assert "A top-tier sponsor." in sponsor_block + current_block += 1 - mock_client.chat_postMessage.assert_not_called() + sponsor_block = blocks[current_block]["text"]["text"] + assert "*2. *" in sponsor_block + assert "Member Type: Gold" in sponsor_block + assert "A mid-tier sponsor." in sponsor_block + current_block += 1 - def test_sponsors_handler_message_error(self, mock_client, mock_command, mock_ack): - """Test handling of message posting errors.""" - settings.SLACK_COMMANDS_ENABLED = True - mock_client.chat_postMessage.side_effect = Exception("Message error") + assert blocks[current_block]["type"] == "divider" + current_block += 1 - with pytest.raises(Exception, match="Message error"): - sponsors_handler(ack=mock_ack, command=mock_command, client=mock_client) + footer_block = blocks[current_block]["text"]["text"] + assert ( + "* Please visit the " + in footer_block + ) + assert "for more information about the sponsors" in footer_block From 818aa74e62abc8147d4190289dff44114dc800e9 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 00:33:17 +0530 Subject: [PATCH 16/25] added moving Logo compoennt --- backend/apps/owasp/admin.py | 2 +- backend/apps/owasp/constants.py | 1 + backend/apps/owasp/graphql/nodes/sponsors.py | 15 ++++++ .../apps/owasp/graphql/queries/__init__.py | 3 +- .../apps/owasp/graphql/queries/sponsors.py | 17 ++++++ backend/apps/owasp/migrations/0022_sponsor.py | 4 +- backend/apps/owasp/models/sponsors.py | 31 ++++++----- frontend/src/api/queries/homeQueries.ts | 4 ++ frontend/src/components/LoadingSpinner.tsx | 2 +- frontend/src/components/MovingLogo.tsx | 53 +++++++++++++++++++ frontend/src/components/MultiSearch.tsx | 2 +- frontend/src/pages/Home.tsx | 8 ++- frontend/src/types/home.ts | 6 +++ frontend/tailwind.config.js | 9 ++++ 14 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 backend/apps/owasp/graphql/nodes/sponsors.py create mode 100644 backend/apps/owasp/graphql/queries/sponsors.py create mode 100644 frontend/src/components/MovingLogo.tsx diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index bdda36b04a..a0e803cf99 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -167,7 +167,7 @@ class SponsorAdmin(admin.ModelAdmin): fieldsets = ( ("Basic Information", {"fields": ("name", "sort_name", "description")}), - ("URLs and Images", {"fields": ("url", "job_url", "image_path")}), + ("URLs and Images", {"fields": ("url", "job_url", "image_url")}), ("Status", {"fields": ("is_member", "member_type", "sponsor_type")}), ) diff --git a/backend/apps/owasp/constants.py b/backend/apps/owasp/constants.py index 3bc25f7556..07d1555015 100644 --- a/backend/apps/owasp/constants.py +++ b/backend/apps/owasp/constants.py @@ -1,3 +1,4 @@ """OWASP app constants.""" OWASP_ORGANIZATION_NAME = "OWASP" +OWASP_ORGANIZATION_DATA_URL = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main" diff --git a/backend/apps/owasp/graphql/nodes/sponsors.py b/backend/apps/owasp/graphql/nodes/sponsors.py new file mode 100644 index 0000000000..d099508b26 --- /dev/null +++ b/backend/apps/owasp/graphql/nodes/sponsors.py @@ -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", + ) diff --git a/backend/apps/owasp/graphql/queries/__init__.py b/backend/apps/owasp/graphql/queries/__init__.py index 871bd1c3dc..8a0614c2b6 100644 --- a/backend/apps/owasp/graphql/queries/__init__.py +++ b/backend/apps/owasp/graphql/queries/__init__.py @@ -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.""" diff --git a/backend/apps/owasp/graphql/queries/sponsors.py b/backend/apps/owasp/graphql/queries/sponsors.py new file mode 100644 index 0000000000..1a3a4333b4 --- /dev/null +++ b/backend/apps/owasp/graphql/queries/sponsors.py @@ -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() diff --git a/backend/apps/owasp/migrations/0022_sponsor.py b/backend/apps/owasp/migrations/0022_sponsor.py index 78c2449263..618c90eb40 100644 --- a/backend/apps/owasp/migrations/0022_sponsor.py +++ b/backend/apps/owasp/migrations/0022_sponsor.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.5 on 2025-03-04 10:37 +# Generated by Django 5.1.5 on 2025-03-04 15:28 from django.db import migrations, models @@ -39,7 +39,7 @@ class Migration(migrations.Migration): ("url", models.URLField(blank=True, verbose_name="Website URL")), ("job_url", models.URLField(blank=True, verbose_name="Job URL")), ( - "image_path", + "image_url", models.CharField(blank=True, max_length=255, verbose_name="Image Path"), ), ( diff --git a/backend/apps/owasp/models/sponsors.py b/backend/apps/owasp/models/sponsors.py index b1041dc5f9..824645e192 100644 --- a/backend/apps/owasp/models/sponsors.py +++ b/backend/apps/owasp/models/sponsors.py @@ -5,6 +5,7 @@ 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): @@ -38,7 +39,7 @@ class MemberType(models.TextChoices): # URLs and images url = models.URLField(verbose_name="Website URL", blank=True) job_url = models.URLField(verbose_name="Job URL", blank=True) - image_path = models.CharField(verbose_name="Image Path", max_length=255, 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) @@ -60,20 +61,15 @@ def __str__(self): """Sponsor human readable representation.""" return f"{self.name}" - @property - def readable_sponsor_type(self): - """Get human-readable sponsor type.""" - return self.SponsorType(str(self.sponsor_type)).label - @property def readable_member_type(self): """Get human-readable member type.""" return self.MemberType(str(self.member_type)).label @property - def is_indexable(self): - """Determine if the sponsor should be indexed in Algolia.""" - return True + def readable_sponsor_type(self): + """Get human-readable sponsor type.""" + return self.SponsorType(str(self.sponsor_type)).label @staticmethod def bulk_save(sponsors, fields=None): @@ -97,6 +93,15 @@ def update_data(data, save=True): 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}" + + 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, @@ -106,12 +111,6 @@ def from_dict(self, data): "-1": self.SponsorType.NOT_SPONSOR, } - member_type_mapping = { - "2": self.MemberType.PLATINUM, - "3": self.MemberType.GOLD, - "4": self.MemberType.SILVER, - } - sponsor_type_label = sponsor_type_mapping.get( data.get("sponsor", "-1") or "-1", self.SponsorType.NOT_SPONSOR ) @@ -125,7 +124,7 @@ def from_dict(self, data): "description": data.get("description", ""), "url": normalize_url(data.get("url", "")) or "", "job_url": normalize_url(data.get("job_url", "")) or "", - "image_path": data.get("image", ""), + "image_url": image_url, "is_member": data.get("member", False), "sponsor_type": sponsor_type_label, "member_type": member_type_label, diff --git a/frontend/src/api/queries/homeQueries.ts b/frontend/src/api/queries/homeQueries.ts index 958858a1d0..7ce9f02f9b 100644 --- a/frontend/src/api/queries/homeQueries.ts +++ b/frontend/src/api/queries/homeQueries.ts @@ -51,6 +51,10 @@ export const GET_MAIN_PAGE_DATA = gql` publishedAt tagName } + sponsors { + imageUrl + name + } statsOverview { activeChaptersStats activeProjectsStats diff --git a/frontend/src/components/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner.tsx index 0c0cddb2a1..6c49724cf1 100644 --- a/frontend/src/components/LoadingSpinner.tsx +++ b/frontend/src/components/LoadingSpinner.tsx @@ -8,7 +8,7 @@ const LoadingSpinner: React.FC = ({ imageUrl }) => { const dark = imageUrl.replace('white', 'black') return (
(null) + + useEffect(() => { + if (scrollerRef.current) { + const scrollContainer = scrollerRef.current + + // Duplicate the sponsors to create a seamless loop + scrollContainer.innerHTML += scrollContainer.innerHTML + } + }, [sponsors]) + + return ( +
+
+ {sponsors.map((sponsor, index) => ( +
+
+ {sponsor.imageUrl ? ( + {`${sponsor.name} + ) : ( +
+ )} +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/MultiSearch.tsx b/frontend/src/components/MultiSearch.tsx index f5969f1183..248883a7ae 100644 --- a/frontend/src/components/MultiSearch.tsx +++ b/frontend/src/components/MultiSearch.tsx @@ -205,7 +205,7 @@ const MultiSearchBar: React.FC = ({ )} ) : ( -
+
)} {showSuggestions && suggestions.length > 0 && (
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index b48b81b15c..e33f85af6c 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -21,6 +21,7 @@ import AnimatedCounter from 'components/AnimatedCounter' import ChapterMap from 'components/ChapterMap' import ItemCardList from 'components/ItemCardList' import LoadingSpinner from 'components/LoadingSpinner' +import MovingLogos from 'components/MovingLogo' import MultiSearchBar from 'components/MultiSearch' import SecondaryCard from 'components/SecondaryCard' import TopContributors from 'components/ToggleContributors' @@ -215,7 +216,12 @@ export default function Home() { )} />
-
+ + + + + +
{counterData.map((stat, index) => (
diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts index f7c374c3e1..ce61208b69 100644 --- a/frontend/src/types/home.ts +++ b/frontend/src/types/home.ts @@ -23,6 +23,7 @@ export type MainPageData = { repositoriesCount: number type: string }[] + sponsors: SponsorType[] statsOverview: { activeChaptersStats: number activeProjectsStats: number @@ -30,3 +31,8 @@ export type MainPageData = { countriesStats: number } } + +export type SponsorType = { + imageUrl: string + name: string +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 15a88b8ff9..1a3b50b460 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -53,6 +53,15 @@ export default { sm: 'calc(var(--radius) - 4px)', }, }, + keyframes: { + scroll: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-500%)' }, + }, + }, + animation: { + scroll: 'scroll 0.5s linear infinite', + }, }, darkMode: ['class'], plugins: [require('tailwindcss-animate')], From 7db1f02bf1ca131d1909cf68c562bfc775e92cf7 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 00:36:41 +0530 Subject: [PATCH 17/25] backned test fix --- backend/tests/owasp/models/sponsors_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/tests/owasp/models/sponsors_test.py b/backend/tests/owasp/models/sponsors_test.py index 7b958e2c53..cb7c5f73ad 100644 --- a/backend/tests/owasp/models/sponsors_test.py +++ b/backend/tests/owasp/models/sponsors_test.py @@ -47,11 +47,6 @@ def test_readable_member_type(self, member_type, expected_label): sponsor = Sponsor(member_type=member_type) assert sponsor.readable_member_type == expected_label - def test_is_indexable(self): - """Test the is_indexable property.""" - sponsor = Sponsor() - assert sponsor.is_indexable is True - def test_bulk_save(self): """Test the bulk_save method.""" mock_sponsors = [Mock(id=None), Mock(id=1)] From cfc555da3fcdb33a5c7260b3017af172e295f44f Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 00:45:27 +0530 Subject: [PATCH 18/25] frontend test case fix --- frontend/__tests__/unit/data/mockHomeData.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/__tests__/unit/data/mockHomeData.ts b/frontend/__tests__/unit/data/mockHomeData.ts index cb20a11867..16e4490084 100644 --- a/frontend/__tests__/unit/data/mockHomeData.ts +++ b/frontend/__tests__/unit/data/mockHomeData.ts @@ -9,6 +9,12 @@ export const mockGraphQLData = { repositoriesCount: 1, }, ], + sponsors: [ + { + name: 'OWASP Foundation', + imageUrl: 'https://example.com/owasp-foundation.png', + } + ], recentChapters: [ { name: 'OWASP Sivagangai', From 2c3eec31d0147f0a5eb8d6e203cc0cd94a2a3d13 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 00:48:11 +0530 Subject: [PATCH 19/25] pre-commit --- frontend/__tests__/unit/data/mockHomeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/__tests__/unit/data/mockHomeData.ts b/frontend/__tests__/unit/data/mockHomeData.ts index 16e4490084..69498aecd7 100644 --- a/frontend/__tests__/unit/data/mockHomeData.ts +++ b/frontend/__tests__/unit/data/mockHomeData.ts @@ -13,7 +13,7 @@ export const mockGraphQLData = { { name: 'OWASP Foundation', imageUrl: 'https://example.com/owasp-foundation.png', - } + }, ], recentChapters: [ { From 5d06b86abffc2447a08cc3264a3ad52e68a62206 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 01:13:13 +0530 Subject: [PATCH 20/25] fix e2e case --- frontend/__tests__/e2e/data/mockHomeData.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/__tests__/e2e/data/mockHomeData.ts b/frontend/__tests__/e2e/data/mockHomeData.ts index c1792e23af..73182d46fb 100644 --- a/frontend/__tests__/e2e/data/mockHomeData.ts +++ b/frontend/__tests__/e2e/data/mockHomeData.ts @@ -165,6 +165,12 @@ export const mockHomeData = { __typename: 'ReleaseNode', }, ], + sponsors: [ + { + name: 'Sponsor 1', + imageUrl: 'https://avatars.githubusercontent.com/u/1?v=4', + }, + ], statsOverview: { activeChaptersStats: 100, activeProjectsStats: 100, From b65d61005b8132cb0886a20c1e2d9af9cd1d783e Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 01:48:57 +0530 Subject: [PATCH 21/25] fix bug --- backend/apps/owasp/models/sponsors.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/apps/owasp/models/sponsors.py b/backend/apps/owasp/models/sponsors.py index 824645e192..82532f06a4 100644 --- a/backend/apps/owasp/models/sponsors.py +++ b/backend/apps/owasp/models/sponsors.py @@ -64,12 +64,12 @@ def __str__(self): @property def readable_member_type(self): """Get human-readable member type.""" - return self.MemberType(str(self.member_type)).label + return self.MemberType(self.member_type).label @property def readable_sponsor_type(self): """Get human-readable sponsor type.""" - return self.SponsorType(str(self.sponsor_type)).label + return self.SponsorType(self.sponsor_type).label @staticmethod def bulk_save(sponsors, fields=None): @@ -86,6 +86,7 @@ def update_data(data, save=True): sponsor = Sponsor(key=key) sponsor.from_dict(data) + if save: sponsor.save() @@ -96,6 +97,9 @@ def from_dict(self, data): image_path = data.get("image", "").lstrip("/") image_url = f"{OWASP_ORGANIZATION_DATA_URL}/{image_path}" + 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, @@ -111,12 +115,8 @@ def from_dict(self, data): "-1": self.SponsorType.NOT_SPONSOR, } - sponsor_type_label = sponsor_type_mapping.get( - data.get("sponsor", "-1") or "-1", self.SponsorType.NOT_SPONSOR - ) - member_type_label = member_type_mapping.get( - data.get("membertype", "4") or "4", self.MemberType.SILVER - ) + 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", ""), @@ -125,7 +125,7 @@ def from_dict(self, data): "url": normalize_url(data.get("url", "")) or "", "job_url": normalize_url(data.get("job_url", "")) or "", "image_url": image_url, - "is_member": data.get("member", False), + "is_member": bool(data.get("member", False)), "sponsor_type": sponsor_type_label, "member_type": member_type_label, } From fdda190bad2d6cc9b4fefb0f3eef56888cd603ec Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 03:32:31 +0530 Subject: [PATCH 22/25] improvements --- frontend/src/components/MovingLogo.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/MovingLogo.tsx b/frontend/src/components/MovingLogo.tsx index 35515200ba..6db42156fc 100644 --- a/frontend/src/components/MovingLogo.tsx +++ b/frontend/src/components/MovingLogo.tsx @@ -16,7 +16,6 @@ export default function MovingLogos({ sponsors }: MovingLogosProps) { if (scrollerRef.current) { const scrollContainer = scrollerRef.current - // Duplicate the sponsors to create a seamless loop scrollContainer.innerHTML += scrollContainer.innerHTML } }, [sponsors]) @@ -36,7 +35,7 @@ export default function MovingLogos({ sponsors }: MovingLogosProps) {
{sponsor.imageUrl ? ( {`${sponsor.name} Date: Tue, 4 Mar 2025 18:36:18 -0800 Subject: [PATCH 23/25] Update code --- backend/apps/owasp/admin.py | 2 +- backend/apps/owasp/constants.py | 1 - .../graphql/nodes/{sponsors.py => sponsor.py} | 4 +- .../apps/owasp/graphql/queries/__init__.py | 2 +- .../queries/{sponsors.py => sponsor.py} | 4 +- .../commands/owasp_update_sponsors.py | 6 +-- .../owasp/models/{sponsors.py => sponsor.py} | 41 +++++++++---------- backend/apps/slack/utils.py | 13 +++--- backend/tests/owasp/models/sponsors_test.py | 30 +++++++++----- .../{MovingLogo.tsx => LogoCarousel.tsx} | 0 frontend/src/pages/Home.tsx | 2 +- 11 files changed, 54 insertions(+), 51 deletions(-) rename backend/apps/owasp/graphql/nodes/{sponsors.py => sponsor.py} (84%) rename backend/apps/owasp/graphql/queries/{sponsors.py => sponsor.py} (75%) rename backend/apps/owasp/models/{sponsors.py => sponsor.py} (82%) rename frontend/src/components/{MovingLogo.tsx => LogoCarousel.tsx} (100%) diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index a0e803cf99..c6aabd1c20 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -10,7 +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 +from apps.owasp.models.sponsor import Sponsor class GenericEntityAdminMixin: diff --git a/backend/apps/owasp/constants.py b/backend/apps/owasp/constants.py index 07d1555015..3bc25f7556 100644 --- a/backend/apps/owasp/constants.py +++ b/backend/apps/owasp/constants.py @@ -1,4 +1,3 @@ """OWASP app constants.""" OWASP_ORGANIZATION_NAME = "OWASP" -OWASP_ORGANIZATION_DATA_URL = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main" diff --git a/backend/apps/owasp/graphql/nodes/sponsors.py b/backend/apps/owasp/graphql/nodes/sponsor.py similarity index 84% rename from backend/apps/owasp/graphql/nodes/sponsors.py rename to backend/apps/owasp/graphql/nodes/sponsor.py index d099508b26..15947c3a60 100644 --- a/backend/apps/owasp/graphql/nodes/sponsors.py +++ b/backend/apps/owasp/graphql/nodes/sponsor.py @@ -1,7 +1,7 @@ """OWASP sponsors GraphQL node.""" from apps.common.graphql.nodes import BaseNode -from apps.owasp.models.sponsors import Sponsor +from apps.owasp.models.sponsor import Sponsor class SponsorNode(BaseNode): @@ -10,6 +10,6 @@ class SponsorNode(BaseNode): class Meta: model = Sponsor fields = ( - "name", "image_url", + "name", ) diff --git a/backend/apps/owasp/graphql/queries/__init__.py b/backend/apps/owasp/graphql/queries/__init__.py index 8a0614c2b6..538bdd9a1c 100644 --- a/backend/apps/owasp/graphql/queries/__init__.py +++ b/backend/apps/owasp/graphql/queries/__init__.py @@ -5,7 +5,7 @@ from .event import EventQuery from .project import ProjectQuery from .snapshot import SnapshotQuery -from .sponsors import SponsorQuery +from .sponsor import SponsorQuery from .stats import StatsQuery diff --git a/backend/apps/owasp/graphql/queries/sponsors.py b/backend/apps/owasp/graphql/queries/sponsor.py similarity index 75% rename from backend/apps/owasp/graphql/queries/sponsors.py rename to backend/apps/owasp/graphql/queries/sponsor.py index 1a3a4333b4..d81ed2d8ff 100644 --- a/backend/apps/owasp/graphql/queries/sponsors.py +++ b/backend/apps/owasp/graphql/queries/sponsor.py @@ -3,8 +3,8 @@ import graphene from apps.common.graphql.queries import BaseQuery -from apps.owasp.graphql.nodes.sponsors import SponsorNode -from apps.owasp.models.sponsors import Sponsor +from apps.owasp.graphql.nodes.sponsor import SponsorNode +from apps.owasp.models.sponsor import Sponsor class SponsorQuery(BaseQuery): diff --git a/backend/apps/owasp/management/commands/owasp_update_sponsors.py b/backend/apps/owasp/management/commands/owasp_update_sponsors.py index 0d9dfb96f2..f7c52aece3 100644 --- a/backend/apps/owasp/management/commands/owasp_update_sponsors.py +++ b/backend/apps/owasp/management/commands/owasp_update_sponsors.py @@ -4,17 +4,17 @@ from django.core.management.base import BaseCommand from apps.github.utils import get_repository_file_content -from apps.owasp.models.sponsors import Sponsor +from apps.owasp.models.sponsor import Sponsor class Command(BaseCommand): help = "Import sponsors from the provided YAML file" def handle(self, *args, **kwargs): - data = yaml.safe_load( + sponsors = 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]) + Sponsor.bulk_save([Sponsor.update_data(sponsor) for sponsor in sponsors]) diff --git a/backend/apps/owasp/models/sponsors.py b/backend/apps/owasp/models/sponsor.py similarity index 82% rename from backend/apps/owasp/models/sponsors.py rename to backend/apps/owasp/models/sponsor.py index 82532f06a4..bb1f1c436f 100644 --- a/backend/apps/owasp/models/sponsors.py +++ b/backend/apps/owasp/models/sponsor.py @@ -5,7 +5,6 @@ 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): @@ -95,39 +94,37 @@ def update_data(data, save=True): 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}" - - sponsor_key = str(data.get("sponsor", "-1")) - member_key = str(data.get("membertype", "4")) + image_url = f"https://raw.githubusercontent.com/OWASP/owasp.github.io/main/{image_path}" member_type_mapping = { - "2": self.MemberType.PLATINUM, - "3": self.MemberType.GOLD, - "4": self.MemberType.SILVER, + 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, + -1: self.SponsorType.NOT_SPONSOR, + 1: self.SponsorType.DIAMOND, + 2: self.SponsorType.PLATINUM, + 3: self.SponsorType.GOLD, + 4: self.SponsorType.SILVER, + 5: self.SponsorType.SUPPORTER, } - sponsor_type_label = sponsor_type_mapping.get(sponsor_key, self.SponsorType.NOT_SPONSOR) + member_key = data.get("membertype", 4) + sponsor_key = data.get("sponsor", -1) member_type_label = member_type_mapping.get(member_key, self.MemberType.SILVER) + sponsor_type_label = sponsor_type_mapping.get(sponsor_key, self.SponsorType.NOT_SPONSOR) 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, + "is_member": data.get("member", False), + "job_url": normalize_url(data.get("job_url", "")) or "", "member_type": member_type_label, + "name": data["name"], + "sort_name": data.get("sortname", ""), + "sponsor_type": sponsor_type_label, + "url": normalize_url(data.get("url", "")) or "", } for key, value in fields.items(): diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 35198ee27c..8cdef4b424 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -85,7 +85,7 @@ def get_staff_data(timeout=30): def get_events_data(): - """Get raw events data via Database.""" + """Get events data.""" from apps.owasp.models.event import Event try: @@ -95,15 +95,12 @@ def get_events_data(): return None -MAX_SPONSORS = 10 - - -def get_sponsors_data(): - """Get raw sponsors data via Database.""" - from apps.owasp.models.sponsors import Sponsor +def get_sponsors_data(limit=10): + """Get sponsors data.""" + from apps.owasp.models.sponsor import Sponsor try: - return Sponsor.objects.all()[:MAX_SPONSORS] + return Sponsor.objects.all()[:limit] except Exception as e: logger.exception("Failed to fetch sponsors data via database", extra={"error": str(e)}) return None diff --git a/backend/tests/owasp/models/sponsors_test.py b/backend/tests/owasp/models/sponsors_test.py index cb7c5f73ad..ac354425ea 100644 --- a/backend/tests/owasp/models/sponsors_test.py +++ b/backend/tests/owasp/models/sponsors_test.py @@ -2,7 +2,7 @@ import pytest -from apps.owasp.models.sponsors import Sponsor +from apps.owasp.models.sponsor import Sponsor class TestSponsorModel: @@ -50,34 +50,44 @@ def test_readable_member_type(self, member_type, expected_label): def test_bulk_save(self): """Test the bulk_save method.""" mock_sponsors = [Mock(id=None), Mock(id=1)] - with patch("apps.owasp.models.sponsors.BulkSaveModel.bulk_save") as mock_bulk_save: + with patch("apps.owasp.models.sponsor.BulkSaveModel.bulk_save") as mock_bulk_save: Sponsor.bulk_save(mock_sponsors, fields=["name"]) mock_bulk_save.assert_called_once_with(Sponsor, mock_sponsors, fields=["name"]) @pytest.mark.parametrize( ("sponsor_type_value", "expected_sponsor_type"), [ - ("1", "Diamond"), - ("2", "Platinum"), - ("-1", "Not a Sponsor"), + (-1, "Not a Sponsor"), + (1, "Diamond"), + (2, "Platinum"), ], ) def test_from_dict_sponsor_type_mapping(self, sponsor_type_value, expected_sponsor_type): """Test the from_dict method for sponsor_type mapping.""" sponsor = Sponsor() - sponsor.from_dict({"sponsor": sponsor_type_value}) + sponsor.from_dict( + { + "name": "Sponsor", + "sponsor": sponsor_type_value, + } + ) assert sponsor.sponsor_type == expected_sponsor_type @pytest.mark.parametrize( ("member_type_value", "expected_member_type"), [ - ("2", "Platinum"), # "2" maps to "Platinum" - ("3", "Gold"), # "3" maps to "Gold" - ("4", "Silver"), # "4" maps to "Silver" + (2, "Platinum"), + (3, "Gold"), + (4, "Silver"), ], ) def test_from_dict_member_type_mapping(self, member_type_value, expected_member_type): """Test the from_dict method for member_type mapping.""" sponsor = Sponsor() - sponsor.from_dict({"membertype": member_type_value}) + sponsor.from_dict( + { + "membertype": member_type_value, + "name": "Sponsor", + } + ) assert sponsor.member_type == expected_member_type diff --git a/frontend/src/components/MovingLogo.tsx b/frontend/src/components/LogoCarousel.tsx similarity index 100% rename from frontend/src/components/MovingLogo.tsx rename to frontend/src/components/LogoCarousel.tsx diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index a8ba399129..f390334524 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -22,7 +22,7 @@ import AnimatedCounter from 'components/AnimatedCounter' import ChapterMap from 'components/ChapterMap' import ItemCardList from 'components/ItemCardList' import LoadingSpinner from 'components/LoadingSpinner' -import MovingLogos from 'components/MovingLogo' +import MovingLogos from 'components/LogoCarousel' import MultiSearchBar from 'components/MultiSearch' import SecondaryCard from 'components/SecondaryCard' import TopContributors from 'components/ToggleContributors' From d9a8317c29492e888571ae8793ba9d31a0cb859a Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 09:22:43 +0530 Subject: [PATCH 24/25] changes --- backend/apps/owasp/graphql/nodes/sponsor.py | 1 + frontend/src/api/queries/homeQueries.ts | 1 + frontend/src/components/LogoCarousel.tsx | 39 +++++++++++---------- frontend/src/pages/Home.tsx | 8 ++--- frontend/src/types/home.ts | 1 + 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/backend/apps/owasp/graphql/nodes/sponsor.py b/backend/apps/owasp/graphql/nodes/sponsor.py index 15947c3a60..fec3b30409 100644 --- a/backend/apps/owasp/graphql/nodes/sponsor.py +++ b/backend/apps/owasp/graphql/nodes/sponsor.py @@ -12,4 +12,5 @@ class Meta: fields = ( "image_url", "name", + "url", ) diff --git a/frontend/src/api/queries/homeQueries.ts b/frontend/src/api/queries/homeQueries.ts index 7ce9f02f9b..74bc5f9fda 100644 --- a/frontend/src/api/queries/homeQueries.ts +++ b/frontend/src/api/queries/homeQueries.ts @@ -54,6 +54,7 @@ export const GET_MAIN_PAGE_DATA = gql` sponsors { imageUrl name + url } statsOverview { activeChaptersStats diff --git a/frontend/src/components/LogoCarousel.tsx b/frontend/src/components/LogoCarousel.tsx index 6db42156fc..b26cb6bfdd 100644 --- a/frontend/src/components/LogoCarousel.tsx +++ b/frontend/src/components/LogoCarousel.tsx @@ -1,9 +1,5 @@ import { useEffect, useRef } from 'react' - -interface SponsorType { - imageUrl?: string - name: string -} +import { SponsorType } from 'types/home' interface MovingLogosProps { sponsors: SponsorType[] @@ -30,20 +26,27 @@ export default function MovingLogos({ sponsors }: MovingLogosProps) { {sponsors.map((sponsor, index) => (
-
- {sponsor.imageUrl ? ( - {`${sponsor.name} - ) : ( -
- )} -
+ +
+ {sponsor.imageUrl ? ( + {`${sponsor.name} + ) : ( +
+ )} +
+
))}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index f390334524..dae8930282 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -218,10 +218,6 @@ export default function Home() { />
- - - -
{counterData.map((stat, index) => ( @@ -248,6 +244,10 @@ export default function Home() { Join OWASP Now + + + +
) diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts index ce61208b69..bab092acee 100644 --- a/frontend/src/types/home.ts +++ b/frontend/src/types/home.ts @@ -35,4 +35,5 @@ export type MainPageData = { export type SponsorType = { imageUrl: string name: string + url: string } From b45db631c8254f3b0bce96ba37eafefa150bcaf4 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Mar 2025 10:35:11 +0530 Subject: [PATCH 25/25] increase testcase timing --- frontend/__tests__/e2e/pages/ProjectDetails.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index 3a1d2b07cd..eb8f04c650 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -9,7 +9,7 @@ test.describe('Project Details Page', () => { json: { data: mockProjectDetailsData }, }) }) - await page.goto('/projects/test-project') + await page.goto('/projects/test-project', { timeout: 60000 }) }) test('should have a heading and summary', async ({ page }) => {