Reintroduced News feed widget in admin dashboard.

This commit is contained in:
Antoine Nguyen
2025-07-17 17:29:45 +02:00
parent 0ada7be646
commit d585938df0
8 changed files with 155 additions and 18 deletions
+3
View File
@@ -7,4 +7,7 @@ export default {
getNotifications() {
return repository.get('/admin/notifications/')
},
getNewsFeed() {
return repository.get('/admin/news_feed/')
},
}
@@ -0,0 +1,52 @@
<template>
<v-card :title="$gettext('Latest news')">
<v-card-text>
<v-data-table
:headers="headers"
:items="news"
hide-default-header
hide-default-footer
>
<template #[`item.title`]="{ item }">
<a :href="item.link" target="_blank">{{ item.title }}</a>
</template>
<template #[`item.published`]="{ item }">
{{ $date(item.published) }}
</template>
</v-data-table>
</v-card-text>
<v-card-actions class="justify-center">
<div>
<a href="https://modoboa.org/weblog/" target="_blank">
{{ $gettext('Visit the official weblog for more information') }}
</a>
</div>
</v-card-actions>
</v-card>
</template>
<script setup>
import { ref } from 'vue'
import adminApi from '@/api/admin'
const news = ref([])
const headers = [
{ title: '', key: 'title' },
{ title: '', key: 'published', align: 'end' },
]
const resp = await adminApi.getNewsFeed()
news.value = resp.data
</script>
<style scoped lang="scss">
a {
text-decoration: none;
color: rgb(var(--v-theme-primary));
&:visited {
color: rgb(var(--v-theme-primary));
}
}
</style>
+10 -17
View File
@@ -2,25 +2,18 @@
<v-toolbar flat>
<v-toolbar-title>{{ $gettext('Welcome to Modoboa') }}</v-toolbar-title>
</v-toolbar>
<v-alert type="info" border="top" colored-border elevation="2" class="mt-10">
<p>{{ $gettext('This page is still under construction.') }}</p>
<p>
{{
$gettext(
"It will present a summary of your Modoboa server's status and some useful information about the product."
)
}}
</p>
<p>
Your feedback would be really appreciated, you can use
<a href="https://github.com/modoboa/modoboa/discussions"
>Github discussions</a
>
to share your ideas and/or comments.
</p>
</v-alert>
<v-row>
<v-col sm="12" md="6">
<NewsFeedWidget />
</v-col>
</v-row>
</template>
<script setup>
import NewsFeedWidget from '@/components/admin/dashboard/NewsFeedWidget'
</script>
<style scoped>
.v-toolbar {
background-color: #f7f8fa !important;
+7
View File
@@ -647,3 +647,10 @@ class ModoboaApplicationSerializer(serializers.Serializer):
icon = serializers.CharField()
url = serializers.CharField()
description = serializers.CharField(required=False)
class NewsFeedEntrySerializer(serializers.Serializer):
title = serializers.CharField()
link = serializers.CharField()
published = serializers.DateTimeField()
+39
View File
@@ -543,3 +543,42 @@ class CapabilitiesAPITestCase(ModoAPITestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn("rspamd", response.json()["capabilities"])
class NewsFeedAPIViewTestCase(ModoAPITestCase):
def test_get(self):
url = reverse("v2:news-feed")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 5)
@override_settings(DISABLE_DASHBOARD_EXTERNAL_QUERIES=False)
def test_if_disabling_from_settings_works(self):
url = reverse("v2:news-feed")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
def test_custom_rss_feed(self):
self.set_global_parameter(
"rss_feed_url", "https://www.djangoproject.com/rss/weblog/"
)
# Try as superadmin
url = reverse("v2:news-feed")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn("modoboa", response.json()[0]["link"])
# Try as domainadmin
dadmin = models.User.objects.get(username="admin@test.com")
self.authenticate_user(dadmin)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn("django", response.json()[0]["link"])
# Try fallback
self.set_global_parameter("rss_feed_url", "")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn("modoboa", response.json()[0]["link"])
+5
View File
@@ -37,5 +37,10 @@ urlpatterns += [
views.NotificationsAPIView.as_view(),
name="notifications",
),
path(
"admin/news_feed/",
views.NewsFeedAPIView.as_view(),
name="news-feed",
),
path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
]
+32 -1
View File
@@ -4,6 +4,10 @@ from functools import reduce
import logging
from smtplib import SMTPException
from dateutil import parser
import feedparser
from django.conf import settings
from django.utils.datastructures import MultiValueDictKeyError
from drf_spectacular.utils import extend_schema
@@ -12,7 +16,7 @@ from rest_framework.views import APIView
from modoboa.core import signals
from modoboa.core.utils import check_for_updates, get_capabilities
from modoboa.lib.permissions import IsSuperUser
from modoboa.lib.permissions import IsSuperUser, IsPrivilegedUser
from modoboa.lib.throttle import (
UserLesserDdosUser,
PasswordResetApplyThrottle,
@@ -24,6 +28,8 @@ from . import serializers
logger = logging.getLogger("modoboa.auth")
MODOBOA_WEBSITE_URL = "https://modoboa.org/"
def delete_cache_key(class_target, throttles: list, request) -> None:
"""Attempt to delete cache key from throttling on login/password reset success."""
@@ -165,9 +171,34 @@ class NotificationsAPIView(APIView):
class CapabilitiesAPIView(APIView):
"""Return the capability of this Modoboa instance."""
permission_classes = [permissions.IsAuthenticated]
throttle_classes = [UserLesserDdosUser]
def get(self, *args, **kwargs):
return response.Response({"capabilities": get_capabilities()})
class NewsFeedAPIView(APIView):
"""Return list of latest news from configured RSS feed."""
permission_classes = [permissions.IsAuthenticated, IsPrivilegedUser]
throttle_classes = [UserLesserDdosUser]
@extend_schema(responses=serializers.NewsFeedEntrySerializer(many=True))
def get(self, request, *args, **kwargs):
lang = "fr" if request.user.language == "fr" else "en"
feed_url = f"{MODOBOA_WEBSITE_URL}{lang}/weblog/feeds/"
if request.user.role != "SuperAdmins":
custom_feed_url = request.localconfig.parameters.get_value("rss_feed_url")
if custom_feed_url:
feed_url = custom_feed_url
entries = []
if not settings.DISABLE_DASHBOARD_EXTERNAL_QUERIES:
posts = feedparser.parse(feed_url)
for entry in posts["entries"][:5]:
entry["published"] = parser.parse(entry["published"])
entries.append(entry)
serializer = serializers.NewsFeedEntrySerializer(entries, many=True)
return response.Response(serializer.data)
+7
View File
@@ -154,6 +154,13 @@ class IsSuperUser(permissions.BasePermission):
return request.user.is_superuser
class IsPrivilegedUser(permissions.BasePermission):
"""Permission class to allow any privileged user."""
def has_permission(self, request, view):
return request.user.role != core_constants.SIMPLEUSERS_ROLE[0]
class CanCreateDomain(permissions.BasePermission):
"""Permissions class to allow users with add_domain right."""