Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
4 changes: 4 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
approve_activity,
checkIN,
checkIN_detail,
delete_activity,
delete_room,
delete_time_entry,
dislike_activity,
Expand Down Expand Up @@ -336,6 +337,7 @@
view_thread,
)
from website.views.video_call import video_call
from website.feeds import ActivityFeed

admin.autodiscover()

Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -811,6 +814,7 @@
path("activity/like/<int:id>/", like_activity, name="like_activity"),
path("activity/dislike/<int:id>/", dislike_activity, name="dislike_activity"),
path("activity/approve/<int:id>/", approve_activity, name="approve_activity"),
path("activity/delete/<int:id>/", 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),
Expand Down
51 changes: 51 additions & 0 deletions website/feeds.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions website/templates/feed.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
{% block content %}
{% include "includes/sidenav.html" %}
<h1 class="text-center text-red-600 text-2xl mt-10 mb-5">Global Activity Feed</h1>

<!-- RSS Feed Link -->
<div class="flex justify-center mb-3">
<a href="{% url 'activity_feed_rss' %}" class="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg transition-colors duration-200" title="Subscribe to RSS Feed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M5 3a1 1 0 000 2c5.523 0 10 4.477 10 10a1 1 0 102 0C17 8.373 11.627 3 5 3z"/>
<path d="M4 9a1 1 0 011-1 7 7 0 017 7 1 1 0 11-2 0 5 5 0 00-5-5 1 1 0 01-1-1zM3 15a2 2 0 114 0 2 2 0 01-4 0z"/>
</svg>
Subscribe to RSS Feed
</a>
</div>

<div class="bg-gray-50 rounded-lg shadow-md mx-auto p-5 max-w-4xl overflow-y-auto max-h-[calc(100vh-60px)] mb-5 relative">
<ul class="space-y-5 p-5">
{% for activity in page_obj %}
Expand Down Expand Up @@ -65,6 +77,12 @@ <h3 class="text-xl font-semibold text-gray-900">{{ activity.title }}</h3>
data-id="{{ activity.id }}"
onclick="approveActivity({{ activity.id }})">Approve</button>
{% endif %}
<!-- Delete Button (for superadmins only) -->
{% if request.user.is_superuser %}
<button class="bg-gray-800 text-white px-4 py-2 rounded-lg hover:bg-gray-900 transition-colors duration-200"
data-id="{{ activity.id }}"
onclick="deleteActivity({{ activity.id }})">Delete</button>
{% endif %}
<!-- Approved Label -->
{% if activity.is_approved %}
<div class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-bold">Approved For BlueSky</div>
Expand Down Expand Up @@ -161,5 +179,27 @@ <h3 class="text-xl font-semibold text-gray-900">{{ activity.title }}</h3>
.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);
});
}
}

</script>
{% endblock %}
128 changes: 128 additions & 0 deletions website/test_feed.py
Original file line number Diff line number Diff line change
@@ -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="[email protected]"
)

# Create superuser
self.superuser = User.objects.create_superuser(
username="admin",
password="adminpass123",
email="[email protected]"
)

# 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")
13 changes: 13 additions & 0 deletions website/views/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] + "..."

Expand Down
Loading