Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/apps/owasp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ owasp-generate-community-snapshot-video:
nest-snapshot-video \
python manage.py owasp_generate_community_snapshot_video $(snapshot_key) /home/owasp/generated_videos

SNAPSHOT_FREQUENCY ?= weekly

owasp-create-snapshot:
@echo "Creating OWASP community snapshot"
@CMD="python manage.py owasp_create_snapshot --frequency $(SNAPSHOT_FREQUENCY)" $(MAKE) exec-backend-command

owasp-process-snapshots:
@echo "Processing OWASP snapshots"
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command
Expand Down
146 changes: 146 additions & 0 deletions backend/apps/owasp/management/commands/owasp_create_snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Django management command for creating OWASP community snapshots."""

import logging
from datetime import UTC, datetime, timedelta

from django.core.management.base import BaseCommand

from apps.owasp.models.snapshot import Snapshot

logger = logging.getLogger(__name__)


class Command(BaseCommand):
"""Command to create a community snapshot with automatic date range calculation."""

help = "Create a community snapshot for a given frequency (weekly or monthly)"

def add_arguments(self, parser):
"""Add command-line arguments.

Args:
parser (argparse.ArgumentParser): The argument parser instance.

"""
parser.add_argument(
"--frequency",
type=str,
choices=[Snapshot.Frequency.WEEKLY, Snapshot.Frequency.MONTHLY],
default=Snapshot.Frequency.WEEKLY,
help="Snapshot frequency: weekly (default) or monthly.",
)

def handle(self, *args, **options):
"""Handle command execution.

Args:
*args: Variable length argument list.
**options: Arbitrary keyword arguments containing command options.

"""
frequency = options["frequency"]
start_at, end_at = self.calculate_date_range(frequency)

self.stdout.write(f"Creating {frequency} snapshot")
self.stdout.write(f"Period: {start_at.date()} to {end_at.date()}")
logger.info(
"Creating %s snapshot from %s to %s",
frequency,
start_at.date(),
end_at.date(),
)

# Generate key to check for duplicates before creating.
key = self.generate_key(start_at, frequency)
if Snapshot.objects.filter(key=key).exists():
self.stdout.write(
self.style.WARNING(f"Snapshot with key '{key}' already exists, skipping creation")
)
logger.info("Snapshot with key '%s' already exists, skipping", key)
return

snapshot = Snapshot.objects.create(
Comment thread
HarshitVerma109 marked this conversation as resolved.
key=key,
frequency=frequency,
start_at=start_at,
end_at=end_at,
title=self.generate_title(start_at, frequency),
status=Snapshot.Status.PENDING,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

self.stdout.write(
self.style.SUCCESS(
f"Snapshot created successfully (ID: {snapshot.id}, key: {snapshot.key})"
)
)
logger.info(
"Snapshot created: id=%s, key=%s, frequency=%s",
snapshot.id,
snapshot.key,
frequency,
)

@staticmethod
def calculate_date_range(frequency):
"""Calculate the start and end dates for the snapshot period.

Args:
frequency (str): The snapshot frequency (weekly or monthly).

Returns:
tuple[datetime, datetime]: A tuple of (start_at, end_at) datetimes.

"""
current = datetime.now(tz=UTC)

if frequency == Snapshot.Frequency.WEEKLY:
# Last week: Monday 00:00:00 to Sunday 23:59:59
days_since_monday = current.weekday()
last_monday = current - timedelta(days=days_since_monday + 7)
start_at = last_monday.replace(hour=0, minute=0, second=0, microsecond=0)
end_at = start_at + timedelta(days=6, hours=23, minutes=59, seconds=59)
else:
# Last month: 1st 00:00:00 to last day 23:59:59
first_of_current_month = current.replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
end_at = first_of_current_month - timedelta(seconds=1)
start_at = end_at.replace(day=1, hour=0, minute=0, second=0, microsecond=0)

return start_at, end_at

@staticmethod
def generate_key(start_at, frequency):
"""Generate the snapshot key based on the start date and frequency.

This mirrors the logic in the Snapshot model's save() method.

Args:
start_at (datetime): The start date of the snapshot period.
frequency (str): The snapshot frequency (weekly or monthly).

Returns:
str: The generated snapshot key.

"""
if frequency == Snapshot.Frequency.WEEKLY:
iso_year, iso_week, _ = start_at.isocalendar()
return f"{iso_year}-W{iso_week:02d}"
return start_at.strftime("%Y-%m")

@staticmethod
def generate_title(start_at, frequency):
"""Generate a human-readable title for the snapshot.

Args:
start_at (datetime): The start date of the snapshot period.
frequency (str): The snapshot frequency (weekly or monthly).

Returns:
str: The generated snapshot title.

