From 99e1f85a4ed01ea241bc11770109e9d43fa571a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:59:26 +0000 Subject: [PATCH 1/3] Initial plan From 2f8d9ce43cf81850d3cf00477fb6997301d48694 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:10:00 +0000 Subject: [PATCH 2/3] Add delete functionality for superadmins and RSS feed for activities Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- blt/urls.py | 4 +++ website/feeds.py | 51 +++++++++++++++++++++++++++++++++++ website/templates/feed.html | 40 +++++++++++++++++++++++++++ website/views/organization.py | 13 +++++++++ 4 files changed, 108 insertions(+) create mode 100644 website/feeds.py diff --git a/blt/urls.py b/blt/urls.py index 6bb5db9936..a9a6987f24 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -225,6 +225,7 @@ approve_activity, checkIN, checkIN_detail, + delete_activity, delete_room, delete_time_entry, dislike_activity, @@ -336,6 +337,7 @@ view_thread, ) from website.views.video_call import video_call +from website.feeds import ActivityFeed admin.autodiscover() @@ -756,6 +758,7 @@ re_path(r"^report-ip/$", ReportIpView.as_view(), name="report_ip"), re_path(r"^reported-ips/$", ReportedIpListView.as_view(), name="reported_ips_list"), re_path(r"^feed/$", feed, name="feed"), + re_path(r"^feed/rss/$", ActivityFeed(), name="activity_feed_rss"), re_path( r"^api/v1/createissues/$", csrf_exempt(IssueCreate.as_view()), @@ -811,6 +814,7 @@ path("activity/like//", like_activity, name="like_activity"), path("activity/dislike//", dislike_activity, name="dislike_activity"), path("activity/approve//", approve_activity, name="approve_activity"), + path("activity/delete//", delete_activity, name="delete_activity"), re_path(r"^tz_detect/", include("tz_detect.urls")), re_path(r"^ratings/", include("star_ratings.urls", namespace="ratings")), re_path(r"^robots\.txt$", robots_txt), diff --git a/website/feeds.py b/website/feeds.py new file mode 100644 index 0000000000..0dfcd2c59c --- /dev/null +++ b/website/feeds.py @@ -0,0 +1,51 @@ +from django.contrib.syndication.views import Feed +from django.urls import reverse +from django.utils.feedgenerator import Rss201rev2Feed + +from website.models import Activity + + +class ActivityFeed(Feed): + """RSS feed for global activity feed.""" + + title = "OWASP BLT - Global Activity Feed" + link = "/feed/" + description = "Stay updated with the latest activities on the OWASP Bug Logging Tool" + feed_type = Rss201rev2Feed + + def items(self): + """Return the latest 50 activities.""" + return Activity.objects.all().order_by("-timestamp")[:50] + + def item_title(self, item): + """Return the title of the activity.""" + return item.title + + def item_description(self, item): + """Return the description of the activity.""" + description = f"{item.get_action_type_display()} by {item.user.username}" + if item.description: + description += f"\n\n{item.description}" + return description + + def item_link(self, item): + """Return the link to the activity.""" + if item.url: + return item.url + return reverse("feed") + + def item_pubdate(self, item): + """Return the publication date of the activity.""" + return item.timestamp + + def item_author_name(self, item): + """Return the author's name.""" + return item.user.username + + def item_guid(self, item): + """Return a unique identifier for the item.""" + return f"activity-{item.id}" + + def item_guid_is_permalink(self, item): + """Indicate that the GUID is not a permalink.""" + return False diff --git a/website/templates/feed.html b/website/templates/feed.html index 13ac3fd7f3..bec5b9a1a9 100644 --- a/website/templates/feed.html +++ b/website/templates/feed.html @@ -14,6 +14,18 @@ {% block content %} {% include "includes/sidenav.html" %}

Global Activity Feed

+ + + +
    {% for activity in page_obj %} @@ -65,6 +77,12 @@

    {{ activity.title }}

    data-id="{{ activity.id }}" onclick="approveActivity({{ activity.id }})">Approve {% endif %} + + {% if request.user.is_superuser %} + + {% endif %} {% if activity.is_approved %}
    Approved For BlueSky
    @@ -161,5 +179,27 @@

    {{ activity.title }}

    .catch(error => console.error("Error:", error)); } + function deleteActivity(activityId) { + if (confirm("Are you sure you want to delete this activity? This action cannot be undone.")) { + fetch(`/activity/delete/${activityId}/`, { + method: "POST", + headers: { "X-CSRFToken": getCookie("csrftoken") }, + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } else { + alert(data.error || "Failed to delete activity"); + console.error(data.error); + } + }) + .catch(error => { + alert("An error occurred while deleting the activity"); + console.error("Error:", error); + }); + } + } + {% endblock %} diff --git a/website/views/organization.py b/website/views/organization.py index 7a86441d87..139b062f49 100644 --- a/website/views/organization.py +++ b/website/views/organization.py @@ -1909,6 +1909,19 @@ def approve_activity(request, id): return JsonResponse({"success": False, "error": "Not authorized"}) +@login_required +@require_POST +def delete_activity(request, id): + """Allow superadmins to delete activities from the feed.""" + if not request.user.is_superuser: + return JsonResponse({"success": False, "error": "Only superadmins can delete activities"}, status=403) + + activity = get_object_or_404(Activity, id=id) + activity.delete() + + return JsonResponse({"success": True, "message": "Activity deleted successfully"}) + + def truncate_text(text, length=15): return text if len(text) <= length else text[:length] + "..." From 736057a5198a0abc64e45f22d0edca407f73ae74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:12:04 +0000 Subject: [PATCH 3/3] Add comprehensive tests for activity feed delete and RSS functionality Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- website/test_feed.py | 128 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 website/test_feed.py diff --git a/website/test_feed.py b/website/test_feed.py new file mode 100644 index 0000000000..e8fffe15c1 --- /dev/null +++ b/website/test_feed.py @@ -0,0 +1,128 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import Client, TestCase +from django.urls import reverse + +from website.models import Activity, Issue + + +class ActivityFeedTests(TestCase): + """Tests for activity feed functionality.""" + + def setUp(self): + """Set up test data.""" + self.client = Client() + + # Create regular user + self.user = User.objects.create_user( + username="testuser", + password="testpass123", + email="test@example.com" + ) + + # Create superuser + self.superuser = User.objects.create_superuser( + username="admin", + password="adminpass123", + email="admin@example.com" + ) + + # Create a test issue for activity content + self.issue = Issue.objects.create( + user=self.user, + url="https://example.com/issue", + description="Test issue for activity", + status="open", + ) + + # Create test activity + content_type = ContentType.objects.get_for_model(Issue) + self.activity = Activity.objects.create( + user=self.user, + action_type="create", + title="Created a new bug report", + description="Test activity description", + content_type=content_type, + object_id=self.issue.id, + ) + + def test_feed_page_loads(self): + """Test that the feed page loads successfully.""" + url = reverse("feed") + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Global Activity Feed") + + def test_activity_displayed_on_feed(self): + """Test that activities are displayed on the feed.""" + url = reverse("feed") + response = self.client.get(url) + + self.assertContains(response, self.activity.title) + self.assertContains(response, self.activity.description) + self.assertContains(response, self.user.username) + + def test_delete_button_not_visible_to_regular_user(self): + """Test that delete button is not visible to regular users.""" + self.client.login(username="testuser", password="testpass123") + url = reverse("feed") + response = self.client.get(url) + + # Check that delete button is not present + self.assertNotContains(response, "deleteActivity") + + def test_delete_button_visible_to_superuser(self): + """Test that delete button is visible to superusers.""" + self.client.login(username="admin", password="adminpass123") + url = reverse("feed") + response = self.client.get(url) + + # Check that delete button is present + self.assertContains(response, "deleteActivity") + + def test_regular_user_cannot_delete_activity(self): + """Test that regular users cannot delete activities.""" + self.client.login(username="testuser", password="testpass123") + url = reverse("delete_activity", kwargs={"id": self.activity.id}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 403) + # Activity should still exist + self.assertTrue(Activity.objects.filter(id=self.activity.id).exists()) + + def test_superuser_can_delete_activity(self): + """Test that superusers can delete activities.""" + self.client.login(username="admin", password="adminpass123") + url = reverse("delete_activity", kwargs={"id": self.activity.id}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 200) + # Activity should be deleted + self.assertFalse(Activity.objects.filter(id=self.activity.id).exists()) + + def test_rss_feed_accessible(self): + """Test that RSS feed is accessible.""" + url = reverse("activity_feed_rss") + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8") + + def test_rss_feed_contains_activities(self): + """Test that RSS feed contains activities.""" + url = reverse("activity_feed_rss") + response = self.client.get(url) + + content = response.content.decode("utf-8") + self.assertIn(self.activity.title, content) + self.assertIn(self.user.username, content) + + def test_rss_feed_link_on_page(self): + """Test that RSS feed link is present on the feed page.""" + url = reverse("feed") + response = self.client.get(url) + + rss_url = reverse("activity_feed_rss") + self.assertContains(response, rss_url) + self.assertContains(response, "Subscribe to RSS Feed")