From 302526a4c446be126331dc68a08700cfc11fce6e Mon Sep 17 00:00:00 2001 From: Lvyshnevska Date: Wed, 4 Dec 2024 21:42:16 +0100 Subject: [PATCH 01/80] added pagination to search, updated search tests --- BackEnd/search/tests/test_advanced_search.py | 26 ++-- .../tests/test_search_by_name_region.py | 130 +++++++++--------- BackEnd/search/views.py | 17 ++- 3 files changed, 92 insertions(+), 81 deletions(-) diff --git a/BackEnd/search/tests/test_advanced_search.py b/BackEnd/search/tests/test_advanced_search.py index 0014ada1e..7f6c53489 100644 --- a/BackEnd/search/tests/test_advanced_search.py +++ b/BackEnd/search/tests/test_advanced_search.py @@ -57,7 +57,7 @@ def setUp(self) -> None: def test_get_search_in_all_companies_official_order_by_name(self): response = self.client.get(path="/api/search/advanced?search=official") - names_from_response = [prof["name"] for prof in response.data] + names_from_response = [prof["name"] for prof in response.data["results"]] self.assertEqual(200, response.status_code) self.assertEqual( names_from_response, @@ -74,24 +74,24 @@ def test_get_search_in_all_companies_official_order_by_name(self): def test_get_search_name_service_info_lower_case(self): response = self.client.get(path="/api/search/advanced?search=kyiv") self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.data)) + self.assertEqual(2, response.data["total_items"]) def test_get_search_name_service_info_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/advanced?search=kyiv") self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.data)) + self.assertEqual(2, response.data["total_items"]) def test_get_search_name_service_info_upper_case(self): response = self.client.get(path="/api/search/advanced?search=KYIV") self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.data)) + self.assertEqual(2, response.data["total_items"]) def test_get_search_parcial_item(self): response = self.client.get(path="/api/search/advanced?search=ch") - names_from_response = [prof["name"] for prof in response.data] + names_from_response = [prof["name"] for prof in response.data["results"]] self.assertEqual(200, response.status_code) - self.assertEqual(3, len(response.data)) + self.assertEqual(3, response.data["total_items"]) self.assertEqual( names_from_response, ["Charkiv", "Chernigiv", "Dnipro"] ) @@ -99,27 +99,27 @@ def test_get_search_parcial_item(self): def test_get_search_not_exist(self): response = self.client.get(path="/api/search/advanced?search=QQQ") self.assertEqual(200, response.status_code) - self.assertEqual([], response.json()) + self.assertEqual([], response.data["results"]) def test_get_search_name_product_info(self): response = self.client.get(path="/api/search/advanced?search=dnipro") self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.data)) + self.assertEqual(2, response.data["total_items"]) def test_get_search_name_service_product_common_info(self): response = self.client.get(path="/api/search/advanced?search=KYIV") - names_from_response = [prof["name"] for prof in response.data] + names_from_response = [prof["name"] for prof in response.data["results"]] self.assertEqual(200, response.status_code) - self.assertEqual(2, len(response.data)) + self.assertEqual(2, response.data["total_items"]) self.assertEqual(names_from_response, ["Kryvyi Rig", "Kyiv"]) def test_get_search_devide_item(self): response = self.client.get( path="/api/search/advanced?search=product info" ) - names_from_response = [prof["name"] for prof in response.data] + names_from_response = [prof["name"] for prof in response.data["results"]] self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual(names_from_response, ["Kryvyi Rig"]) def test_advanced_search_serializer_fields(self): @@ -144,4 +144,4 @@ def test_advanced_search_serializer_fields(self): "person": self.company_kyiv.person_id, } ] - self.assertEqual(expected_result, response.json()) + self.assertEqual(expected_result, response.data["results"]) diff --git a/BackEnd/search/tests/test_search_by_name_region.py b/BackEnd/search/tests/test_search_by_name_region.py index b279afff2..42d7a4b89 100644 --- a/BackEnd/search/tests/test_search_by_name_region.py +++ b/BackEnd/search/tests/test_search_by_name_region.py @@ -40,12 +40,12 @@ def setUp(self) -> None: def test_get_profile_filtered_by_name_unauthorized(self): response = self.client.get(path="/api/search/?name=Kyivbud") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) def test_get_profile_filtered_by_region_eng_unauthorized(self): response = self.client.get(path="/api/search/?regions_eng=Dnipro") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -69,14 +69,14 @@ def test_get_profile_filtered_by_region_eng_unauthorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_eng_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_eng=Dnipro") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -100,14 +100,14 @@ def test_get_profile_filtered_by_region_eng_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_ukr=Дніпро") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -131,13 +131,13 @@ def test_get_profile_filtered_by_region_ukr_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_unauthorized(self): response = self.client.get(path="/api/search/?regions_ukr=Дніпро") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -161,14 +161,14 @@ def test_get_profile_filtered_by_region_ukr_unauthorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_partial_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_ukr=Дніп") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -192,13 +192,13 @@ def test_get_profile_filtered_by_region_ukr_partial_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_partial_unauthorized(self): response = self.client.get(path="/api/search/?regions_ukr=Дніпро") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -222,14 +222,14 @@ def test_get_profile_filtered_by_region_ukr_partial_unauthorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_lower_case_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_ukr=дніпро") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -253,40 +253,40 @@ def test_get_profile_filtered_by_region_ukr_lower_case_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_region_ukr_not_exist_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_ukr=УУ") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_region_eng_not_exist_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_eng=WWWW") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_region_ukr_not_exist_unauthorized(self): response = self.client.get(path="/api/search/?regions_ukr=УУ") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_region_eng_not_exist_unauthorized(self): response = self.client.get(path="/api/search/?regions_eng=WWWW") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_name_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?name=Kyivbud") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -310,39 +310,39 @@ def test_get_profile_filtered_by_name_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profiles_without_filter_unauthorized(self): response = self.client.get(path="/api/search/") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) + self.assertEqual(4, response.data["total_items"]) def test_get_profiles_without_filter_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) + self.assertEqual(4, response.data["total_items"]) def test_get_profiles_filter_wrong_name_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?name=Chernovcimilk") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profiles_filter_wrong_name_unauthorized(self): response = self.client.get(path="/api/search/?name=Chernovcimilk") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_name_and_region_eng_unauthorized(self): response = self.client.get( path="/api/search/?name=Kyivbud®ions_eng=Kyiv" ) self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -366,7 +366,7 @@ def test_get_profile_filtered_by_name_and_region_eng_unauthorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_name_and_region_eng_authorized(self): @@ -375,7 +375,7 @@ def test_get_profile_filtered_by_name_and_region_eng_authorized(self): path="/api/search/?name=Kyivbud®ion=Kyiv" ) self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -399,7 +399,7 @@ def test_get_profile_filtered_by_name_and_region_eng_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_wrong_name_and_region_ukr_authorized( @@ -410,16 +410,16 @@ def test_get_profile_filtered_by_wrong_name_and_region_ukr_authorized( path="/api/search/?name=Pizza®ions_ukr=Київ" ) self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_wrong_name_and_region_ukr_unauthorized( self, ): response = self.client.get(path="/api/search/?name=Pizza®ion=Київ") self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_name_and_wrong_region_eng_authorized( self, @@ -429,16 +429,16 @@ def test_get_profile_filtered_by_name_and_wrong_region_eng_authorized( path="/api/search/?name=Kyivbud®ions_eng=Ghernigiv" ) self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_name_and_wrong_region_unauthorized(self): response = self.client.get( path="/api/search/?name=Kyivbud®ions_eng=Ghernigiv" ) self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_wrong_name_and_wrong_region_authorized( self, @@ -448,8 +448,8 @@ def test_get_profile_filtered_by_wrong_name_and_wrong_region_authorized( path="/api/search/?name=Pizza®ions_eng=London" ) self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_wrong_name_and_wrong_region_unauthorized( self, @@ -458,25 +458,25 @@ def test_get_profile_filtered_by_wrong_name_and_wrong_region_unauthorized( path="/api/search/?name=Pizza®ions_ukr=Лондон" ) self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_filtered_by_region_eng_partially_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?regions_eng=Dnip") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) def test_get_profile_filtered_by_region_eng_partially_unauthorized(self): response = self.client.get(path="/api/search/?regions_eng=Dnip") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) def test_get_profile_filtered_by_name_partially_authorized(self): self.client.force_authenticate(self.user) response = self.client.get(path="/api/search/?name=Kyiv") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -500,13 +500,13 @@ def test_get_profile_filtered_by_name_partially_authorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_name_partially_unauthorized(self): response = self.client.get(path="/api/search/?name=Kyiv") self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -530,7 +530,7 @@ def test_get_profile_filtered_by_name_partially_unauthorized(self): "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_name_and_region_eng_partialyy_authorized( @@ -541,7 +541,7 @@ def test_get_profile_filtered_by_name_and_region_eng_partialyy_authorized( path="/api/search/?name=Kyiv®ions_eng=Ky" ) self.assertEqual(200, response.status_code) - self.assertEqual(1, len(response.data)) + self.assertEqual(1, response.data["total_items"]) self.assertEqual( [ { @@ -565,7 +565,7 @@ def test_get_profile_filtered_by_name_and_region_eng_partialyy_authorized( "is_saved": False, } ], - response.json(), + response.data["results"], ) def test_get_profile_filtered_by_wrong_name_and_wrong_region_partially_authorized( @@ -576,8 +576,8 @@ def test_get_profile_filtered_by_wrong_name_and_wrong_region_partially_authorize path="/api/search/?name=Kyivf®ions_eng=Kyif" ) self.assertEqual(200, response.status_code) - self.assertEqual(0, len(response.data)) - self.assertEqual([], response.json()) + self.assertEqual(0, response.data["total_items"]) + self.assertEqual([], response.data["results"]) def test_get_profile_ordered_by_name_asc_unauthorized( self, @@ -667,8 +667,8 @@ def test_get_profile_ordered_by_name_asc_unauthorized( response = self.client.get(path="/api/search/?ordering=name") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) - self.assertEqual(ordered_response_data, response.json()) + self.assertEqual(4, response.data["total_items"]) + self.assertEqual(ordered_response_data, response.data["results"]) def test_get_profile_ordered_by_name_asc_authorized( self, @@ -759,8 +759,8 @@ def test_get_profile_ordered_by_name_asc_authorized( response = self.client.get(path="/api/search/?ordering=name") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) - self.assertEqual(ordered_response_data, response.json()) + self.assertEqual(4, response.data["total_items"]) + self.assertEqual(ordered_response_data, response.data["results"]) def test_get_profile_ordered_by_name_dsc_unauthorized( self, @@ -850,8 +850,8 @@ def test_get_profile_ordered_by_name_dsc_unauthorized( response = self.client.get(path="/api/search/?ordering=-name") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) - self.assertEqual(ordered_response_data, response.json()) + self.assertEqual(4, response.data["total_items"]) + self.assertEqual(ordered_response_data, response.data["results"]) def test_get_profile_ordered_by_name_dsc_authorized( self, @@ -942,5 +942,5 @@ def test_get_profile_ordered_by_name_dsc_authorized( response = self.client.get(path="/api/search/?ordering=-name") self.assertEqual(200, response.status_code) - self.assertEqual(4, len(response.data)) - self.assertEqual(ordered_response_data, response.json()) + self.assertEqual(4, response.data["total_items"]) + self.assertEqual(ordered_response_data, response.data["results"]) diff --git a/BackEnd/search/views.py b/BackEnd/search/views.py index 7667f807b..162e9f96b 100644 --- a/BackEnd/search/views.py +++ b/BackEnd/search/views.py @@ -2,14 +2,19 @@ import django_filters from rest_framework import filters +from forum.pagination import ForumPagination from profiles.models import Profile, SavedCompany from .serializers import CompanySerializers, CompanyAdvancedSerializers from search.filters import CompanyFilter class SearchCompanyView(ListAPIView): - queryset = Profile.objects.active_only().prefetch_related( + queryset = ( + Profile.objects.active_only() + .prefetch_related( "regions", "categories", "activities" + ) + .order_by("id") ) serializer_class = CompanySerializers filter_backends = [ @@ -17,6 +22,7 @@ class SearchCompanyView(ListAPIView): filters.OrderingFilter, ] filterset_class = CompanyFilter + pagination_class = ForumPagination ordering_fields = ["name"] def get_serializer_context(self): @@ -32,10 +38,15 @@ def get_serializer_context(self): class AdvancedSearchView(ListAPIView): - queryset = Profile.objects.active_only().prefetch_related( - "regions", "categories", "activities" + queryset = ( + Profile.objects.active_only() + .prefetch_related( + "regions", "categories", "activities" + ) + .order_by("id") ) serializer_class = CompanyAdvancedSerializers + pagination_class = ForumPagination filter_backends = [ filters.SearchFilter, filters.OrderingFilter, From 680e7d766826421060ea562e7cbe0a79c96e93c8 Mon Sep 17 00:00:00 2001 From: Andrewakiv Date: Thu, 5 Dec 2024 15:58:41 +0200 Subject: [PATCH 02/80] companies stats --- BackEnd/administration/serializers.py | 22 +++++++++++++++++ BackEnd/administration/urls.py | 6 +++++ BackEnd/administration/views.py | 35 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index 24cda9beb..f84ec9c08 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.db.models import Q from rest_framework import serializers from utils.administration.feedback_category import FeedbackCategory from authentication.models import CustomUser @@ -213,3 +214,24 @@ class FeedbackSerializer(serializers.Serializer): required=True, error_messages={"required": "Please select a category."}, ) + + +class StatisticsSerializer(serializers.Serializer): + companies_count = serializers.SerializerMethodField() + investors_count = serializers.SerializerMethodField() + startups_count = serializers.SerializerMethodField() + blocked_companies_count = serializers.SerializerMethodField() + + def get_companies_count(self, obj) -> int: + return Profile.objects.filter( + Q(is_registered=True) | Q(is_startup=True) + ).count() + + def get_investors_count(self, obj) -> int: + return Profile.objects.filter(is_registered=True).count() + + def get_startups_count(self, obj) -> int: + return Profile.objects.filter(is_startup=True).count() + + def get_blocked_companies_count(self, obj) -> int: + return Profile.objects.filter(status='blocked').count() diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index d860830c3..ec746ec3e 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -4,6 +4,7 @@ ContactsView, ProfilesListView, ProfileDetailView, + ProfileStatisticsView, UsersListView, UserDetailView, AutoModerationHoursView, @@ -19,6 +20,11 @@ path("users//", UserDetailView.as_view(), name="user-detail"), path("profiles/", ProfilesListView.as_view(), name="profile-list"), path("profiles//", ProfileDetailView.as_view(), name="profile-detail"), + path( + "companies/satistics/", + ProfileStatisticsView.as_view(), + name="profile-satistics", + ), path( "automoderation/", AutoModerationHoursView.as_view(), diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 7f137a5bc..c3bab2558 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -1,5 +1,6 @@ from django.http import JsonResponse from django.views import View +from django.db.models import Q from drf_spectacular.utils import ( extend_schema, OpenApiExample, @@ -8,6 +9,7 @@ from rest_framework.generics import ( ListAPIView, + RetrieveAPIView, RetrieveUpdateDestroyAPIView, RetrieveUpdateAPIView, CreateAPIView, @@ -22,6 +24,7 @@ AdminUserDetailSerializer, AutoModerationHoursSerializer, ModerationEmailSerializer, + StatisticsSerializer, ) from administration.pagination import ListPagination from administration.models import AutoModeration, ModerationEmail @@ -34,6 +37,8 @@ from django_filters.rest_framework import DjangoFilterBackend from .filters import UsersFilter +from rest_framework.permissions import AllowAny + class UsersListView(ListAPIView): """ @@ -104,6 +109,36 @@ class ProfileDetailView(RetrieveUpdateDestroyAPIView): ) +class ProfileStatisticsView(RetrieveAPIView): + """ + Count of companies + """ + queryset = Profile.objects.all() + permission_classes = [AllowAny] + serializer_class = StatisticsSerializer + + def get_object(self): + return {} + + # def get_serializer_context(self): + # context = super().get_serializer_context() + # context["companies_count"] = ( + # self.get_queryset() + # .filter(Q(is_registered=True) | Q(is_startup=True)) + # .count() + # ) + # context["investors_count"] = ( + # self.get_queryset().filter(is_registered=True).count() + # ) + # context["startups_count"] = ( + # self.get_queryset().filter(is_startup=True).count() + # ) + # context["blocked_companies_count"] = ( + # self.get_queryset().filter(status='blocked').count() + # ) + # return context + + @extend_schema( request=AutoModerationHoursSerializer, responses={ From 2ae940ae5430d0ec603262bd92f03cfacdeca5aa Mon Sep 17 00:00:00 2001 From: Lvyshnevska Date: Thu, 5 Dec 2024 15:09:23 +0100 Subject: [PATCH 03/80] replaced search results with profile list component, added 1200 breakpoint for searchpage --- .../src/components/Loader/Loader.module.css | 1 + .../src/pages/CustomThemes/customTheme.js | 1 + .../src/pages/ProfileList/ProfileList.jsx | 11 +- .../pages/ProfileList/ProfileList.module.css | 21 -- .../src/pages/ProfileList/ProfileListPage.jsx | 2 +- FrontEnd/src/pages/SearchPage/Search.jsx | 200 +++++------- .../SearchPage/SearchField/SearchResults.jsx | 45 --- .../SearchField/SearchResults.module.css | 18 -- .../SearchField/tests/SearchResults.test.js | 138 --------- .../src/pages/SearchPage/img/link_to_left.svg | 3 - .../pages/SearchPage/img/link_to_right.svg | 3 - .../src/pages/SearchPage/search.module.scss | 120 +++---- .../src/pages/SearchPage/tests/Search.test.js | 293 +++++++++++++----- 13 files changed, 381 insertions(+), 475 deletions(-) delete mode 100644 FrontEnd/src/pages/ProfileList/ProfileList.module.css delete mode 100644 FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx delete mode 100644 FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css delete mode 100644 FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js delete mode 100644 FrontEnd/src/pages/SearchPage/img/link_to_left.svg delete mode 100644 FrontEnd/src/pages/SearchPage/img/link_to_right.svg diff --git a/FrontEnd/src/components/Loader/Loader.module.css b/FrontEnd/src/components/Loader/Loader.module.css index c24dedb7a..f8ed507fc 100644 --- a/FrontEnd/src/components/Loader/Loader.module.css +++ b/FrontEnd/src/components/Loader/Loader.module.css @@ -1,4 +1,5 @@ .loader__container { padding-top: 30%; min-height: var(--min-height-block-main); + width: 100%; } diff --git a/FrontEnd/src/pages/CustomThemes/customTheme.js b/FrontEnd/src/pages/CustomThemes/customTheme.js index 94ba1b9c0..652dca796 100644 --- a/FrontEnd/src/pages/CustomThemes/customTheme.js +++ b/FrontEnd/src/pages/CustomThemes/customTheme.js @@ -50,6 +50,7 @@ const customTheme = { }, List: { fontFamily: 'Geologica', + emptyTextPadding: 0, } }, token: { diff --git a/FrontEnd/src/pages/ProfileList/ProfileList.jsx b/FrontEnd/src/pages/ProfileList/ProfileList.jsx index ade6f33df..a53206a6a 100644 --- a/FrontEnd/src/pages/ProfileList/ProfileList.jsx +++ b/FrontEnd/src/pages/ProfileList/ProfileList.jsx @@ -5,6 +5,7 @@ import CompanyCard from '../../components/CompanyCard/CompanyCard'; export default function ProfileList({ isAuthorized, + emptyText, current, items, profiles, @@ -34,7 +35,7 @@ export default function ProfileList({ grid={{ justify: 'center', align: 'stretch', - gutter: [32, 24], + gutter: [24, 24], xs: 1, md: 2, xl: 3, @@ -53,9 +54,13 @@ export default function ProfileList({ }} dataSource={profiles} split={false} - locale={{emptyText: 'Жодна компанія не відповідає обраному фільтру.'}} + locale={{emptyText: emptyText}} renderItem={(item) => ( - + { - const response = await axios.get(url); - setSearchResults(response.data); - }; + async function fetcher(url) { + return axios.get(url) + .then(res => res.data); + } - const { data: companylist, error } = useSWR( - `${servedAddress}/api/search/?name=${searchTerm}&ordering=name`, - fetcher + const { data: companylist, isLoading } = useSWR( + `${servedAddress}/api/search/?name=${searchTerm}&ordering=name&page_size=${pageSize}&page=${currentPage}`, + fetcher, + {onSuccess: (data) => setSearchResults(data.results)} ); const changeCompanies = (id, isSaved) => { @@ -42,116 +40,90 @@ export function Search({ isAuthorized }) { setSearchResults(newCompanies); }; - useEffect(() => { - if (searchTerm) { - setSearchPerformed(true); - } - }, [searchTerm, servedAddress, searchUrl, companylist]); + const windowWidth = useWindowWidth(); useEffect(() => { - setCurrentPage(pageNumber); - }, [pageNumber]); - - const [currentPage, setCurrentPage] = useState(pageNumber); - const totalItems = searchResults.length; - const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE); - - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const displayedResults = searchResults.slice(startIndex, endIndex); + if (windowWidth < 768) { + setPageSize(4); + } else if (windowWidth >= 768 && windowWidth < 1200) { + setPageSize(16); + } else if (windowWidth >= 1200 && windowWidth < 1512) { + setPageSize(12); + } else if (windowWidth >= 1512) { + setPageSize(16); + } + }, [windowWidth]); - const handlePageChange = (newPage) => { - setCurrentPage(newPage); + const updateQueryParams = (newPage) => { searchParams.set('page', newPage); - navigate(`?${searchParams.toString()}`); + setSearchParams(searchParams); }; + const handlePageChange = (page) => { + setCurrentPage(page); + updateQueryParams(page); + }; + + useEffect(() => { + if (companylist?.total_items === 0) { + setCurrentPage(1); + searchParams.delete('page'); + setSearchParams(searchParams); + } else { + const totalPages = Math.ceil(companylist?.total_items / pageSize); + if (currentPage > totalPages) { + setCurrentPage(totalPages); + updateQueryParams(totalPages); + } + } + }, [companylist?.total_items, pageSize, currentPage]); + return ( -
-
-
- {searchResults && ( -
-

- Результати пошуку - - {` “${searchTerm}” `} - - : {searchResults.length > 0 ? searchResults.length : 0} -

-
- )} -
- {!error && searchResults.length > 0 && ( +
+
+ {isLoading ? + : ( <> - -
- {totalPages > 1 && ( -
- {currentPage > 1 && ( - - )} - {currentPage > 1 && ( - <> - - {currentPage > 2 && ( - ... - )} - - )} - {Array.from({ length: totalPages }, (_, i) => { - if ( - i === 2 || - i === totalPages || - (i >= currentPage - 1 && i <= currentPage) - ) { - return ( - - ); - } - return null; - })} - {currentPage < totalPages - 1 && ( - <> - {currentPage < totalPages - 1 && ( - ... - )} - - - )} - {currentPage < totalPages && ( - - )} +
+ {searchResults && ( +
+

+ Результати пошуку + + {` “${searchTerm}” `} + + : {companylist?.total_items || 0} +

)}
+
+ +
)}
- {searchResults.length === 0 && -
-

+ {companylist?.total_items === 0 && +

+

Пошук не дав результатів

-
} +
+ }
); } diff --git a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx b/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx deleted file mode 100644 index 8871d4fac..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Row, Col} from 'antd'; -import CompanyCard from '../../../components/CompanyCard/CompanyCard'; -import styles from './SearchResults.module.css'; -import PropTypes from 'prop-types'; - -const SearchResults = ({ - results, - displayedResults, - isAuthorized, - changeCompanies, -}) => { - let error = null; - - if (results && results.error) { - error = results.error; - } - - return ( -
- {!error && ( - - {displayedResults.map((result, resultIndex) => ( - - - - ))} - - )} -
- ); -}; - -export default SearchResults; - -SearchResults.propTypes = { - results: PropTypes.array, - displayedResults: PropTypes.array, - isAuthorized: PropTypes.bool, - changeCompanies: PropTypes.func, -}; diff --git a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css b/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css deleted file mode 100644 index ad8067862..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.new-companies-block { - width: 375px; - padding: 18px 0px 38px 0px; -} - - -@media only screen and (min-width: 768px) { - .new-companies-block { - width: 768px; - } -} - -@media only screen and (min-width: 1512px) { - .new-companies-block { - width: 1304px; - } -} - diff --git a/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js b/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js deleted file mode 100644 index 2ec9fbc7f..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import { render, screen, cleanup } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -import SearchResults from '../SearchResults'; - -const mockedUser = { - email: 'test@test.com', - id: 1, - name: 'Test', - profile_id: 1, - surname: 'Test' -}; -jest.mock('../../../../hooks/useAuth', () => ({ - useAuth: () => { - return mockedUser; - }, -})); - -afterEach(cleanup); - -describe('SearchResults component unit tests', () => { - test('renders search results', () => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - const results = [ - { - id: 1, - name: 'saleonline', - categories: [ - { - id: 1, - name: 'trade', - }, - { - id: 2, - name: 'transport', - }, - ], - region: 'Dnipro', - founded: 1980, - service_info: null, - address: 'Kyiv', - banner: null, - is_saved: true, - }, - { - id: 2, - name: 'sale', - categories: [ - { - id: 1, - name: 'trade', - }, - ], - region: 'Dnipro', - founded: 2007, - service_info: null, - address: 'Dnipro', - banner: null, - is_saved: false, - }, - { - id: 3, - name: 'PizzaHousesale', - categories: [], - region: 'Charkiv', - founded: null, - service_info: null, - address: 'Zaporija', - banner: null, - }, - { - id: 4, - name: 'salefruits', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 5, - name: 'salevegetables', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 6, - name: 'salebushes', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 7, - name: 'GGGsale', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - ]; - - const displayedResults = results.slice(0, 7); - render( - - - - ); - const nameElement = screen.getAllByText(/sale/i, { exact: false }); - expect(nameElement).toHaveLength(results.length); - }); -}); diff --git a/FrontEnd/src/pages/SearchPage/img/link_to_left.svg b/FrontEnd/src/pages/SearchPage/img/link_to_left.svg deleted file mode 100644 index 849379319..000000000 --- a/FrontEnd/src/pages/SearchPage/img/link_to_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/FrontEnd/src/pages/SearchPage/img/link_to_right.svg b/FrontEnd/src/pages/SearchPage/img/link_to_right.svg deleted file mode 100644 index 450b2ecdc..000000000 --- a/FrontEnd/src/pages/SearchPage/img/link_to_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/FrontEnd/src/pages/SearchPage/search.module.scss b/FrontEnd/src/pages/SearchPage/search.module.scss index 7ed26a476..d2137f902 100644 --- a/FrontEnd/src/pages/SearchPage/search.module.scss +++ b/FrontEnd/src/pages/SearchPage/search.module.scss @@ -1,46 +1,44 @@ -.main_block_outer { +.search-page__outer { display: flex; flex-direction: column; position: relative; } -.new-companies-result_pages { - padding: 40px; -} - -.new-companies-main__error { - width: 100%; - background-color: rgba(249, 245, 236, 1); - display: flex; - justify-content: center; - align-items: center; - min-height: 288px; - flex-grow: 1; -} - -.new-companies-main { +.search-page { display: flex; align-self: center; align-items: flex-start; flex-direction: column; background: var(--new-companies-background-color); + box-sizing: border-box; min-width: 375px; + padding: 40px 16px; + gap: 32px; } -.new-companies-search_count { +.search-page__empty { + height: 102px; +} + +.search-list__content--items { + display: flex; + flex-direction: column; + width: 100%; +} + +.search-page__results-count { display: flex; flex-direction: row; - padding: 40px 16px; } -.search_field_entered_value { +.search-field__entered-value { color: #707070; font: var(--font-main); font-weight: 300; font-size: 18px ; } -.search_results_text { +.search-results__text { font: normal normal bold 18px/2 var(--font-main); font-size: 18px; font-weight: 700; @@ -48,72 +46,78 @@ line-height: 22px; } -.search_result_error { +.search-page__error { width: 100%; - color: var(--search-text-color); - font: normal normal bold 24px/2 var(--font-main); - text-align: center; - white-space: nowrap; -} - -.pagination { + background-color: rgba(249, 245, 236, 1); display: flex; justify-content: center; align-items: center; - margin-top: 20px; -} - -.pagination button { - all: unset; - font-size: 16px; - font-weight: 500; - padding: 8px 16px; - margin: 0 4px; - cursor: pointer; + min-height: 288px; + flex-grow: 1; } -.pagination button.active { - background-color: transparent; +.search_result__error { + width: 100%; color: var(--search-text-color); - border: 1px solid var(--search-text-color); - border-radius: 5px; - transition: background-color 0.3s, color 0.3s; + font: normal normal bold 24px/2 var(--font-main); + text-align: center; + white-space: nowrap; } - - - @media only screen and (min-width: 768px){ - .search_results_text { + .search-results__text { font-size: 34px; line-height: 41px; } - .search_field_entered_value{ + .search-field__entered-value{ font-size: 34px; } - .search_result_error { + .search-result__error { font: normal normal bold 40px/2 var(--font-main); } - .new-companies-search_count { - padding: 40px 24px; + .search-page { + width: 768px; + padding: 40px 27px; } - .new-companies-main { - width: 768px; + .search-page__empty { + height: 121px; } } -@media only screen and (min-width: 1512px){ - .new-companies-search_count { - padding: 40px 14px; +@media only screen and (min-width: 1200px){ + .search-results__text { + font-size: 34px; + line-height: 41px; + } + + .search-field__entered-value{ + font-size: 34px; + } + + .search-result__error { + font: normal normal bold 40px/2 var(--font-main); } - .new-companies-main { - width: 1304px; + .search-page { + width: 1200px; + padding: 40px 35px; + } + + .search-page__error { + min-height: 480px; + } + +} + +@media only screen and (min-width: 1512px){ + .search-page { + width: 1512px; + padding: 40px 107px; } } diff --git a/FrontEnd/src/pages/SearchPage/tests/Search.test.js b/FrontEnd/src/pages/SearchPage/tests/Search.test.js index 57ad370df..15e66ec70 100644 --- a/FrontEnd/src/pages/SearchPage/tests/Search.test.js +++ b/FrontEnd/src/pages/SearchPage/tests/Search.test.js @@ -1,95 +1,246 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, cleanup, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; import Search from '../Search'; +import ProfileList from '../../ProfileList/ProfileList'; -jest.mock('../../../hooks', () => ({ - useAuth: () => ({isStaff: false}), +const mockedUser = { + email: 'test@test.com', + id: 1, + name: 'Test', + profile_id: 1, + surname: 'Test' +}; + +jest.mock('../../../hooks/useAuth', () => ({ + useAuth: () => { + return mockedUser; + }, })); -afterEach(() => { - jest.resetAllMocks(); +jest.mock('axios'); + +beforeEach(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); }); +afterEach(cleanup); + describe('Search component unit tests', () => { - test('renders sale search page', () => { - render( - - - - ); + test('renders sale search page', async () => { + await act(async () => { + render( + + + + ); + }); const counterElement = screen.getByText(/Результати пошуку/i, { exact: false, }); expect(counterElement).toBeInTheDocument(); }); - test('testing search', () => { - jest.mock('axios'); - - const axios = require('axios', () => { - jest.fn().mockResolvedValue({}); - - () => { - axios.get.mockResolvedValue({ - data: [ - { - id: 1, - name: 'saleonline', - categories: [ - { - id: 1, - name: 'trade', - }, - { - id: 2, - name: 'transport', - }, - ], - region: 'Dnipro', - founded: 1980, - service_info: null, - address: 'Kyiv', - banner: null, - }, - { - id: 2, - name: 'sale', - categories: [ - { - id: 1, - name: 'trade', - }, - ], - region: 'Dnipro', - founded: 2007, - service_info: null, - address: 'Dnipro', - banner: null, - }, - { - id: 3, - name: 'PizzaHousesale', - categories: [], - region: 'Charkiv', - founded: null, - service_info: null, - address: 'Zaporija', - banner: null, - }, - ], - }); - }; + test('testing search', async () => { + axios.get.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'saleonline', + categories: [ + { + id: 1, + name: 'trade', + }, + { + id: 2, + name: 'transport', + }, + ], + region: 'Dnipro', + founded: 1980, + service_info: null, + address: 'Kyiv', + banner: null, + }, + { + id: 2, + name: 'sale', + categories: [ + { + id: 1, + name: 'trade', + }, + ], + region: 'Dnipro', + founded: 2007, + service_info: null, + address: 'Dnipro', + banner: null, + }, + { + id: 3, + name: 'PizzaHousesale', + categories: [], + region: 'Charkiv', + founded: null, + service_info: null, + address: 'Zaporija', + banner: null, + }, + ], + total_items: 3, + }, + }); + await act(async () => { render( ); - expect(axios.get).toBeCalled(); - expect(screen.getByText(/назад/i, { exact: false })).toBeInTheDocument(); - expect(screen.getByRole('link')).toHaveAttribute('href', '/'); - expect(screen.getByText(/3/i, { exact: false })).toBeInTheDocument(); }); + + expect(axios.get).toBeCalled(); + const links = screen.getAllByRole('link'); + links.forEach(link => {expect(link).toHaveAttribute('href');}); + expect(screen.getByText(/Результати пошуку/i, { exact: false })).toBeInTheDocument(); + expect(screen.getByText(/3/i, { exact: false })).toBeInTheDocument(); + }); +}); + + +describe('Search results unit tests', () => { + test('renders search results', async () => { + const results = [ + { + id: 1, + name: 'saleonline', + categories: [ + { + id: 1, + name: 'trade', + }, + { + id: 2, + name: 'transport', + }, + ], + region: 'Dnipro', + founded: 1980, + service_info: null, + address: 'Kyiv', + banner: null, + is_saved: true, + }, + { + id: 2, + name: 'sale', + categories: [ + { + id: 1, + name: 'trade', + }, + ], + region: 'Dnipro', + founded: 2007, + service_info: null, + address: 'Dnipro', + banner: null, + is_saved: false, + }, + { + id: 3, + name: 'PizzaHousesale', + categories: [], + region: 'Charkiv', + founded: null, + service_info: null, + address: 'Zaporija', + banner: null, + }, + { + id: 4, + name: 'salefruits', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 5, + name: 'salevegetables', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 6, + name: 'salebushes', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 7, + name: 'GGGsale', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + ]; + + const totalItems = 7; + + axios.get.mockResolvedValue({ + data: { + results: results, + total_items: totalItems, + }, + }); + + await act(async () => { + render( + + + + ); + }); + + const nameElements = screen.getAllByText(/sale/i, { exact: false }); + expect(nameElements).toHaveLength(results.length); }); }); From 7a9e85f5e52b2b757d7ed64aca018245575f1ef5 Mon Sep 17 00:00:00 2001 From: romanmyko Date: Thu, 5 Dec 2024 17:41:40 +0200 Subject: [PATCH 04/80] add categori api --- BackEnd/administration/filters.py | 9 +++++++ BackEnd/administration/serializers.py | 16 +++++++++++ BackEnd/administration/urls.py | 12 +++++++++ BackEnd/administration/views.py | 39 +++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/BackEnd/administration/filters.py b/BackEnd/administration/filters.py index 2a522ddc5..217832eb8 100644 --- a/BackEnd/administration/filters.py +++ b/BackEnd/administration/filters.py @@ -43,3 +43,12 @@ def filter_is_deleted(self, queryset, name, value): ("profile__created_at", "registration_date"), ) ) + + +class CategoriFilter(FilterSet): + """ + manage categori filter + """ + + id = filters.CharFilter(lookup_expr="icontains") + name = filters.CharFilter(lookup_expr="icontains") diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index 24cda9beb..8eb3d8937 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -5,6 +5,7 @@ from profiles.models import ( Profile, Region, + Category, ) from utils.administration.create_password import generate_password from utils.administration.send_email import send_email_about_admin_registration @@ -213,3 +214,18 @@ class FeedbackSerializer(serializers.Serializer): required=True, error_messages={"required": "Please select a category."}, ) + + +class ManageCategoriesSerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ( + "id", + "name", + ) + + +class CategorieDetailSerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ("name",) diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index d860830c3..cd5f9049c 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -10,6 +10,8 @@ ModerationEmailView, FeedbackView, CreateAdminUserView, + ManageCategoriesView, + CategoriesDetailView, ) app_name = "administration" @@ -28,4 +30,14 @@ path("contacts/", ContactsView.as_view(), name="contacts"), path("feedback/", FeedbackView.as_view(), name="feedback"), path("admin_create/", CreateAdminUserView.as_view(), name="admin-create"), + path( + "manage_categories/", + ManageCategoriesView.as_view(), + name="manage-categories", + ), + path( + "manage_categories//", + CategoriesDetailView.as_view(), + name="categories_detail", + ), ] diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 7f137a5bc..04b0b125c 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -8,6 +8,7 @@ from rest_framework.generics import ( ListAPIView, + ListCreateAPIView, RetrieveUpdateDestroyAPIView, RetrieveUpdateAPIView, CreateAPIView, @@ -22,17 +23,19 @@ AdminUserDetailSerializer, AutoModerationHoursSerializer, ModerationEmailSerializer, + ManageCategoriesSerializer, + CategorieDetailSerializer, ) from administration.pagination import ListPagination from administration.models import AutoModeration, ModerationEmail from authentication.models import CustomUser -from profiles.models import Profile +from profiles.models import Profile, Category from .permissions import IsStaffUser, IsStaffUserOrReadOnly, IsSuperUser from .serializers import FeedbackSerializer from utils.administration.send_email_feedback import send_email_feedback from django_filters.rest_framework import DjangoFilterBackend -from .filters import UsersFilter +from .filters import UsersFilter, CategoriFilter class UsersListView(ListAPIView): @@ -199,3 +202,35 @@ def perform_create(self, serializer): category = serializer.validated_data["category"] send_email_feedback(email, message, category) + + +class ManageCategoriesView(ListCreateAPIView): + """ + Manage categories + ### Query Parameters: + - **id** / **name** + + ### Ordering: + - Use the `ordering` parameter to sort the results. + - Example: `/manage_categori/?ordering=id` (ascending by ID) or `/manage_categori/?ordering=-id` (descending by ID). + + ### Filters: + - Filters are applied using `DjangoFilterBackend`. All the above query parameters are supported for filtering. + """ + + permission_classes = [IsStaffUser] + serializer_class = ManageCategoriesSerializer + filter_backends = [DjangoFilterBackend] + filterset_class = CategoriFilter + pagination_class = ListPagination + queryset = Category.objects.all().order_by("id") + + +class CategoriesDetailView(RetrieveUpdateAPIView): + """ + Modify activity category + """ + + permission_classes = [IsStaffUser] + serializer_class = CategorieDetailSerializer + queryset = Category.objects.all() From e20229f8524d5cd952d1ae4a49b4e7005c365a35 Mon Sep 17 00:00:00 2001 From: romanmyko Date: Thu, 5 Dec 2024 18:08:16 +0200 Subject: [PATCH 05/80] api name categori --- BackEnd/administration/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index cd5f9049c..51f743bca 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -31,12 +31,12 @@ path("feedback/", FeedbackView.as_view(), name="feedback"), path("admin_create/", CreateAdminUserView.as_view(), name="admin-create"), path( - "manage_categories/", + "categories/", ManageCategoriesView.as_view(), - name="manage-categories", + name="categories", ), path( - "manage_categories//", + "categories//", CategoriesDetailView.as_view(), name="categories_detail", ), From c0f70999d63a78db258284974cd234ebed8a06d1 Mon Sep 17 00:00:00 2001 From: Andrewakiv Date: Thu, 5 Dec 2024 18:33:30 +0200 Subject: [PATCH 06/80] companies stats --- FrontEnd/src/pages/AdminPage/Menu/Menu.jsx | 41 ++++++++++++++++++- .../src/pages/AdminPage/Menu/Menu.module.css | 11 +++++ .../UserProfilesTable/ProfilesStatistics.jsx | 30 ++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx diff --git a/FrontEnd/src/pages/AdminPage/Menu/Menu.jsx b/FrontEnd/src/pages/AdminPage/Menu/Menu.jsx index 70cd9910d..a5095f20d 100644 --- a/FrontEnd/src/pages/AdminPage/Menu/Menu.jsx +++ b/FrontEnd/src/pages/AdminPage/Menu/Menu.jsx @@ -1,7 +1,33 @@ -import { NavLink } from 'react-router-dom'; +import { NavLink, useLocation } from 'react-router-dom'; import { useAuth } from '../../../hooks'; import css from './Menu.module.css'; +import React from 'react'; +import { Descriptions } from 'antd'; + +const items = [ + { + key: '1', + label: 'Кількість зареєстрованих компаній', + children: '2', + }, + { + key: '2', + label: 'Кількість Інвесторів', + children: '22', + }, + { + key: '3', + label: 'Кількість стратапів', + children: '222', + }, + { + key: '4', + label: 'Кількість заблокованих компаній', + children: '2222', + } + ]; + const MENU = [ { id: 'am1', @@ -25,6 +51,17 @@ const MENU = [ }, ]; +const ProfilesStatistics = () => ({ + ...item, + label: {item.label}, + children: {item.children} + }))} />; + const SUPERUSER_MENU = [ { id: 'am5', @@ -41,6 +78,7 @@ const SUPERUSER_MENU = [ function Menu() { const { isSuperUser } = useAuth(); + const location = useLocation(); return (
@@ -53,6 +91,7 @@ function Menu() { key={element.id} to={element.link}>{element.title} ))} + {location.pathname === '/customadmin/profiles/' && }
); } diff --git a/FrontEnd/src/pages/AdminPage/Menu/Menu.module.css b/FrontEnd/src/pages/AdminPage/Menu/Menu.module.css index 93170f61f..5cf96a2d4 100644 --- a/FrontEnd/src/pages/AdminPage/Menu/Menu.module.css +++ b/FrontEnd/src/pages/AdminPage/Menu/Menu.module.css @@ -22,3 +22,14 @@ .menu-section-element__active { font-weight: 600; } + +.description-item-label { + max-width: 150px; +} +.description-item-content { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx new file mode 100644 index 000000000..e0ad51071 --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Descriptions } from 'antd'; +const items = [ + { + key: '1', + label: 'Product', + children: 'Cloud Database', + span: 3 + }, + { + key: '2', + label: 'Billing Mode', + children: 'Prepaid', + span: 3 + }, + { + key: '3', + label: 'Automatic Renewal', + children: 'YES', + span: 3 + }, + { + key: '4', + label: 'Automatic Renewal', + children: 'YES', + span: 3 + } +]; +const ProfilesStatistics = () => ; +export default ProfilesStatistics; \ No newline at end of file From 0153017bd0e957a0c55189cbb97e1b3d924590be Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 6 Dec 2024 08:10:27 +0200 Subject: [PATCH 07/80] Add Cancel button, change buttons and input styles, add head text --- .../user_feedback_template.html | 2 +- .../AutoApproveDelay/AutoApproveDelay.jsx | 11 ++++++- .../AutoApproveDelay.module.css | 30 ++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/BackEnd/administration/templates/administration/user_feedback_template.html b/BackEnd/administration/templates/administration/user_feedback_template.html index df486d219..a0030c2b8 100644 --- a/BackEnd/administration/templates/administration/user_feedback_template.html +++ b/BackEnd/administration/templates/administration/user_feedback_template.html @@ -24,7 +24,7 @@