From 3e7889c277bad4060ace621b2da118b5859377d1 Mon Sep 17 00:00:00 2001 From: Harsh Pandey Date: Sat, 18 Jan 2025 18:34:02 +0530 Subject: [PATCH 01/39] backend --- backend/.env.example | 3 + backend/apps/owasp/admin.py | 9 + backend/apps/owasp/api/feedback.py | 115 ++++++++ backend/apps/owasp/api/urls.py | 2 + .../apps/owasp/migrations/0015_feedback.py | 36 +++ backend/apps/owasp/models/feedback.py | 24 ++ backend/poetry.lock | 251 ++++++++++-------- backend/pyproject.toml | 1 + backend/settings/base.py | 15 ++ docker-compose.yaml | 3 + 10 files changed, 342 insertions(+), 117 deletions(-) create mode 100644 backend/apps/owasp/api/feedback.py create mode 100644 backend/apps/owasp/migrations/0015_feedback.py create mode 100644 backend/apps/owasp/models/feedback.py diff --git a/backend/.env.example b/backend/.env.example index e60ec3501..6439ba760 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,6 +4,9 @@ DJANGO_ALGOLIA_WRITE_API_KEY=None DJANGO_ALLOWED_HOSTS=* DJANGO_AWS_ACCESS_KEY_ID=None DJANGO_AWS_SECRET_ACCESS_KEY=None +DJANGO_AWS_STORAGE_BUCKET_NAME=None +DJANGO_AWS_S3_REGION_NAME=None +DJANGO_FEEDBACK_SHEET_KEY=None DJANGO_CONFIGURATION=Test DJANGO_DB_HOST=None DJANGO_DB_NAME=None diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index 9c9c00c0f..c520f3e72 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -6,6 +6,7 @@ from apps.owasp.models.chapter import Chapter from apps.owasp.models.committee import Committee from apps.owasp.models.event import Event +from apps.owasp.models.feedback import Feedback from apps.owasp.models.project import Project @@ -101,7 +102,15 @@ def custom_field_name(self, obj): custom_field_name.short_description = "Name" +class FeedbackAdmin(admin.ModelAdmin): + list_display = ("name", "email", "isAnonymous", "isNestbot", "created_at", "s3_file_path") + search_fields = ("name", "email", "isAnonymous", "isNestbot", "created_at", "s3_file_path") + readonly_fields = ("created_at", "s3_file_path") + ordering = ("-created_at",) + + admin.site.register(Chapter, ChapterAdmin) admin.site.register(Committee, CommetteeAdmin) admin.site.register(Event, EventAdmin) +admin.site.register(Feedback, FeedbackAdmin) admin.site.register(Project, ProjectAdmin) diff --git a/backend/apps/owasp/api/feedback.py b/backend/apps/owasp/api/feedback.py new file mode 100644 index 000000000..7a865da85 --- /dev/null +++ b/backend/apps/owasp/api/feedback.py @@ -0,0 +1,115 @@ +"""Feedback API endpoint for handling feedback submissions.""" + +import csv +import logging +from io import StringIO + +import boto3 +from django.conf import settings +from rest_framework import serializers, status, viewsets +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from apps.owasp.models.feedback import Feedback + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler() # This ensures logs go to stdout/stderr + ], +) +logger = logging.getLogger(__name__) + + +class FeedbackSerializer(serializers.ModelSerializer): + """Serializer for Feedback model.""" + + class Meta: + model = Feedback + fields = "__all__" + + +class FeedbackViewSet(viewsets.ModelViewSet): + """ViewSet for handling feedback.""" + + permission_classes = [AllowAny] + queryset = Feedback.objects.all() + serializer_class = FeedbackSerializer + + def post(self, request): + """Handle POST request for feedback submission.""" + logger.info("Processing new feedback submission") + serializer = FeedbackSerializer(data=request.data) + + if serializer.is_valid(): + try: + # Initialize S3 client + logger.debug("Initializing S3 client") + s3_client = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.AWS_S3_REGION_NAME, + ) + + logger.debug("Received feedback data: %s", serializer.validated_data) + logger.info( + "AWS Configuration - Bucket: %s, Region: %s", + settings.AWS_STORAGE_BUCKET_NAME, + settings.AWS_S3_REGION_NAME, + ) + + # Try to get existing file + tsv_key = "feedbacks.tsv" + try: + logger.debug("Attempting to read existing TSV file: %s", tsv_key) + response = s3_client.get_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + Key=tsv_key, + ) + existing_content = response["Body"].read().decode("utf-8") + output = StringIO(existing_content) + logger.debug("Successfully read existing TSV file") + except s3_client.exceptions.NoSuchKey: + logger.info("No existing TSV file found, creating new one") + output = StringIO() + writer = csv.writer(output, delimiter="\t") + writer.writerow( + ["Name", "Email", "Message", "isAnonymous", "isNestbot", "created_at"] + ) + else: + writer = csv.writer(output, delimiter="\t") + + # Write new feedback to the TSV file + writer.writerow( + [ + serializer.validated_data["name"], + serializer.validated_data["email"], + serializer.validated_data["message"], + serializer.validated_data["is_anonymous"], + serializer.validated_data["is_nestbot"], + serializer.validated_data["created_at"], + ] + ) + + # Upload the updated TSV file back to S3 + s3_client.put_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + Key=tsv_key, + Body=output.getvalue(), + ContentType="text/tab-separated-values", + ) + logger.info("Feedback successfully saved to %s", tsv_key) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + except Exception: + logger.exception("Error processing feedback submission") + return Response( + {"error": "An error occurred while processing feedback"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + else: + logger.warning("Invalid feedback data received: %s", serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/apps/owasp/api/urls.py b/backend/apps/owasp/api/urls.py index 31fd1ff4c..c20f10eb8 100644 --- a/backend/apps/owasp/api/urls.py +++ b/backend/apps/owasp/api/urls.py @@ -5,6 +5,7 @@ from apps.owasp.api.chapter import ChapterViewSet from apps.owasp.api.committee import CommitteeViewSet from apps.owasp.api.event import EventViewSet +from apps.owasp.api.feedback import FeedbackViewSet from apps.owasp.api.project import ProjectViewSet router = routers.SimpleRouter() @@ -12,4 +13,5 @@ router.register(r"owasp/chapters", ChapterViewSet) router.register(r"owasp/committees", CommitteeViewSet) router.register(r"owasp/events", EventViewSet) +router.register(r"owasp/feedback", FeedbackViewSet) router.register(r"owasp/projects", ProjectViewSet) diff --git a/backend/apps/owasp/migrations/0015_feedback.py b/backend/apps/owasp/migrations/0015_feedback.py new file mode 100644 index 000000000..8a6a1c072 --- /dev/null +++ b/backend/apps/owasp/migrations/0015_feedback.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.5 on 2025-01-17 08:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0014_project_custom_tags"), + ] + + operations = [ + migrations.CreateModel( + name="Feedback", + 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(default="Anonymous", max_length=100)), + ("email", models.EmailField(default="Anonymous", max_length=254)), + ("message", models.TextField()), + ("isAnonymous", models.BooleanField(default=True)), + ("isNestbot", models.BooleanField(default=False)), + ("s3_file_path", models.CharField(blank=True, max_length=255)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "verbose_name": "Feedback", + "verbose_name_plural": "Feedbacks", + }, + ), + ] diff --git a/backend/apps/owasp/models/feedback.py b/backend/apps/owasp/models/feedback.py new file mode 100644 index 000000000..95950e4b0 --- /dev/null +++ b/backend/apps/owasp/models/feedback.py @@ -0,0 +1,24 @@ +"""Feedback model for storing feedback.""" + +from django.db import models + +from apps.common.models import TimestampedModel + + +class Feedback(TimestampedModel): + """Model for storing feedback.""" + + name = models.CharField(max_length=100, default="Anonymous") + email = models.EmailField(default="Anonymous") + message = models.TextField() + is_anonymous = models.BooleanField(default=True) + is_nestbot = models.BooleanField(default=False) + s3_file_path = models.CharField(max_length=255, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name_plural = "Feedbacks" + + def __str__(self): + """Return string representation of the Feedback model.""" + return f"{self.name} - {self.created_at}" diff --git a/backend/poetry.lock b/backend/poetry.lock index eef7c42cb..2dccaf71f 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -232,32 +232,32 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "boto3" -version = "1.35.92" +version = "1.36.1" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.92-py3-none-any.whl", hash = "sha256:786930d5f1cd13d03db59ff2abbb2b7ffc173fd66646d5d8bee07f316a5f16ca"}, - {file = "boto3-1.35.92.tar.gz", hash = "sha256:f7851cb320dcb2a53fc73b4075187ec9b05d51291539601fa238623fdc0e8cd3"}, + {file = "boto3-1.36.1-py3-none-any.whl", hash = "sha256:eb21380d73fec6645439c0d802210f72a0cdb3295b02953f246ff53f512faa8f"}, + {file = "boto3-1.36.1.tar.gz", hash = "sha256:258ab77225a81d3cf3029c9afe9920cd9dec317689dfadec6f6f0a23130bb60a"}, ] [package.dependencies] -botocore = ">=1.35.92,<1.36.0" +botocore = ">=1.36.1,<1.37.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.10.0,<0.11.0" +s3transfer = ">=0.11.0,<0.12.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.92" +version = "1.36.1" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.92-py3-none-any.whl", hash = "sha256:f94ae1e056a675bd67c8af98a6858d06e3927d974d6c712ed6e27bb1d11bee1d"}, - {file = "botocore-1.35.92.tar.gz", hash = "sha256:caa7d5d857fed5b3d694b89c45f82b9f938f840e90a4eb7bf50aa65da2ba8f82"}, + {file = "botocore-1.36.1-py3-none-any.whl", hash = "sha256:dec513b4eb8a847d79bbefdcdd07040ed9d44c20b0001136f0890a03d595705a"}, + {file = "botocore-1.36.1.tar.gz", hash = "sha256:f789a6f272b5b3d8f8756495019785e33868e5e00dd9662a3ee7959ac939bb12"}, ] [package.dependencies] @@ -266,7 +266,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.22.0)"] +crt = ["awscrt (==0.23.4)"] [[package]] name = "certifi" @@ -582,6 +582,7 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -592,6 +593,7 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -1736,25 +1738,25 @@ files = [ [[package]] name = "psycopg" -version = "3.2.3" +version = "3.2.4" description = "PostgreSQL database adapter for Python" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, - {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, + {file = "psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381"}, + {file = "psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92"}, ] [package.dependencies] tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.2.3)"] -c = ["psycopg-c (==3.2.3)"] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +binary = ["psycopg-binary (==3.2.4)"] +c = ["psycopg-c (==3.2.4)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] -test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] +test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] [[package]] name = "pycparser" @@ -1769,13 +1771,13 @@ files = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, + {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, + {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, ] [package.dependencies] @@ -2273,57 +2275,57 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.9.1" +version = "0.9.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, - {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, - {file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"}, - {file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"}, - {file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"}, - {file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"}, - {file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"}, - {file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"}, - {file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"}, - {file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"}, - {file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"}, - {file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"}, + {file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"}, + {file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"}, + {file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"}, + {file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"}, + {file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"}, + {file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"}, + {file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"}, ] [[package]] name = "s3transfer" -version = "0.10.4" +version = "0.11.1" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" files = [ - {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, - {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, + {file = "s3transfer-0.11.1-py3-none-any.whl", hash = "sha256:8fa0aa48177be1f3425176dfe1ab85dcd3d962df603c3dbfc585e6bf857ef0ff"}, + {file = "s3transfer-0.11.1.tar.gz", hash = "sha256:3f25c900a367c8b7f7d8f9c34edc87e300bde424f779dc9f0a8ae4f9df9264f6"}, ] [package.dependencies] -botocore = ">=1.33.2,<2.0a.0" +botocore = ">=1.36.0,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] [[package]] name = "sentry-sdk" -version = "2.19.2" +version = "2.20.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, - {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, + {file = "sentry_sdk-2.20.0-py2.py3-none-any.whl", hash = "sha256:c359a1edf950eb5e80cffd7d9111f3dbeef57994cb4415df37d39fda2cf22364"}, + {file = "sentry_sdk-2.20.0.tar.gz", hash = "sha256:afa82713a92facf847df3c6f63cec71eb488d826a50965def3d7722aa6f0fdab"}, ] [package.dependencies] @@ -2369,6 +2371,7 @@ sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] [[package]] name = "six" @@ -2497,13 +2500,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.28.1" +version = "20.29.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, - {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, + {file = "virtualenv-20.29.0-py3-none-any.whl", hash = "sha256:c12311863497992dc4b8644f8ea82d3b35bb7ef8ee82e6630d76d0197c39baf9"}, + {file = "virtualenv-20.29.0.tar.gz", hash = "sha256:6345e1ff19d4b1296954cee076baaf58ff2a12a84a338c62b02eda39f20aa982"}, ] [package.dependencies] @@ -2517,76 +2520,90 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wrapt" -version = "1.17.0" +version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] [[package]] @@ -2688,4 +2705,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.13" -content-hash = "31ee623505ba0b2b2c17bcb9638f86fa180a110c42c1c305c14bcfa8dd7e8143" +content-hash = "2557cff47630081b0ce58c97e0098ee76af2e9b0d92039b4afa28884f86f95e9" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 822ea922e..7f77f03f1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -42,6 +42,7 @@ pyyaml = "^6.0.2" requests = "^2.32.3" sentry-sdk = {extras = ["django"], version = "^2.19.2"} slack-bolt = "^1.22.0" +boto3 = "^1.36.1" [tool.poetry.group.dev.dependencies] diff --git a/backend/settings/base.py b/backend/settings/base.py index bbb3cab45..bdaf98c52 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -100,6 +100,21 @@ class Base(Configuration): "INDEX_PREFIX": ENVIRONMENT.lower(), } + # AWS S3 Configuration + AWS_ACCESS_KEY_ID = values.SecretValue(environ_name="AWS_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY = values.SecretValue(environ_name="AWS_SECRET_ACCESS_KEY") + AWS_STORAGE_BUCKET_NAME = values.SecretValue(environ_name="AWS_STORAGE_BUCKET_NAME") + AWS_S3_REGION_NAME = values.SecretValue(environ_name="AWS_S3_REGION_NAME") + FEEDBACK_SHEET_KEY = values.SecretValue(environ_name="FEEDBACK_SHEET_KEY") + + AWS = { + "ACCESS_KEY_ID": AWS_ACCESS_KEY_ID, + "SECRET_ACCESS_KEY": AWS_SECRET_ACCESS_KEY, + "STORAGE_BUCKET_NAME": AWS_STORAGE_BUCKET_NAME, + "S3_REGION_NAME": AWS_S3_REGION_NAME, + "FEEDBACK_SHEET_KEY": FEEDBACK_SHEET_KEY, + } + CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", diff --git a/docker-compose.yaml b/docker-compose.yaml index 93d0e27e4..9d5816b4a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,9 +1,12 @@ services: backend: + logging: + driver: "json-file" container_name: nest-backend command: > bash -c " poetry install --no-root && + poetry run python manage.py makemigrations owasp && poetry run python manage.py migrate && poetry run python manage.py runserver 0.0.0.0:8000 " From 523a1f724c33937c99b2c26f7878291deac049df Mon Sep 17 00:00:00 2001 From: Harsh Pandey Date: Sun, 19 Jan 2025 21:20:26 +0530 Subject: [PATCH 02/39] update api --- backend/apps/owasp/admin.py | 9 -- backend/apps/owasp/api/feedback.py | 176 +++++++++++++------------- backend/apps/owasp/api/urls.py | 2 +- backend/apps/owasp/models/feedback.py | 24 ---- 4 files changed, 88 insertions(+), 123 deletions(-) delete mode 100644 backend/apps/owasp/models/feedback.py diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index c520f3e72..9c9c00c0f 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -6,7 +6,6 @@ from apps.owasp.models.chapter import Chapter from apps.owasp.models.committee import Committee from apps.owasp.models.event import Event -from apps.owasp.models.feedback import Feedback from apps.owasp.models.project import Project @@ -102,15 +101,7 @@ def custom_field_name(self, obj): custom_field_name.short_description = "Name" -class FeedbackAdmin(admin.ModelAdmin): - list_display = ("name", "email", "isAnonymous", "isNestbot", "created_at", "s3_file_path") - search_fields = ("name", "email", "isAnonymous", "isNestbot", "created_at", "s3_file_path") - readonly_fields = ("created_at", "s3_file_path") - ordering = ("-created_at",) - - admin.site.register(Chapter, ChapterAdmin) admin.site.register(Committee, CommetteeAdmin) admin.site.register(Event, EventAdmin) -admin.site.register(Feedback, FeedbackAdmin) admin.site.register(Project, ProjectAdmin) diff --git a/backend/apps/owasp/api/feedback.py b/backend/apps/owasp/api/feedback.py index 7a865da85..c7a377598 100644 --- a/backend/apps/owasp/api/feedback.py +++ b/backend/apps/owasp/api/feedback.py @@ -1,115 +1,113 @@ -"""Feedback API endpoint for handling feedback submissions.""" +"""Handle feedback submission, saving to local DB and uploading to S3.""" import csv import logging +from datetime import datetime, timezone from io import StringIO import boto3 from django.conf import settings -from rest_framework import serializers, status, viewsets +from rest_framework import status, viewsets from rest_framework.permissions import AllowAny from rest_framework.response import Response -from apps.owasp.models.feedback import Feedback - # Configure logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[ - logging.StreamHandler() # This ensures logs go to stdout/stderr - ], + handlers=[logging.StreamHandler()], # Ensures logs go to stdout/stderr ) logger = logging.getLogger(__name__) -class FeedbackSerializer(serializers.ModelSerializer): - """Serializer for Feedback model.""" - - class Meta: - model = Feedback - fields = "__all__" - - class FeedbackViewSet(viewsets.ModelViewSet): """ViewSet for handling feedback.""" permission_classes = [AllowAny] - queryset = Feedback.objects.all() - serializer_class = FeedbackSerializer - def post(self, request): + def create(self, request): """Handle POST request for feedback submission.""" logger.info("Processing new feedback submission") - serializer = FeedbackSerializer(data=request.data) - - if serializer.is_valid(): - try: - # Initialize S3 client - logger.debug("Initializing S3 client") - s3_client = boto3.client( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - region_name=settings.AWS_S3_REGION_NAME, - ) - - logger.debug("Received feedback data: %s", serializer.validated_data) - logger.info( - "AWS Configuration - Bucket: %s, Region: %s", - settings.AWS_STORAGE_BUCKET_NAME, - settings.AWS_S3_REGION_NAME, - ) - - # Try to get existing file - tsv_key = "feedbacks.tsv" - try: - logger.debug("Attempting to read existing TSV file: %s", tsv_key) - response = s3_client.get_object( - Bucket=settings.AWS_STORAGE_BUCKET_NAME, - Key=tsv_key, - ) - existing_content = response["Body"].read().decode("utf-8") - output = StringIO(existing_content) - logger.debug("Successfully read existing TSV file") - except s3_client.exceptions.NoSuchKey: - logger.info("No existing TSV file found, creating new one") - output = StringIO() - writer = csv.writer(output, delimiter="\t") - writer.writerow( - ["Name", "Email", "Message", "isAnonymous", "isNestbot", "created_at"] - ) - else: - writer = csv.writer(output, delimiter="\t") - - # Write new feedback to the TSV file - writer.writerow( - [ - serializer.validated_data["name"], - serializer.validated_data["email"], - serializer.validated_data["message"], - serializer.validated_data["is_anonymous"], - serializer.validated_data["is_nestbot"], - serializer.validated_data["created_at"], - ] - ) - - # Upload the updated TSV file back to S3 - s3_client.put_object( - Bucket=settings.AWS_STORAGE_BUCKET_NAME, - Key=tsv_key, - Body=output.getvalue(), - ContentType="text/tab-separated-values", - ) - logger.info("Feedback successfully saved to %s", tsv_key) - - return Response(serializer.data, status=status.HTTP_201_CREATED) - except Exception: - logger.exception("Error processing feedback submission") - return Response( - {"error": "An error occurred while processing feedback"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) + try: + feedback_data = request.data + logger.debug("Received feedback data: %s", feedback_data) + + # Initialize S3 client + s3_client = self._get_s3_client() + + # Get or create the TSV file + output, writer = self._get_or_create_tsv(s3_client) + + # Write new feedback data + self._write_feedback_to_tsv(writer, feedback_data) + + # Upload the updated TSV file back to S3 + self._upload_tsv_to_s3(s3_client, output) + + logger.info("Feedback successfully saved to %s", "feedbacks.tsv") + + return Response(status=status.HTTP_201_CREATED) + except Exception: + logger.exception("Error processing feedback submission: %s") + return Response( + {"error": "An error occurred while processing feedback"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + def _get_s3_client(self): + """Initialize and returns the S3 client.""" + logger.debug("Initializing S3 client") + return boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.AWS_S3_REGION_NAME, + ) + + def _get_or_create_tsv(self, s3_client): + """Get the existing TSV file or creates a new one if it doesn't exist.""" + tsv_key = "feedbacks.tsv" + try: + logger.debug("Attempting to read existing TSV file: %s", tsv_key) + response = s3_client.get_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + Key=tsv_key, + ) + existing_content = response["Body"].read().decode("utf-8") + output = StringIO(existing_content) + logger.debug("Successfully read existing TSV file") + except s3_client.exceptions.NoSuchKey: + logger.info("No existing TSV file found, creating a new one") + output = StringIO() + writer = csv.writer(output, delimiter="\t") + writer.writerow( + ["Name", "Email", "Message", "is_anonymous", "is_nestbot", "created_at"] + ) else: - logger.warning("Invalid feedback data received: %s", serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + writer = csv.writer(output, delimiter="\t") + + return output, writer + + def _write_feedback_to_tsv(self, writer, feedback_data): + """Write the new feedback data to the TSV file.""" + writer.writerow( + [ + feedback_data["name"], + feedback_data["email"], + feedback_data["message"], + feedback_data["is_anonymous"], + feedback_data["is_nestbot"], + datetime.now(timezone.utc).isoformat(), + ] + ) + logger.debug("New feedback data written to TSV") + + def _upload_tsv_to_s3(self, s3_client, output): + """Upload the updated TSV file back to S3.""" + s3_client.put_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + Key="feedbacks.tsv", + Body=output.getvalue(), + ContentType="text/tab-separated-values", + ) + logger.debug("TSV file uploaded to S3 successfully") diff --git a/backend/apps/owasp/api/urls.py b/backend/apps/owasp/api/urls.py index c20f10eb8..1a8402244 100644 --- a/backend/apps/owasp/api/urls.py +++ b/backend/apps/owasp/api/urls.py @@ -13,5 +13,5 @@ router.register(r"owasp/chapters", ChapterViewSet) router.register(r"owasp/committees", CommitteeViewSet) router.register(r"owasp/events", EventViewSet) -router.register(r"owasp/feedback", FeedbackViewSet) +router.register(r"owasp/feedback", FeedbackViewSet, basename="feedback") router.register(r"owasp/projects", ProjectViewSet) diff --git a/backend/apps/owasp/models/feedback.py b/backend/apps/owasp/models/feedback.py deleted file mode 100644 index 95950e4b0..000000000 --- a/backend/apps/owasp/models/feedback.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Feedback model for storing feedback.""" - -from django.db import models - -from apps.common.models import TimestampedModel - - -class Feedback(TimestampedModel): - """Model for storing feedback.""" - - name = models.CharField(max_length=100, default="Anonymous") - email = models.EmailField(default="Anonymous") - message = models.TextField() - is_anonymous = models.BooleanField(default=True) - is_nestbot = models.BooleanField(default=False) - s3_file_path = models.CharField(max_length=255, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - verbose_name_plural = "Feedbacks" - - def __str__(self): - """Return string representation of the Feedback model.""" - return f"{self.name} - {self.created_at}" From eb4adea1238bd5913951c41c5f9e50b9c2c75b2d Mon Sep 17 00:00:00 2001 From: Harsh Pandey Date: Sun, 19 Jan 2025 21:29:34 +0530 Subject: [PATCH 03/39] remove logging --- backend/apps/owasp/api/feedback.py | 28 ++------------- .../apps/owasp/migrations/0015_feedback.py | 36 ------------------- docker-compose.yaml | 3 -- 3 files changed, 3 insertions(+), 64 deletions(-) delete mode 100644 backend/apps/owasp/migrations/0015_feedback.py diff --git a/backend/apps/owasp/api/feedback.py b/backend/apps/owasp/api/feedback.py index c7a377598..5b49dccd7 100644 --- a/backend/apps/owasp/api/feedback.py +++ b/backend/apps/owasp/api/feedback.py @@ -1,24 +1,16 @@ """Handle feedback submission, saving to local DB and uploading to S3.""" import csv -import logging from datetime import datetime, timezone from io import StringIO import boto3 from django.conf import settings +from django.core.exceptions import ValidationError from rest_framework import status, viewsets from rest_framework.permissions import AllowAny from rest_framework.response import Response -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[logging.StreamHandler()], # Ensures logs go to stdout/stderr -) -logger = logging.getLogger(__name__) - class FeedbackViewSet(viewsets.ModelViewSet): """ViewSet for handling feedback.""" @@ -27,10 +19,8 @@ class FeedbackViewSet(viewsets.ModelViewSet): def create(self, request): """Handle POST request for feedback submission.""" - logger.info("Processing new feedback submission") try: feedback_data = request.data - logger.debug("Received feedback data: %s", feedback_data) # Initialize S3 client s3_client = self._get_s3_client() @@ -44,19 +34,12 @@ def create(self, request): # Upload the updated TSV file back to S3 self._upload_tsv_to_s3(s3_client, output) - logger.info("Feedback successfully saved to %s", "feedbacks.tsv") - return Response(status=status.HTTP_201_CREATED) - except Exception: - logger.exception("Error processing feedback submission: %s") - return Response( - {"error": "An error occurred while processing feedback"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) + except ValidationError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) def _get_s3_client(self): """Initialize and returns the S3 client.""" - logger.debug("Initializing S3 client") return boto3.client( "s3", aws_access_key_id=settings.AWS_ACCESS_KEY_ID, @@ -68,16 +51,13 @@ def _get_or_create_tsv(self, s3_client): """Get the existing TSV file or creates a new one if it doesn't exist.""" tsv_key = "feedbacks.tsv" try: - logger.debug("Attempting to read existing TSV file: %s", tsv_key) response = s3_client.get_object( Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=tsv_key, ) existing_content = response["Body"].read().decode("utf-8") output = StringIO(existing_content) - logger.debug("Successfully read existing TSV file") except s3_client.exceptions.NoSuchKey: - logger.info("No existing TSV file found, creating a new one") output = StringIO() writer = csv.writer(output, delimiter="\t") writer.writerow( @@ -100,7 +80,6 @@ def _write_feedback_to_tsv(self, writer, feedback_data): datetime.now(timezone.utc).isoformat(), ] ) - logger.debug("New feedback data written to TSV") def _upload_tsv_to_s3(self, s3_client, output): """Upload the updated TSV file back to S3.""" @@ -110,4 +89,3 @@ def _upload_tsv_to_s3(self, s3_client, output): Body=output.getvalue(), ContentType="text/tab-separated-values", ) - logger.debug("TSV file uploaded to S3 successfully") diff --git a/backend/apps/owasp/migrations/0015_feedback.py b/backend/apps/owasp/migrations/0015_feedback.py deleted file mode 100644 index 8a6a1c072..000000000 --- a/backend/apps/owasp/migrations/0015_feedback.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-17 08:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("owasp", "0014_project_custom_tags"), - ] - - operations = [ - migrations.CreateModel( - name="Feedback", - 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(default="Anonymous", max_length=100)), - ("email", models.EmailField(default="Anonymous", max_length=254)), - ("message", models.TextField()), - ("isAnonymous", models.BooleanField(default=True)), - ("isNestbot", models.BooleanField(default=False)), - ("s3_file_path", models.CharField(blank=True, max_length=255)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - options={ - "verbose_name": "Feedback", - "verbose_name_plural": "Feedbacks", - }, - ), - ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 9d5816b4a..93d0e27e4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,12 +1,9 @@ services: backend: - logging: - driver: "json-file" container_name: nest-backend command: > bash -c " poetry install --no-root && - poetry run python manage.py makemigrations owasp && poetry run python manage.py migrate && poetry run python manage.py runserver 0.0.0.0:8000 " From 64f0eb873e6ea58e2577f37237bda2b220aab600 Mon Sep 17 00:00:00 2001 From: Harsh Pandey Date: Sun, 19 Jan 2025 23:17:13 +0530 Subject: [PATCH 04/39] integrate frontend --- frontend/package-lock.json | 126 ++++++++++++++++- frontend/package.json | 7 +- frontend/src/App.tsx | 2 + frontend/src/components/Feedback.tsx | 13 ++ frontend/src/components/FeedbackForm.tsx | 166 ++++++++++++++++++++++ frontend/src/components/ui/Button.tsx | 50 +++++++ frontend/src/components/ui/Form.tsx | 167 +++++++++++++++++++++++ frontend/src/components/ui/Input.tsx | 22 +++ frontend/src/components/ui/Label.tsx | 19 +++ frontend/src/components/ui/Switch.tsx | 27 ++++ frontend/src/components/ui/Textarea.tsx | 21 +++ frontend/src/utils/helpers/schema.ts | 16 +++ 12 files changed, 634 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Feedback.tsx create mode 100644 frontend/src/components/FeedbackForm.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Form.tsx create mode 100644 frontend/src/components/ui/Input.tsx create mode 100644 frontend/src/components/ui/Label.tsx create mode 100644 frontend/src/components/ui/Switch.tsx create mode 100644 frontend/src/components/ui/Textarea.tsx create mode 100644 frontend/src/utils/helpers/schema.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5a6e0b2b6..468f30423 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,10 @@ "@fortawesome/free-regular-svg-icons": "^6.7.0", "@fortawesome/free-solid-svg-icons": "^6.7.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@sentry/browser": "^8.46.0", "@sentry/react": "^8.48.0", @@ -36,11 +39,13 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-gtm-module": "^2.0.11", + "react-hook-form": "^7.54.2", "react-icons": "^5.3.0", "react-router-dom": "^7.0.2", "react-tooltip": "^5.28.0", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -1231,6 +1236,15 @@ "react": ">=16.3" } }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2045,6 +2059,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", @@ -2134,6 +2171,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz", @@ -2234,6 +2300,39 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", @@ -9564,6 +9663,22 @@ "integrity": "sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw==", "license": "MIT" }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", @@ -11670,6 +11785,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index c8c4f4bab..170f59963 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,10 @@ "@fortawesome/free-regular-svg-icons": "^6.7.0", "@fortawesome/free-solid-svg-icons": "^6.7.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@sentry/browser": "^8.46.0", "@sentry/react": "^8.48.0", @@ -46,11 +49,13 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-gtm-module": "^2.0.11", + "react-hook-form": "^7.54.2", "react-icons": "^5.3.0", "react-router-dom": "^7.0.2", "react-tooltip": "^5.28.0", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 754638f6b..e8b996d03 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import { useEffect } from 'react' import { Routes, Route, useLocation } from 'react-router-dom' import { ErrorDisplay, ERROR_CONFIGS } from 'wrappers/ErrorWrapper' +import Feedback from 'components/Feedback' import Footer from 'components/Footer' import Header from 'components/Header' import { Toaster } from 'components/ui/Toaster' @@ -40,6 +41,7 @@ function App() { }> }> }> + } /> } />