diff --git a/BackEnd/administration/templates/administration/admin_message_template.html b/BackEnd/administration/templates/administration/admin_message_template.html new file mode 100644 index 000000000..9235b2d43 --- /dev/null +++ b/BackEnd/administration/templates/administration/admin_message_template.html @@ -0,0 +1,47 @@ + + + + + + Нове повідомлення + + + +
+ CraftMerge Logo +
+

Доброго дня, {{ user_name }}!

+

Ви отримали нове повідомлення:

+

Категорія: {{ category }}

+

Повідомлення:

+

{{ message }}

+
+ +
+ + diff --git a/BackEnd/administration/tests/test_send_message.py b/BackEnd/administration/tests/test_send_message.py new file mode 100644 index 000000000..eb3784b9e --- /dev/null +++ b/BackEnd/administration/tests/test_send_message.py @@ -0,0 +1,109 @@ +from django.core import mail +from django.conf import settings +from rest_framework import status +from rest_framework.test import APITestCase +from authentication.factories import UserFactory +from utils.administration.send_email_notification import send_email_to_user + + +class TestSendMessageView(APITestCase): + def setUp(self): + self.admin = UserFactory(is_staff=True, is_active=True) + self.user = UserFactory(is_active=True) + self.client.force_authenticate(self.admin) + self.url = f"/api/admin/users/{self.user.id}/send_message/" + + def test_send_message_success(self): + data = { + "email": self.user.email, + "category": "Інше", + "message": "Valid message for testing.", + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_send_message_invalid_data(self): + data = { + "email": self.user.email, + "category": "Iнше", + "message": "Short", + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_send_message_unauthorized(self): + self.client.logout() + data = { + "email": self.user.email, + "category": "Інше", + "message": "Valid message for testing.", + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_send_message_user_not_found(self): + url = "/api/admin/users/9999/send_message/" + data = { + "email": "nonexistent@test.com", + "category": "Інше", + "message": "Valid message for testing.", + } + response = self.client.post(url, data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +class TestSendEmailFunctionality(APITestCase): + def setUp(self): + self.user = UserFactory( + name="Test", surname="User", email="test_user@example.com" + ) + + def send_email(self, category, message_content, email=None): + email = email if email else self.user.email + return send_email_to_user( + user=self.user, + category=category, + message_content=message_content, + email=email, + ) + + def test_send_email_success(self): + self.send_email( + category="Інше", + message_content="This is a test message.", + email="test_user@example.com", + ) + + self.assertEqual(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEqual(email.subject, "Адміністратор CraftMerge - Інше") + self.assertIn("This is a test message.", email.body) + self.assertEqual(email.to, ["test_user@example.com"]) + self.assertEqual(email.from_email, settings.EMAIL_HOST_USER) + + def test_send_email_empty_message(self): + with self.assertRaises(ValueError) as e: + self.send_email( + category="Інше", + message_content="", + email="test_user@example.com", + ) + self.assertEqual(str(e.exception), "Message content cannot be empty.") + + def test_send_email_invalid_email(self): + with self.assertRaises(ValueError) as e: + self.send_email( + category="Інше", + message_content="Test message", + email="invalid_email", + ) + self.assertEqual(str(e.exception), "Invalid email address.") + + def test_send_email_missing_category(self): + with self.assertRaises(ValueError) as e: + self.send_email( + category="", + message_content="Test message", + email="test_user@example.com", + ) + self.assertEqual(str(e.exception), "Category is required.") diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index d860830c3..b71b96953 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -10,6 +10,7 @@ ModerationEmailView, FeedbackView, CreateAdminUserView, + SendMessageView, ) app_name = "administration" @@ -28,4 +29,9 @@ path("contacts/", ContactsView.as_view(), name="contacts"), path("feedback/", FeedbackView.as_view(), name="feedback"), path("admin_create/", CreateAdminUserView.as_view(), name="admin-create"), + path( + "users//send_message/", + SendMessageView.as_view(), + name="send-message", + ), ] diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 7f137a5bc..52db67f0a 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -5,7 +5,6 @@ OpenApiExample, OpenApiResponse, ) - from rest_framework.generics import ( ListAPIView, RetrieveUpdateDestroyAPIView, @@ -30,6 +29,7 @@ from .permissions import IsStaffUser, IsStaffUserOrReadOnly, IsSuperUser from .serializers import FeedbackSerializer from utils.administration.send_email_feedback import send_email_feedback +from utils.administration.send_email_notification import send_email_to_user from django_filters.rest_framework import DjangoFilterBackend from .filters import UsersFilter @@ -199,3 +199,42 @@ def perform_create(self, serializer): category = serializer.validated_data["category"] send_email_feedback(email, message, category) + + +class SendMessageView(CreateAPIView): + """ + API endpoint for sending a custom email message to a specific user. + + This view allows administrators to send a message to a user's registered email. + It validates the request payload, retrieves the user based on the provided ID, + and sends the email using the specified category and message content. + """ + + queryset = CustomUser.objects.all() + permission_classes = [IsStaffUser] + serializer_class = FeedbackSerializer + + def perform_create(self, serializer): + """ + Handles the email sending logic after successful validation. + + This method is executed after the request data has been validated + by the serializer. It retrieves the user, validates their existence, + and sends the email with the provided category and message content. + + Parameters: + serializer (FeedbackSerializer): The serializer instance containing + the validated data from the request. + """ + user = self.get_object() + email = serializer.validated_data["email"] + category = serializer.validated_data["category"] + message_content = serializer.validated_data["message"] + + send_email_to_user( + user=user, + category=category, + message_content=message_content, + email=email, + sender_name="Адміністратор CraftMerge", + ) diff --git a/BackEnd/utils/administration/send_email_notification.py b/BackEnd/utils/administration/send_email_notification.py new file mode 100644 index 000000000..003c881b1 --- /dev/null +++ b/BackEnd/utils/administration/send_email_notification.py @@ -0,0 +1,58 @@ +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.conf import settings +from django.core.validators import validate_email +from django.core.exceptions import ValidationError + +EMAIL_CONTENT_SUBTYPE = "html" +PROTOCOL = "http" + + +def send_email_to_user( + user, + category, + message_content, + email=None, + sender_name="Адміністратор CraftMerge", + template_name="administration/admin_message_template.html", +): + """ + Sends an email message to the user using the specified template. + + :param user: The user object (CustomUser) + :param category: The email category + :param message_content: The message content + :param email: (Optional) The recipient's email + :param sender_name: Name of the sender + :param template_name: The path to the HTML template + """ + if not category: + raise ValueError("Category is required.") + if not message_content.strip(): + raise ValueError("Message content cannot be empty.") + try: + validate_email(email or user.email) + except ValidationError: + raise ValueError("Invalid email address.") + + context = { + "user_name": f"{user.name} {user.surname}", + "message": message_content, + "category": category, + "sender_name": sender_name, + "logo_url": f"{PROTOCOL}://178.212.110.52/craftMerge-logo.png", + } + + email_body = render_to_string(template_name, context) + recipient_email = email if email else user.email + + subject = f"{sender_name} - {category}" + + email = EmailMultiAlternatives( + subject=subject, + body=email_body, + from_email=settings.EMAIL_HOST_USER, + to=[recipient_email], + ) + email.content_subtype = EMAIL_CONTENT_SUBTYPE + email.send(fail_silently=False) diff --git a/FrontEnd/public/craftMerge-logo.png b/FrontEnd/public/craftMerge-logo.png new file mode 100644 index 000000000..842d972f0 Binary files /dev/null and b/FrontEnd/public/craftMerge-logo.png differ diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActions.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActions.jsx new file mode 100644 index 000000000..56db1e9ac --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActions.jsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react'; +import { Dropdown, Modal, Button, Select, Input, Tooltip } from 'antd'; +import { toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import axios from 'axios'; +import PropTypes from 'prop-types'; +import styles from './UserActions.module.css'; +import { useNavigate } from 'react-router-dom'; + +function UserActions({ user, onActionComplete }) { + const [selectedCategory, setSelectedCategory] = useState('Інше'); + const [messageContent, setMessageContent] = useState(''); + const [isSending, setIsSending] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const validateMessage = (message) => { + if (message.trim().length >= 10) { + setError(''); + return true; + } else { + setError('Повідомлення має бути не менше 10 символів.'); + return false; + } + }; + + const handleSendMessage = async () => { + if (!validateMessage(messageContent)) return; + + setIsSending(true); + try { + await axios.post( + `${process.env.REACT_APP_BASE_API_URL}/api/admin/users/${user.id}/send_message/`, + { + email: user.email, + category: selectedCategory, + message: messageContent.trim(), + } + ); + toast.success('Повідомлення успішно надіслано'); + setMessageContent(''); + setIsModalVisible(false); + if (onActionComplete) onActionComplete(); + } catch { + toast.error('Не вдалося надіслати повідомлення. Спробуйте ще раз.'); + } finally { + setIsSending(false); + } + }; + + const viewProfile = () => { + try { + navigate(`/customadmin/users/${user.id}`); + } catch (error) { + toast.error('Не вдалося переглянути профіль. Спробуйте оновити сторінку.'); + } + }; + + const menuItems = [ + { + key: 'sendMessage', + label: ( + + Надіслати листа + + ), + onClick: () => setIsModalVisible(true), + }, + { + key: 'viewProfile', + label: ( + + Переглянути профіль + + ), + onClick: viewProfile, + }, + ]; + + return ( + <> + + + + { + setIsModalVisible(false); + setError(''); + setMessageContent(''); + }} + footer={[ + , + , + ]} + width={600} + > +
+ +