"""
if frequency == Snapshot.Frequency.WEEKLY:
iso_year, iso_week, _ = start_at.isocalendar()
return f"Week {iso_week} {iso_year} OWASP Community Snapshot"
return f"{start_at.strftime('%B %Y')} OWASP Community Snapshot"
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
from datetime import UTC, datetime
from unittest import mock

import pytest

from apps.owasp.management.commands.owasp_create_snapshot import Command
from apps.owasp.models.snapshot import Snapshot


class TestCreateSnapshot:
"""Tests for owasp_create_snapshot management command."""

def test_handle_creates_snapshot_weekly(self):
"""Test that handle creates a weekly snapshot successfully."""
command = Command()
mock_snapshot = mock.MagicMock()
mock_snapshot.id = 1
mock_snapshot.key = "2026-W24"

with (
mock.patch.object(
command,
"calculate_date_range",
return_value=(
datetime(2026, 6, 8, tzinfo=UTC),
datetime(2026, 6, 14, 23, 59, 59, tzinfo=UTC),
),
),
mock.patch.object(command, "generate_key", return_value="2026-W24"),
mock.patch.object(
command, "generate_title", return_value="Week 24 2026 OWASP Community Snapshot"
),
mock.patch("apps.owasp.models.snapshot.Snapshot.objects") as mock_objects,
):
mock_objects.filter.return_value.exists.return_value = False
mock_objects.create.return_value = mock_snapshot

command.handle(frequency="weekly")

mock_objects.create.assert_called_once_with(
key="2026-W24",
frequency="weekly",
start_at=datetime(2026, 6, 8, tzinfo=UTC),
end_at=datetime(2026, 6, 14, 23, 59, 59, tzinfo=UTC),
title="Week 24 2026 OWASP Community Snapshot",
status=Snapshot.Status.PENDING,
)

def test_handle_creates_snapshot_monthly(self):
"""Test that handle creates a monthly snapshot successfully."""
command = Command()
mock_snapshot = mock.MagicMock()
mock_snapshot.id = 1
mock_snapshot.key = "2026-05"

with (
mock.patch.object(
command,
"calculate_date_range",
return_value=(
datetime(2026, 5, 1, tzinfo=UTC),
datetime(2026, 5, 31, 23, 59, 59, tzinfo=UTC),
),
),
mock.patch.object(command, "generate_key", return_value="2026-05"),
mock.patch.object(
command, "generate_title", return_value="May 2026 OWASP Community Snapshot"
),
mock.patch("apps.owasp.models.snapshot.Snapshot.objects") as mock_objects,
):
mock_objects.filter.return_value.exists.return_value = False
mock_objects.create.return_value = mock_snapshot

command.handle(frequency="monthly")

mock_objects.create.assert_called_once_with(
key="2026-05",
frequency="monthly",
start_at=datetime(2026, 5, 1, tzinfo=UTC),
end_at=datetime(2026, 5, 31, 23, 59, 59, tzinfo=UTC),
title="May 2026 OWASP Community Snapshot",
status=Snapshot.Status.PENDING,
)

def test_handle_skips_duplicate_snapshot(self):
"""Test that handle skips creation when a snapshot with the same key exists."""
command = Command()

with (
mock.patch.object(
command,
"calculate_date_range",
return_value=(
datetime(2026, 6, 8, tzinfo=UTC),
datetime(2026, 6, 14, 23, 59, 59, tzinfo=UTC),
),
),
mock.patch.object(command, "generate_key", return_value="2026-W24"),
mock.patch("apps.owasp.models.snapshot.Snapshot.objects") as mock_objects,
mock.patch(
"apps.owasp.management.commands.owasp_create_snapshot.logger"
) as mock_logger,
):
mock_objects.filter.return_value.exists.return_value = True

command.handle(frequency="weekly")

mock_objects.create.assert_not_called()
mock_logger.info.assert_any_call(
"Snapshot with key '%s' already exists, skipping", "2026-W24"
)

@pytest.mark.parametrize(
("frozen_now", "expected_start", "expected_end"),
[
pytest.param(
datetime(2026, 6, 15, 12, 0, 0, tzinfo=UTC),
datetime(2026, 6, 8, 0, 0, 0, tzinfo=UTC),
datetime(2026, 6, 14, 23, 59, 59, tzinfo=UTC),
id="monday-calculates-last-week",
),
pytest.param(
datetime(2026, 6, 11, 12, 0, 0, tzinfo=UTC),
datetime(2026, 6, 1, 0, 0, 0, tzinfo=UTC),
datetime(2026, 6, 7, 23, 59, 59, tzinfo=UTC),
id="thursday-calculates-last-week",
),
],
)
def test_calculate_date_range_weekly(self, frozen_now, expected_start, expected_end):
"""Test weekly date range calculation."""
with mock.patch(
"apps.owasp.management.commands.owasp_create_snapshot.datetime"
) as mock_datetime:
mock_datetime.now.return_value = frozen_now
mock_datetime.side_effect = datetime
start_at, end_at = Command.calculate_date_range("weekly")

assert start_at == expected_start
assert end_at == expected_end

def test_calculate_date_range_monthly(self):
"""Test monthly date range calculation."""
frozen_now = datetime(2026, 6, 15, 12, 0, 0, tzinfo=UTC)

with mock.patch(
"apps.owasp.management.commands.owasp_create_snapshot.datetime"
) as mock_datetime:
mock_datetime.now.return_value = frozen_now
mock_datetime.side_effect = datetime
start_at, end_at = Command.calculate_date_range("monthly")

assert start_at == datetime(2026, 5, 1, 0, 0, 0, tzinfo=UTC)
assert end_at == datetime(2026, 5, 31, 23, 59, 59, tzinfo=UTC)

def test_generate_key_weekly(self):
"""Test key generation for weekly snapshots."""
start_at = datetime(2026, 6, 8, tzinfo=UTC)
key = Command.generate_key(start_at, "weekly")
assert key == "2026-W24"

def test_generate_key_monthly(self):
"""Test key generation for monthly snapshots."""
start_at = datetime(2026, 5, 1, tzinfo=UTC)
key = Command.generate_key(start_at, "monthly")
assert key == "2026-05"

def test_generate_title_weekly(self):
"""Test title generation for weekly snapshots."""
start_at = datetime(2026, 6, 8, tzinfo=UTC)
title = Command.generate_title(start_at, "weekly")
assert title == "Week 24 2026 OWASP Community Snapshot"

def test_generate_title_monthly(self):
"""Test title generation for monthly snapshots."""
start_at = datetime(2026, 5, 1, tzinfo=UTC)
title = Command.generate_title(start_at, "monthly")
assert title == "May 2026 OWASP Community Snapshot"

def test_generate_key_matches_model_save_weekly(self):
"""Test that generate_key produces the same key as the model's save method."""
start_at = datetime(2026, 1, 5, tzinfo=UTC)
key = Command.generate_key(start_at, "weekly")
iso_year, iso_week, _ = start_at.isocalendar()
expected = f"{iso_year}-W{iso_week:02d}"
assert key == expected

