diff --git a/deployment/docker/requirements.txt b/deployment/docker/requirements.txt index 2f7621b96..ef33580fa 100644 --- a/deployment/docker/requirements.txt +++ b/deployment/docker/requirements.txt @@ -34,6 +34,8 @@ lxml==4.9.3 oauthlib==3.2.2 packaging==23.2 Pillow==10.1.0 +pinax-announcements==4.0.1 +pinax-templates==3.0.0 platformdirs==3.11.0 psycopg2==2.9.9 pycparser==2.21 diff --git a/django_project/minisass/settings.py b/django_project/minisass/settings.py index 2d8066db9..c9abacecb 100644 --- a/django_project/minisass/settings.py +++ b/django_project/minisass/settings.py @@ -206,6 +206,9 @@ 'minisass_authentication', 'monitor', 'minisass', + 'pinax.announcements', + 'bootstrapform', + 'pinax.templates', # 'google_analytics' ] diff --git a/django_project/minisass/urls.py b/django_project/minisass/urls.py index 23646ba3c..e9df66ba0 100644 --- a/django_project/minisass/urls.py +++ b/django_project/minisass/urls.py @@ -7,7 +7,8 @@ from minisass.views import ( GroupScoresListView, VideoListView, - GetMobileApp + GetMobileApp, + get_announcements ) from minisass_frontend.views import ReactHomeView @@ -18,6 +19,7 @@ path('group-scores/', GroupScoresListView.as_view(), name='group-scores'), path('videos/', VideoListView.as_view(), name='video-list'), path('mobile-app/', GetMobileApp.as_view(), name='get-mobile-app'), + path('announcements/', get_announcements, name='get_announcements'), path('jsi18n//', JavaScriptCatalog.as_view(), name='javascript-catalog'), # Use JavaScriptCatalog directly path('admin/', admin.site.urls), diff --git a/django_project/minisass/views.py b/django_project/minisass/views.py index 5d99f09bd..8097a54f9 100644 --- a/django_project/minisass/views.py +++ b/django_project/minisass/views.py @@ -11,6 +11,9 @@ VideoSerializer, MobileAppSerializer ) +from django.http import JsonResponse +from pinax.announcements.models import Announcement +from django.utils.timezone import now class GroupScoresListView(generics.ListAPIView): queryset = GroupScores.objects.all().order_by('name') @@ -28,3 +31,28 @@ class GetMobileApp(APIView): def get(self, request): mobile_app = MobileApp.objects.filter(active=True).order_by('-id').first() return Response(self.serializer_class(mobile_app).data) + + +def get_announcements(request): + current_time = now() + + user_is_authenticated = request.user.is_authenticated if request.user else False + + announcements = Announcement.objects.filter( + publish_start__lte=current_time, + publish_end__gte=current_time, + site_wide=True + ) + + if user_is_authenticated: + announcements = announcements | Announcement.objects.filter( + publish_start__lte=current_time, + publish_end__gte=current_time, + members_only=True + ) + else: + announcements = announcements.exclude(members_only=True) + + announcements_data = announcements.values('title', 'content', 'dismissal_type') + return JsonResponse(list(announcements_data), safe=False) + diff --git a/django_project/minisass_frontend/src/components/Banner/index.tsx b/django_project/minisass_frontend/src/components/Banner/index.tsx index dcdc1e76e..3d635db4e 100644 --- a/django_project/minisass_frontend/src/components/Banner/index.tsx +++ b/django_project/minisass_frontend/src/components/Banner/index.tsx @@ -1,17 +1,40 @@ import React, { useState, useEffect } from 'react'; import { AiOutlineClose } from 'react-icons/ai'; +import { globalVariables } from '../../utils'; const Banner: React.FC = () => { const [visible, setVisible] = useState(false); + const [announcements, setAnnouncements] = useState([]); + const [showCloseButton, setShowCloseButton] = useState(false); + const announcements_url = globalVariables.baseUrl + '/announcements/'; + + // Fetch announcements from the API useEffect(() => { - if(visible) + fetch(announcements_url) + .then((response) => response.json()) + .then((data) => { + if (data.length > 0) { + setAnnouncements(data); + const shouldShowCloseButton = data.some(announcement => announcement.dismissal_type !== 1); + setShowCloseButton(shouldShowCloseButton); + setVisible(true); + } else { + setVisible(false); + } + }) + .catch((error) => console.error('Error fetching announcements:', error)); + }, []); + + useEffect(() => { + if (showCloseButton && visible) { const timer = setTimeout(() => { setVisible(false); }, 15000); - return () => clearTimeout(timer); - }, []); + return () => clearTimeout(timer); + } + }, [visible, showCloseButton]); const bannerStyle: React.CSSProperties = { backgroundColor: '#003f81', @@ -32,15 +55,13 @@ const Banner: React.FC = () => { const textStyle: React.CSSProperties = { flex: 1, - textAlign: 'center', - marginTop: '2%' + textAlign: 'center' }; const buttonStyle: React.CSSProperties = { color: 'white', background: 'transparent', border: 'none', - marginTop: '2%', display: 'flex', alignItems: 'center' }; @@ -49,15 +70,19 @@ const Banner: React.FC = () => { <> {visible && (
- - The miniSASS site will undergo routine maintenance on Friday, 30th August 2024. We apologize for any inconvenience caused by the brief downtime. - - +
+ {announcements.map((announcement, index) => ( +
+ {announcement.title}
+ {announcement.content} +
+ ))} +
+ {showCloseButton && ( + + )}
)}