def test_generate_key_matches_model_save_monthly(self):
"""Test that generate_key produces the same key as the model's save method."""
start_at = datetime(2026, 12, 1, tzinfo=UTC)
key = Command.generate_key(start_at, "monthly")
assert key == "2026-12"
3 changes: 3 additions & 0 deletions infrastructure/modules/tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
| <a name="module_load_data_task"></a> [load\_data\_task](#module\_load\_data\_task) | ./modules/task | n/a |
| <a name="module_mentorship_sync_modules_data"></a> [mentorship\_sync\_modules\_data](#module\_mentorship\_sync\_modules\_data) | ./modules/task | n/a |
| <a name="module_migrate_task"></a> [migrate\_task](#module\_migrate\_task) | ./modules/task | n/a |
| <a name="module_owasp_create_snapshot_task"></a> [owasp\_create\_snapshot\_task](#module\_owasp\_create\_snapshot\_task) | ./modules/task | n/a |
| <a name="module_owasp_update_project_health_metrics_task"></a> [owasp\_update\_project\_health\_metrics\_task](#module\_owasp\_update\_project\_health\_metrics\_task) | ./modules/task | n/a |
| <a name="module_owasp_update_project_health_scores_task"></a> [owasp\_update\_project\_health\_scores\_task](#module\_owasp\_update\_project\_health\_scores\_task) | ./modules/task | n/a |
| <a name="module_slack_sync_data_task"></a> [slack\_sync\_data\_task](#module\_slack\_sync\_data\_task) | ./modules/task | n/a |
Expand Down Expand Up @@ -53,6 +54,8 @@
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | The AWS region. | `string` | n/a | yes |
| <a name="input_common_tags"></a> [common\_tags](#input\_common\_tags) | A map of common tags to apply to all resources. | `map(string)` | `{}` | no |
| <a name="input_container_parameters_arns"></a> [container\_parameters\_arns](#input\_container\_parameters\_arns) | Map of environment variable names to the ARNs of all SSM parameters. | `map(string)` | `{}` | no |
| <a name="input_create_snapshot_task_cpu"></a> [create\_snapshot\_task\_cpu](#input\_create\_snapshot\_task\_cpu) | The CPU for the create-snapshot task. | `string` | `"256"` | no |
| <a name="input_create_snapshot_task_memory"></a> [create\_snapshot\_task\_memory](#input\_create\_snapshot\_task\_memory) | The memory for the create-snapshot task. | `string` | `"1024"` | no |
| <a name="input_ecr_repository_arn"></a> [ecr\_repository\_arn](#input\_ecr\_repository\_arn) | The ARN of the ECR repository for the backend image. | `string` | n/a | yes |
| <a name="input_ecr_repository_url"></a> [ecr\_repository\_url](#input\_ecr\_repository\_url) | The URL of the ECR repository for the backend image. | `string` | n/a | yes |
| <a name="input_ecs_sg_id"></a> [ecs\_sg\_id](#input\_ecs\_sg\_id) | The ID of the security group for the ECS tasks. | `string` | n/a | yes |
Expand Down
Loading
Loading