From a2f486bbb356c2d6afd4ae0c6e3199df1e81a950 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jan 2025 22:04:41 +0200 Subject: [PATCH 01/31] Added bar chart for types of activities of profiles in statistics section in admin panel --- BackEnd/administration/serializers.py | 4 + BackEnd/administration/views.py | 5 ++ FrontEnd/package-lock.json | 30 +++++++ FrontEnd/package.json | 2 + .../UserProfilesTable/ProfilesStatistics.jsx | 90 +++++++++++++++---- .../ProfilesStatistics.module.css | 8 ++ 6 files changed, 121 insertions(+), 18 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index c03ec1238..6d6a42fe9 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -269,3 +269,7 @@ class StatisticsSerializer(serializers.Serializer): investors_count = serializers.IntegerField() startups_count = serializers.IntegerField() blocked_companies_count = serializers.IntegerField() + manufacturers_count = serializers.IntegerField() + importers_count = serializers.IntegerField() + retail_networks_count = serializers.IntegerField() + others_count = serializers.IntegerField() diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index b779988e0..55fbff422 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -162,6 +162,11 @@ def get_object(self): investors_count=Count("pk", filter=Q(is_registered=True)), startups_count=Count("pk", filter=Q(is_startup=True)), blocked_companies_count=Count("pk", filter=Q(status="blocked")), + manufacturers_count=Count("pk", filter=Q(activities__name="Виробник")), + importers_count=Count("pk", filter=Q(activities__name="Імпортер")), + retail_networks_count=Count("pk", filter=Q(activities__name="Роздрібна мережа")), + others_count=Count("pk", filter=Q(activities__name="Інші послуги")), + ) diff --git a/FrontEnd/package-lock.json b/FrontEnd/package-lock.json index 6d4200ac0..9cd85068a 100644 --- a/FrontEnd/package-lock.json +++ b/FrontEnd/package-lock.json @@ -18,9 +18,11 @@ "@testing-library/user-event": "^13.5.0", "antd": "^5.21.5", "axios": "^1.5.0", + "chart.js": "^4.4.7", "hamburger-react": "^2.5.1", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-cookie": "^6.1.0", "react-dom": "^18.2.0", "react-google-recaptcha": "^3.1.0", @@ -3822,6 +3824,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -6744,6 +6752,18 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", @@ -17553,6 +17573,16 @@ "react": ">=16.4.1" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-cookie": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-6.1.0.tgz", diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 8115027b2..2a06a3692 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -19,9 +19,11 @@ "@testing-library/user-event": "^13.5.0", "antd": "^5.21.5", "axios": "^1.5.0", + "chart.js": "^4.4.7", "hamburger-react": "^2.5.1", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-cookie": "^6.1.0", "react-dom": "^18.2.0", "react-google-recaptcha": "^3.1.0", diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 3a8a07efd..b2b196242 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -3,6 +3,26 @@ import useSWR from 'swr'; import { Descriptions } from 'antd'; import Loader from '../../../components/Loader/Loader'; import css from './ProfilesStatistics.module.css'; +import React from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); async function fetcher(url) { const response = await axios.get(url); @@ -28,7 +48,7 @@ function ProfilesStatistics() { }, { key: '3', - label: 'Кількість Cтратапів', + label: 'Кількість Cтартапів', children: statistics.startups_count, }, { @@ -39,6 +59,37 @@ function ProfilesStatistics() { ] : []; + const chartData = statistics + ?{ + labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'Інші'], + datasets: [ + { + label: 'Типи компаній', + data: [ + statistics.manufacturers_count, + statistics.importers_count, + statistics.retail_networks_count, + statistics.others_count + ], + backgroundColor: [ + '#87f3b0', + ] + }, + ] + }: { labels: [], datasets: [] }; + const options = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Статистика по типам компаній', + }, + }, + maintainAspectRatio: false, + }; return isLoading ? (
@@ -46,25 +97,28 @@ function ProfilesStatistics() { ) : error ? (
Не вдалося отримати статистику компаній
) : ( -
- ({ - ...item, - label: ( - {item.label} - ), - children: ( - +
+ ({ + ...item, + label: ( + {item.label} + ), + children: ( + {item.children} - ), - }))} - /> -
+ ), + }))} + /> +
+ +
+
); } diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css index 8c86f64ca..9403ab407 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css @@ -6,6 +6,14 @@ height: 100%; padding: 30px; } +.chart-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 600px; +} .description-item-label { max-width: 150px; From 0cd26daa84c455e600443c77cecc371d012b58f0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 10:28:17 +0200 Subject: [PATCH 02/31] Merged develop --- .../UserProfilesTable/ProfilesStatistics.jsx | 153 ++++++++---------- 1 file changed, 63 insertions(+), 90 deletions(-) diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 8553a3f72..5fa929faf 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -85,73 +85,6 @@ function ProfilesStatistics() { const handleRangeChange = (value, dateString) => { setPeriodRange({ start_date: dateString[0], end_date: dateString[1] }); }; - - return ( -
-

Статистика компаній

- { - setTab(value); - setPeriodType(''); - setSelectedDate(null); - }} - /> - {tab === 'period' && ( -
- - {periodType === 'range' && ( - - )} - {periodType !== 'range' && ( - - )} -
- )} - {isLoading && ( -
- -
- )} - {error && ( -
Не вдалося отримати статистику компаній
- )} - {!isLoading && !error && ( - - )} -
const chartData = statistics ?{ labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'Інші'], @@ -183,31 +116,71 @@ function ProfilesStatistics() { }, maintainAspectRatio: false, }; - return isLoading ? ( -
- -
- ) : error ? ( -
Не вдалося отримати статистику компаній
- ) : ( + return (
- ({ - ...item, - label: ( - {item.label} - ), - children: ( - - {item.children} - - ), - }))} +

Статистика компаній

+ { + setTab(value); + setPeriodType(''); + setSelectedDate(null); + }} /> + {tab === 'period' && ( +
+ + {periodType === 'range' && ( + + )} + {periodType !== 'range' && ( + + )} +
+ )} + {isLoading && ( +
+ +
+ )} + {error && ( +
Не вдалося отримати статистику компаній
+ )} + {!isLoading && !error && ( + + )}
From db50f8d702933454bb2f60ffc7dc7f2b4514232f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 16:49:13 +0200 Subject: [PATCH 03/31] Added a new api point for statistics of company activities --- BackEnd/administration/serializers.py | 3 +++ BackEnd/administration/views.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index 6d6a42fe9..30198c9a5 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -269,6 +269,9 @@ class StatisticsSerializer(serializers.Serializer): investors_count = serializers.IntegerField() startups_count = serializers.IntegerField() blocked_companies_count = serializers.IntegerField() + + +class StatisticsActivitiesSerializer(serializers.Serializer): manufacturers_count = serializers.IntegerField() importers_count = serializers.IntegerField() retail_networks_count = serializers.IntegerField() diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 55fbff422..4893f84f8 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -21,7 +21,7 @@ CreateAPIView, ) -from administration.serializers import AdminRegistrationSerializer +from administration.serializers import AdminRegistrationSerializer, StatisticsActivitiesSerializer from forum.settings import CONTACTS_INFO from administration.serializers import ( AdminCompanyListSerializer, @@ -162,11 +162,24 @@ def get_object(self): investors_count=Count("pk", filter=Q(is_registered=True)), startups_count=Count("pk", filter=Q(is_startup=True)), blocked_companies_count=Count("pk", filter=Q(status="blocked")), + ) + + +class ProfileStatisticsActivitiesView(RetrieveAPIView): + """ + Count of companies in terms of their activities + """ + + permission_classes = [IsStaffUser] + serializer_class = StatisticsActivitiesSerializer + + def get_object(self): + queryset = self.filter_queryset(Profile.objects.all()) + return queryset.aggregate( manufacturers_count=Count("pk", filter=Q(activities__name="Виробник")), importers_count=Count("pk", filter=Q(activities__name="Імпортер")), retail_networks_count=Count("pk", filter=Q(activities__name="Роздрібна мережа")), others_count=Count("pk", filter=Q(activities__name="Інші послуги")), - ) From 5f58eff27a61bbd9ac1b93f8dd0cbe98cb5204c1 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 16:49:20 +0200 Subject: [PATCH 04/31] Added a new api point for statistics of company activities --- BackEnd/administration/urls.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index 83e19e306..b6582fc7a 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -13,7 +13,7 @@ CreateAdminUserView, CategoriesListView, CategoryDetailView, - SendMessageView, + SendMessageView, ProfileStatisticsActivitiesView, ) app_name = "administration" @@ -27,6 +27,11 @@ ProfileStatisticsView.as_view(), name="profile-statistics", ), + path( + "profiles/statistics-activities/", + ProfileStatisticsActivitiesView.as_view(), + name="profile-statistics-activities", + ), path("profiles//", ProfileDetailView.as_view(), name="profile-detail"), path( "automoderation/", From 8a7789460b3e3dfc97b66f32846fa6797a6e4459 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 16:54:22 +0200 Subject: [PATCH 05/31] Adjusted frontend for a new api point --- .../UserProfilesTable/ProfilesStatistics.jsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 5fa929faf..83e2b3b2f 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -50,8 +50,10 @@ function ProfilesStatistics() { const url = `${baseUrl}/api/admin/profiles/statistics/${ queryParams.length ? `?${queryParams.join('&')}` : '' }`; + const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; const { data: statistics, error, isLoading } = useSWR(url, fetcher); + const { data: activities, error: activitiesError, isLoading: activitiesLoading } = useSWR(activities_url, fetcher); const items = statistics ? [ @@ -85,17 +87,17 @@ function ProfilesStatistics() { const handleRangeChange = (value, dateString) => { setPeriodRange({ start_date: dateString[0], end_date: dateString[1] }); }; - const chartData = statistics + const chartData = activities ?{ labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'Інші'], datasets: [ { label: 'Типи компаній', data: [ - statistics.manufacturers_count, - statistics.importers_count, - statistics.retail_networks_count, - statistics.others_count + activities.manufacturers_count, + activities.importers_count, + activities.retail_networks_count, + activities.others_count ], backgroundColor: [ '#87f3b0', @@ -181,9 +183,20 @@ function ProfilesStatistics() { items={items} /> )} -
- -
+ {activitiesLoading && ( +
+ +
+ )} + {activitiesError && ( +
Не вдалося отримати статистику компаній
+ )} + {!activitiesLoading && !activitiesError && ( +
+ +
+ ) + }
); } From cf8920dd1ad3c7e0e66879857253062b617418db Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 17:15:06 +0200 Subject: [PATCH 06/31] Created backend tests for profiles statistics about activities --- .../tests/test_statistics_activities.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 BackEnd/administration/tests/test_statistics_activities.py diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py new file mode 100644 index 000000000..760f47599 --- /dev/null +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -0,0 +1,51 @@ +from rest_framework.test import APITestCase +from administration.factories import AdminUserFactory, AdminProfileFactory +from rest_framework import status +from profiles.models import Activity, Profile + + +class ProfileStatisticsActivitiesTest(APITestCase): + def setUp(self): + self.user = AdminUserFactory() + self.activities = { + "Виробник": Activity.objects.create(name="Виробник"), + "Імпортер": Activity.objects.create(name="Імпортер"), + "Роздрібна мережа": Activity.objects.create(name="Роздрібна мережа"), + "Інші послуги": Activity.objects.create(name="Інші послуги"), + } + self.profiles = [] + profile1 = AdminProfileFactory() + profile1.activities.set([self.activities["Виробник"]]) + self.profiles.append(profile1) + + profile2 = AdminProfileFactory() + profile2.activities.set([self.activities["Імпортер"]]) + self.profiles.append(profile2) + + profile3 = AdminProfileFactory() + profile3.activities.set([self.activities["Роздрібна мережа"]]) + self.profiles.append(profile3) + + profile4 = AdminProfileFactory() + profile4.activities.set([self.activities["Інші послуги"]]) + self.profiles.append(profile4) + + profile5 = AdminProfileFactory() + profile5.activities.set([self.activities["Виробник"]]) + self.profiles.append(profile5) + self.url = "/api/admin/profiles/statistics-activities/" + + def test_statistics_view_requires_authentication(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_statistics_view_with_valid_user(self): + self.client.force_authenticate(user=self.user) + response = self.client.get(self.url) + data = { + "manufacturers_count": 2, + "importers_count": 1, + "retail_networks_count": 1, + "others_count": 1, + } + self.assertEqual(response.json(), data) From 286b46facfea46181ae221a67d91a5baee57f9ae Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 23:05:38 +0200 Subject: [PATCH 07/31] Created backend tests for profiles statistics about activities --- BackEnd/administration/tests/test_statistics_activities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py index 760f47599..7b73735be 100644 --- a/BackEnd/administration/tests/test_statistics_activities.py +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -1,7 +1,7 @@ from rest_framework.test import APITestCase from administration.factories import AdminUserFactory, AdminProfileFactory from rest_framework import status -from profiles.models import Activity, Profile +from profiles.models import Activity class ProfileStatisticsActivitiesTest(APITestCase): From f824311ef7ac46cf4d4b077ab24e3d161c1bbb80 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Jan 2025 13:17:38 +0200 Subject: [PATCH 08/31] Reformatted backend --- .../tests/test_statistics_activities.py | 4 +++- BackEnd/administration/urls.py | 3 ++- BackEnd/administration/views.py | 17 +++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py index 7b73735be..34a768216 100644 --- a/BackEnd/administration/tests/test_statistics_activities.py +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -10,7 +10,9 @@ def setUp(self): self.activities = { "Виробник": Activity.objects.create(name="Виробник"), "Імпортер": Activity.objects.create(name="Імпортер"), - "Роздрібна мережа": Activity.objects.create(name="Роздрібна мережа"), + "Роздрібна мережа": Activity.objects.create( + name="Роздрібна мережа" + ), "Інші послуги": Activity.objects.create(name="Інші послуги"), } self.profiles = [] diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index b6582fc7a..8a97b8dd1 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -13,7 +13,8 @@ CreateAdminUserView, CategoriesListView, CategoryDetailView, - SendMessageView, ProfileStatisticsActivitiesView, + SendMessageView, + ProfileStatisticsActivitiesView, ) app_name = "administration" diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 4893f84f8..0a559148a 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -21,7 +21,10 @@ CreateAPIView, ) -from administration.serializers import AdminRegistrationSerializer, StatisticsActivitiesSerializer +from administration.serializers import ( + AdminRegistrationSerializer, + StatisticsActivitiesSerializer, +) from forum.settings import CONTACTS_INFO from administration.serializers import ( AdminCompanyListSerializer, @@ -176,10 +179,16 @@ class ProfileStatisticsActivitiesView(RetrieveAPIView): def get_object(self): queryset = self.filter_queryset(Profile.objects.all()) return queryset.aggregate( - manufacturers_count=Count("pk", filter=Q(activities__name="Виробник")), + manufacturers_count=Count( + "pk", filter=Q(activities__name="Виробник") + ), importers_count=Count("pk", filter=Q(activities__name="Імпортер")), - retail_networks_count=Count("pk", filter=Q(activities__name="Роздрібна мережа")), - others_count=Count("pk", filter=Q(activities__name="Інші послуги")), + retail_networks_count=Count( + "pk", filter=Q(activities__name="Роздрібна мережа") + ), + others_count=Count( + "pk", filter=Q(activities__name="Інші послуги") + ), ) From e60d7997c042fbd7cde6afbcbb63571f64caa663 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Jan 2025 18:34:23 +0200 Subject: [PATCH 09/31] Added horeca category --- BackEnd/administration/serializers.py | 1 + BackEnd/administration/views.py | 1 + .../pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx | 3 ++- .../AdminPage/UserProfilesTable/ProfilesStatistics.module.css | 3 +-- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index 78a49d0af..c210b99a5 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -275,6 +275,7 @@ class StatisticsActivitiesSerializer(serializers.Serializer): manufacturers_count = serializers.IntegerField() importers_count = serializers.IntegerField() retail_networks_count = serializers.IntegerField() + horeca_count = serializers.IntegerField() others_count = serializers.IntegerField() diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 8cd664bdb..7df576f10 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -192,6 +192,7 @@ def get_object(self): retail_networks_count=Count( "pk", filter=Q(activities__name="Роздрібна мережа") ), + horeca_count=Count("pk", filter=Q(activities__name="HORECA")), others_count=Count( "pk", filter=Q(activities__name="Інші послуги") ), diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 83e2b3b2f..923a4dc3d 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -89,7 +89,7 @@ function ProfilesStatistics() { }; const chartData = activities ?{ - labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'Інші'], + labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], datasets: [ { label: 'Типи компаній', @@ -97,6 +97,7 @@ function ProfilesStatistics() { activities.manufacturers_count, activities.importers_count, activities.retail_networks_count, + activities.horeca_count, activities.others_count ], backgroundColor: [ diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css index 7983949f3..9b4412fc6 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css @@ -11,8 +11,7 @@ flex-direction: column; justify-content: center; align-items: center; - height: 100%; - width: 600px; + height: 250px; } .statistics-title { From 01c28dbfe9b2a063b008eac71e29a98aedb33199 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Jan 2025 18:39:02 +0200 Subject: [PATCH 10/31] Fixed tests with HORECA activity --- .../administration/tests/test_statistics_activities.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py index 34a768216..62ac4ce59 100644 --- a/BackEnd/administration/tests/test_statistics_activities.py +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -13,23 +13,24 @@ def setUp(self): "Роздрібна мережа": Activity.objects.create( name="Роздрібна мережа" ), + "HORECA": Activity.objects.create(name="HORECA"), "Інші послуги": Activity.objects.create(name="Інші послуги"), } self.profiles = [] profile1 = AdminProfileFactory() - profile1.activities.set([self.activities["Виробник"]]) + profile1.activities.set([self.activities["Виробник"], self.activities["HORECA"]]) self.profiles.append(profile1) profile2 = AdminProfileFactory() - profile2.activities.set([self.activities["Імпортер"]]) + profile2.activities.set([self.activities["Імпортер"], self.activities["HORECA"]]) self.profiles.append(profile2) profile3 = AdminProfileFactory() - profile3.activities.set([self.activities["Роздрібна мережа"]]) + profile3.activities.set([self.activities["Роздрібна мережа"], self.activities["HORECA"]]) self.profiles.append(profile3) profile4 = AdminProfileFactory() - profile4.activities.set([self.activities["Інші послуги"]]) + profile4.activities.set([self.activities["Інші послуги"], self.activities["HORECA"]]) self.profiles.append(profile4) profile5 = AdminProfileFactory() @@ -48,6 +49,7 @@ def test_statistics_view_with_valid_user(self): "manufacturers_count": 2, "importers_count": 1, "retail_networks_count": 1, + "horeca_count": 4, "others_count": 1, } self.assertEqual(response.json(), data) From 67647be5e192555bb4597a97eb5b04d868a2cc0c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 10:54:47 +0200 Subject: [PATCH 11/31] Reformatted backend --- BackEnd/administration/serializers.py | 2 +- .../tests/test_statistics_activities.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index c210b99a5..f80de65c3 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -275,7 +275,7 @@ class StatisticsActivitiesSerializer(serializers.Serializer): manufacturers_count = serializers.IntegerField() importers_count = serializers.IntegerField() retail_networks_count = serializers.IntegerField() - horeca_count = serializers.IntegerField() + horeca_count = serializers.IntegerField() others_count = serializers.IntegerField() diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py index 62ac4ce59..1c3f80e13 100644 --- a/BackEnd/administration/tests/test_statistics_activities.py +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -18,19 +18,27 @@ def setUp(self): } self.profiles = [] profile1 = AdminProfileFactory() - profile1.activities.set([self.activities["Виробник"], self.activities["HORECA"]]) + profile1.activities.set( + [self.activities["Виробник"], self.activities["HORECA"]] + ) self.profiles.append(profile1) profile2 = AdminProfileFactory() - profile2.activities.set([self.activities["Імпортер"], self.activities["HORECA"]]) + profile2.activities.set( + [self.activities["Імпортер"], self.activities["HORECA"]] + ) self.profiles.append(profile2) profile3 = AdminProfileFactory() - profile3.activities.set([self.activities["Роздрібна мережа"], self.activities["HORECA"]]) + profile3.activities.set( + [self.activities["Роздрібна мережа"], self.activities["HORECA"]] + ) self.profiles.append(profile3) profile4 = AdminProfileFactory() - profile4.activities.set([self.activities["Інші послуги"], self.activities["HORECA"]]) + profile4.activities.set( + [self.activities["Інші послуги"], self.activities["HORECA"]] + ) self.profiles.append(profile4) profile5 = AdminProfileFactory() From 00d27d2b7533a366dce3f5e46a711b272a588cd3 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 11:09:29 +0200 Subject: [PATCH 12/31] Transferred Bar chart for activities into a new component frontend --- .../UserProfilesTable/ActivitiesBarChart.jsx | 87 +++++++++++++++++++ .../ActivitiesBarChart.module.css | 8 ++ .../UserProfilesTable/ProfilesStatistics.jsx | 71 +-------------- .../ProfilesStatistics.module.css | 7 -- 4 files changed, 97 insertions(+), 76 deletions(-) create mode 100644 FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx create mode 100644 FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx new file mode 100644 index 000000000..f6e426175 --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; +import axios from 'axios'; +import useSWR from 'swr'; +import Loader from '../../../components/Loader/Loader'; +import css from './ActivitiesBarChart.module.css'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +async function fetcher(url) { + const response = await axios.get(url); + return response.data; +} + +function ActivitiesBarChart() { + const baseUrl = process.env.REACT_APP_BASE_API_URL; + const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; + const { data: activities, error: activitiesError, isLoading: activitiesLoading } = useSWR(activities_url, fetcher); + const chartData = activities + ?{ + labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], + datasets: [ + { + label: 'Типи компаній', + data: [ + activities.manufacturers_count, + activities.importers_count, + activities.retail_networks_count, + activities.horeca_count, + activities.others_count + ], + backgroundColor: [ + '#87f3b0', + ] + }, + ] + }: { labels: [], datasets: [] }; + const options = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Статистика по типам компаній', + }, + }, + maintainAspectRatio: false, + }; + return ( +
+ {activitiesLoading && ( +
+ +
+ ) + } + {activitiesError && ( +
Не вдалося отримати статистику компаній
+ ) + } + {!activitiesLoading && !activitiesError && ( + + ) + } +
+ ); +} + +export default ActivitiesBarChart; \ No newline at end of file diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css new file mode 100644 index 000000000..171ddb797 --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css @@ -0,0 +1,8 @@ +.chart-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 250px; + width: 400px; +} \ No newline at end of file diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 923a4dc3d..126bc10f7 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -5,25 +5,7 @@ import { Descriptions, Segmented, Select, DatePicker } from 'antd'; import Loader from '../../../components/Loader/Loader'; import css from './ProfilesStatistics.module.css'; import React from 'react'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, -} from 'chart.js'; -import { Bar } from 'react-chartjs-2'; - -ChartJS.register( - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend -); +import ActivitiesBarChart from './ActivitiesBarChart'; const { Option } = Select; @@ -50,11 +32,7 @@ function ProfilesStatistics() { const url = `${baseUrl}/api/admin/profiles/statistics/${ queryParams.length ? `?${queryParams.join('&')}` : '' }`; - const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; - const { data: statistics, error, isLoading } = useSWR(url, fetcher); - const { data: activities, error: activitiesError, isLoading: activitiesLoading } = useSWR(activities_url, fetcher); - const items = statistics ? [ { @@ -87,38 +65,6 @@ function ProfilesStatistics() { const handleRangeChange = (value, dateString) => { setPeriodRange({ start_date: dateString[0], end_date: dateString[1] }); }; - const chartData = activities - ?{ - labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], - datasets: [ - { - label: 'Типи компаній', - data: [ - activities.manufacturers_count, - activities.importers_count, - activities.retail_networks_count, - activities.horeca_count, - activities.others_count - ], - backgroundColor: [ - '#87f3b0', - ] - }, - ] - }: { labels: [], datasets: [] }; - const options = { - responsive: true, - plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: 'Статистика по типам компаній', - }, - }, - maintainAspectRatio: false, - }; return (

Статистика компаній

@@ -184,20 +130,7 @@ function ProfilesStatistics() { items={items} /> )} - {activitiesLoading && ( -
- -
- )} - {activitiesError && ( -
Не вдалося отримати статистику компаній
- )} - {!activitiesLoading && !activitiesError && ( -
- -
- ) - } +
); } diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css index 9b4412fc6..d8217f260 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.module.css @@ -6,13 +6,6 @@ max-width: 400px; padding: 30px; } -.chart-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 250px; -} .statistics-title { font-size: 16px; From a43323126bcd75dde8fc0a53c2fc86ee8dd770a9 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 11:21:59 +0200 Subject: [PATCH 13/31] Reformatted --- .../UserProfilesTable/ActivitiesBarChart.jsx | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx index f6e426175..91161ae6c 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx @@ -15,73 +15,73 @@ import Loader from '../../../components/Loader/Loader'; import css from './ActivitiesBarChart.module.css'; ChartJS.register( - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend ); async function fetcher(url) { - const response = await axios.get(url); - return response.data; + const response = await axios.get(url); + return response.data; } function ActivitiesBarChart() { const baseUrl = process.env.REACT_APP_BASE_API_URL; const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; - const { data: activities, error: activitiesError, isLoading: activitiesLoading } = useSWR(activities_url, fetcher); + const {data: activities, error: activitiesError, isLoading: activitiesLoading} = useSWR(activities_url, fetcher); const chartData = activities - ?{ - labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], - datasets: [ - { - label: 'Типи компаній', - data: [ - activities.manufacturers_count, - activities.importers_count, - activities.retail_networks_count, - activities.horeca_count, - activities.others_count - ], - backgroundColor: [ - '#87f3b0', - ] - }, - ] - }: { labels: [], datasets: [] }; - const options = { - responsive: true, - plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: 'Статистика по типам компаній', + ? { + labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], + datasets: [ + { + label: 'Типи компаній', + data: [ + activities.manufacturers_count, + activities.importers_count, + activities.retail_networks_count, + activities.horeca_count, + activities.others_count + ], + backgroundColor: [ + '#87f3b0', + ] + }, + ] + } : {labels: [], datasets: []}; + const options = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Статистика по типам компаній', + }, }, - }, - maintainAspectRatio: false, + maintainAspectRatio: false, }; - return ( -
- {activitiesLoading && ( -
- -
- ) - } - {activitiesError && ( -
Не вдалося отримати статистику компаній
- ) - } - {!activitiesLoading && !activitiesError && ( - - ) - } -
- ); + return ( +
+ {activitiesLoading && ( +
+ +
+ ) + } + {activitiesError && ( +
Не вдалося отримати статистику компаній
+ ) + } + {!activitiesLoading && !activitiesError && ( + + ) + } +
+ ); } export default ActivitiesBarChart; \ No newline at end of file From e5347a2a505873f98639ffcfe7e3bfab5528a688 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 13:06:46 +0200 Subject: [PATCH 14/31] Added css for bar chart --- .../ActivitiesBarChart.module.css | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css index 171ddb797..7b738171d 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css @@ -2,7 +2,20 @@ display: flex; flex-direction: column; justify-content: center; - align-items: center; height: 250px; width: 400px; +} + +.error { + color: #f5222d; + background-color: #fff1f0; + border: 1px solid #ffa39e; + padding: 10px; + border-radius: 4px; + margin-top: 10px; +} + +.loader-container { + width: 200px; + height: 300px; } \ No newline at end of file From 8c5e0be3cfc709b6b03832a7bb8a23465c54d4e8 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 13:08:44 +0200 Subject: [PATCH 15/31] Moved the chart to a different folder --- .../{UserProfilesTable => Charts}/ActivitiesBarChart.jsx | 0 .../{UserProfilesTable => Charts}/ActivitiesBarChart.module.css | 0 .../pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename FrontEnd/src/pages/AdminPage/{UserProfilesTable => Charts}/ActivitiesBarChart.jsx (100%) rename FrontEnd/src/pages/AdminPage/{UserProfilesTable => Charts}/ActivitiesBarChart.module.css (100%) diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx similarity index 100% rename from FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.jsx rename to FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css similarity index 100% rename from FrontEnd/src/pages/AdminPage/UserProfilesTable/ActivitiesBarChart.module.css rename to FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index 126bc10f7..fb4855e9f 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -5,7 +5,7 @@ import { Descriptions, Segmented, Select, DatePicker } from 'antd'; import Loader from '../../../components/Loader/Loader'; import css from './ProfilesStatistics.module.css'; import React from 'react'; -import ActivitiesBarChart from './ActivitiesBarChart'; +import ActivitiesBarChart from '../Charts/ActivitiesBarChart'; const { Option } = Select; From a39e224730792aa92f58255994846b0f87605aba Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 14:46:48 +0200 Subject: [PATCH 16/31] Deleted the decimal part of the y in bar chart --- .../src/pages/AdminPage/Charts/ActivitiesBarChart.jsx | 8 +++++++- .../pages/AdminPage/Charts/ActivitiesBarChart.module.css | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx index 91161ae6c..555306c76 100644 --- a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx @@ -61,8 +61,14 @@ function ActivitiesBarChart() { display: true, text: 'Статистика по типам компаній', }, + scales: { + y: { + ticks: { + stepSize: 1, + }, + }, + }, }, - maintainAspectRatio: false, }; return (
diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css index 7b738171d..38743d84f 100644 --- a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css @@ -1,8 +1,4 @@ .chart-container { - display: flex; - flex-direction: column; - justify-content: center; - height: 250px; width: 400px; } @@ -13,6 +9,7 @@ padding: 10px; border-radius: 4px; margin-top: 10px; + } .loader-container { From bacc7bea8407a57b43fbbbd7cb8acdbe35acd140 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 00:47:35 +0200 Subject: [PATCH 17/31] Added view and serializer --- BackEnd/administration/serializers.py | 5 +++++ BackEnd/administration/views.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index c03ec1238..f3f796cf8 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -269,3 +269,8 @@ class StatisticsSerializer(serializers.Serializer): investors_count = serializers.IntegerField() startups_count = serializers.IntegerField() blocked_companies_count = serializers.IntegerField() + manufacturers_count = serializers.IntegerField() + importers_count = serializers.IntegerField() + retail_networks_count = serializers.IntegerField() + horeca_count = serializers.IntegerField() + others_count = serializers.IntegerField() \ No newline at end of file diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index b779988e0..155244b24 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -162,6 +162,17 @@ def get_object(self): investors_count=Count("pk", filter=Q(is_registered=True)), startups_count=Count("pk", filter=Q(is_startup=True)), blocked_companies_count=Count("pk", filter=Q(status="blocked")), + manufacturers_count=Count( + "pk", filter=Q(activities__name="Виробник") + ), + importers_count=Count("pk", filter=Q(activities__name="Імпортер")), + retail_networks_count=Count( + "pk", filter=Q(activities__name="Роздрібна мережа") + ), + horeca_count=Count("pk", filter=Q(activities__name="HORECA")), + others_count=Count( + "pk", filter=Q(activities__name="Інші послуги") + ), ) From b69ceb6d8f858714b639075bb1fb412bd82b2d0f Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 00:49:06 +0200 Subject: [PATCH 18/31] Added tests --- .../tests/test_monthly_profile_statistics.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 BackEnd/administration/tests/test_monthly_profile_statistics.py diff --git a/BackEnd/administration/tests/test_monthly_profile_statistics.py b/BackEnd/administration/tests/test_monthly_profile_statistics.py new file mode 100644 index 000000000..6511bdf43 --- /dev/null +++ b/BackEnd/administration/tests/test_monthly_profile_statistics.py @@ -0,0 +1,102 @@ +from rest_framework.test import APITestCase +from rest_framework import status +from administration.factories import AdminUserFactory, AdminProfileFactory +from utils.unittest_helper import utc_datetime + + +class TestMonthlyProfileStatisticsStaff(APITestCase): + def setUp(self): + self.user = AdminUserFactory() + self.client.force_authenticate(self.user) + self.test_startup_user = AdminUserFactory(is_staff=False) + self.test_investor_user = AdminUserFactory(is_staff=False) + self.test_blocked_company_user = AdminUserFactory(is_staff=False) + self.startup_company = AdminProfileFactory( + person_id=self.test_startup_user.id, is_registered=False + ) + self.startup_company.created_at = utc_datetime(2023, 12, 7) + self.startup_company.save() + self.investor_company = AdminProfileFactory( + person_id=self.test_investor_user.id, is_startup=False + ) + self.investor_company.created_at = utc_datetime(2024, 5, 10) + self.investor_company.save() + self.blocked_company = AdminProfileFactory( + person_id=self.test_blocked_company_user.id, status="blocked" + ) + self.blocked_company.created_at = utc_datetime(2024, 12, 12) + self.blocked_company.save() + + def test_get_monthly_profile_statistics(self): + response = self.client.get("/api/admin/profiles/statistics/monthly/") + data = [ + { + "month": 12, + "year": 2023, + "investors_count": 0, + "startups_count": 1, + "startup_investor_count": 0, + }, + { + "month": 5, + "year": 2024, + "investors_count": 1, + "startups_count": 0, + "startup_investor_count": 0, + }, + { + "month": 12, + "year": 2024, + "investors_count": 0, + "startups_count": 0, + "startup_investor_count": 1, + }, + ] + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, data) + + def test_get_monthly_statistics_incorrect_year(self): + response = self.client.get( + "/api/admin/profiles/statistics/monthly/?year=2002" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, []) + + def test_get_monthly_statistics_correct_year(self): + response = self.client.get( + "/api/admin/profiles/statistics/monthly/?year=2024" + ) + data = [ + { + "month": 5, + "year": 2024, + "investors_count": 1, + "startups_count": 0, + "startup_investor_count": 0, + }, + { + "month": 12, + "year": 2024, + "investors_count": 0, + "startups_count": 0, + "startup_investor_count": 1, + }, + ] + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, data) + + +class TestMonthlyProfileStatisticsNotStaff(APITestCase): + def setUp(self): + self.user = AdminUserFactory(is_staff=False) + self.client.force_authenticate(self.user) + + def test_get_profile_statistics(self): + response = self.client.get("/api/admin/profiles/statistics/monthly/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class TestMonthlyProfileStatisticsUnauthorized(APITestCase): + def test_get_profile_statistics(self): + response = self.client.get("/api/admin/profiles/statistics/monthly/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) From b6ee3c37b24349c32f57091a4976b5ec3401b6b8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 00:51:00 +0200 Subject: [PATCH 19/31] Added files --- .../tests/test_statistics_activities.py | 63 +++++++++++++ .../AdminPage/Charts/ActivitiesBarChart.jsx | 93 +++++++++++++++++++ .../Charts/ActivitiesBarChart.module.css | 18 ++++ 3 files changed, 174 insertions(+) create mode 100644 BackEnd/administration/tests/test_statistics_activities.py create mode 100644 FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx create mode 100644 FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py new file mode 100644 index 000000000..1c3f80e13 --- /dev/null +++ b/BackEnd/administration/tests/test_statistics_activities.py @@ -0,0 +1,63 @@ +from rest_framework.test import APITestCase +from administration.factories import AdminUserFactory, AdminProfileFactory +from rest_framework import status +from profiles.models import Activity + + +class ProfileStatisticsActivitiesTest(APITestCase): + def setUp(self): + self.user = AdminUserFactory() + self.activities = { + "Виробник": Activity.objects.create(name="Виробник"), + "Імпортер": Activity.objects.create(name="Імпортер"), + "Роздрібна мережа": Activity.objects.create( + name="Роздрібна мережа" + ), + "HORECA": Activity.objects.create(name="HORECA"), + "Інші послуги": Activity.objects.create(name="Інші послуги"), + } + self.profiles = [] + profile1 = AdminProfileFactory() + profile1.activities.set( + [self.activities["Виробник"], self.activities["HORECA"]] + ) + self.profiles.append(profile1) + + profile2 = AdminProfileFactory() + profile2.activities.set( + [self.activities["Імпортер"], self.activities["HORECA"]] + ) + self.profiles.append(profile2) + + profile3 = AdminProfileFactory() + profile3.activities.set( + [self.activities["Роздрібна мережа"], self.activities["HORECA"]] + ) + self.profiles.append(profile3) + + profile4 = AdminProfileFactory() + profile4.activities.set( + [self.activities["Інші послуги"], self.activities["HORECA"]] + ) + self.profiles.append(profile4) + + profile5 = AdminProfileFactory() + profile5.activities.set([self.activities["Виробник"]]) + self.profiles.append(profile5) + self.url = "/api/admin/profiles/statistics-activities/" + + def test_statistics_view_requires_authentication(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_statistics_view_with_valid_user(self): + self.client.force_authenticate(user=self.user) + response = self.client.get(self.url) + data = { + "manufacturers_count": 2, + "importers_count": 1, + "retail_networks_count": 1, + "horeca_count": 4, + "others_count": 1, + } + self.assertEqual(response.json(), data) diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx new file mode 100644 index 000000000..555306c76 --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; +import axios from 'axios'; +import useSWR from 'swr'; +import Loader from '../../../components/Loader/Loader'; +import css from './ActivitiesBarChart.module.css'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +async function fetcher(url) { + const response = await axios.get(url); + return response.data; +} + +function ActivitiesBarChart() { + const baseUrl = process.env.REACT_APP_BASE_API_URL; + const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; + const {data: activities, error: activitiesError, isLoading: activitiesLoading} = useSWR(activities_url, fetcher); + const chartData = activities + ? { + labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], + datasets: [ + { + label: 'Типи компаній', + data: [ + activities.manufacturers_count, + activities.importers_count, + activities.retail_networks_count, + activities.horeca_count, + activities.others_count + ], + backgroundColor: [ + '#87f3b0', + ] + }, + ] + } : {labels: [], datasets: []}; + const options = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Статистика по типам компаній', + }, + scales: { + y: { + ticks: { + stepSize: 1, + }, + }, + }, + }, + }; + return ( +
+ {activitiesLoading && ( +
+ +
+ ) + } + {activitiesError && ( +
Не вдалося отримати статистику компаній
+ ) + } + {!activitiesLoading && !activitiesError && ( + + ) + } +
+ ); +} + +export default ActivitiesBarChart; \ No newline at end of file diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css new file mode 100644 index 000000000..38743d84f --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css @@ -0,0 +1,18 @@ +.chart-container { + width: 400px; +} + +.error { + color: #f5222d; + background-color: #fff1f0; + border: 1px solid #ffa39e; + padding: 10px; + border-radius: 4px; + margin-top: 10px; + +} + +.loader-container { + width: 200px; + height: 300px; +} \ No newline at end of file From 9cba01c1e7d3ad5e46927c0a4909e657afb4bbf9 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 01:00:03 +0200 Subject: [PATCH 20/31] Added branch files --- BackEnd/administration/serializers.py | 12 +++++++-- BackEnd/administration/urls.py | 6 ----- BackEnd/administration/views.py | 25 ------------------- .../AdminPage/Charts/ActivitiesBarChart.jsx | 2 +- 4 files changed, 11 insertions(+), 34 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index f3f796cf8..c9af58bb5 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -97,7 +97,7 @@ def get_status(self, obj) -> dict: "is_superuser": obj.is_superuser, "is_deleted": obj.email.startswith("is_deleted_"), "is_inactive": not obj.is_active - and not obj.email.startswith("is_deleted_"), + and not obj.email.startswith("is_deleted_"), } return data @@ -273,4 +273,12 @@ class StatisticsSerializer(serializers.Serializer): importers_count = serializers.IntegerField() retail_networks_count = serializers.IntegerField() horeca_count = serializers.IntegerField() - others_count = serializers.IntegerField() \ No newline at end of file + others_count = serializers.IntegerField() + + +class MonthlyProfileStatisticsSerializer(serializers.Serializer): + month = serializers.IntegerField() + year = serializers.IntegerField() + investors_count = serializers.IntegerField() + startups_count = serializers.IntegerField() + startup_investor_count = serializers.IntegerField() diff --git a/BackEnd/administration/urls.py b/BackEnd/administration/urls.py index 0ae5076d0..e5a73a058 100644 --- a/BackEnd/administration/urls.py +++ b/BackEnd/administration/urls.py @@ -15,7 +15,6 @@ CategoriesListView, CategoryDetailView, SendMessageView, - ProfileStatisticsActivitiesView, ) app_name = "administration" @@ -29,11 +28,6 @@ ProfileStatisticsView.as_view(), name="profile-statistics", ), - path( - "profiles/statistics-activities/", - ProfileStatisticsActivitiesView.as_view(), - name="profile-statistics-activities", - ), path( "profiles/statistics/monthly/", MonthlyProfileStatisticsView.as_view(), diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index d8feca4cc..717912d08 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -27,7 +27,6 @@ from administration.serializers import ( AdminRegistrationSerializer, - StatisticsActivitiesSerializer, ) from forum.settings import CONTACTS_INFO from administration.serializers import ( @@ -182,31 +181,7 @@ def get_object(self): others_count=Count( "pk", filter=Q(activities__name="Інші послуги") ), - ) - - -class ProfileStatisticsActivitiesView(RetrieveAPIView): - """ - Count of companies in terms of their activities - """ - permission_classes = [IsStaffUser] - serializer_class = StatisticsActivitiesSerializer - - def get_object(self): - queryset = self.filter_queryset(Profile.objects.all()) - return queryset.aggregate( - manufacturers_count=Count( - "pk", filter=Q(activities__name="Виробник") - ), - importers_count=Count("pk", filter=Q(activities__name="Імпортер")), - retail_networks_count=Count( - "pk", filter=Q(activities__name="Роздрібна мережа") - ), - horeca_count=Count("pk", filter=Q(activities__name="HORECA")), - others_count=Count( - "pk", filter=Q(activities__name="Інші послуги") - ), ) diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx index 555306c76..c2e7ce27f 100644 --- a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx @@ -30,7 +30,7 @@ async function fetcher(url) { function ActivitiesBarChart() { const baseUrl = process.env.REACT_APP_BASE_API_URL; - const activities_url = `${baseUrl}/api/admin/profiles/statistics-activities/`; + const activities_url = `${baseUrl}/api/admin/profiles/statistics/`; const {data: activities, error: activitiesError, isLoading: activitiesLoading} = useSWR(activities_url, fetcher); const chartData = activities ? { From 37d06a60ea033ff410542f10181c6d110848215a Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 12:01:24 +0200 Subject: [PATCH 21/31] Moved chart to another component --- .../AdminPage/Charts/ActivitiesBarChart.jsx | 38 +++++++------------ .../UserProfilesTable/ProfilesStatistics.jsx | 5 ++- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx index c2e7ce27f..fdbde6c52 100644 --- a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx @@ -8,12 +8,6 @@ import { Tooltip, Legend, } from 'chart.js'; -import { Bar } from 'react-chartjs-2'; -import axios from 'axios'; -import useSWR from 'swr'; -import Loader from '../../../components/Loader/Loader'; -import css from './ActivitiesBarChart.module.css'; - ChartJS.register( CategoryScale, LinearScale, @@ -22,28 +16,24 @@ ChartJS.register( Tooltip, Legend ); +import { Bar } from 'react-chartjs-2'; +import css from './ActivitiesBarChart.module.css'; +import Loader from '../../../components/Loader/Loader'; -async function fetcher(url) { - const response = await axios.get(url); - return response.data; -} -function ActivitiesBarChart() { - const baseUrl = process.env.REACT_APP_BASE_API_URL; - const activities_url = `${baseUrl}/api/admin/profiles/statistics/`; - const {data: activities, error: activitiesError, isLoading: activitiesLoading} = useSWR(activities_url, fetcher); - const chartData = activities +function ActivitiesBarChart({statistics, isLoading, error}) { + const chartData = statistics ? { labels: ['Виробники', 'Імпортери', 'Роздрібніки', 'HORECA', 'Інші'], datasets: [ { label: 'Типи компаній', data: [ - activities.manufacturers_count, - activities.importers_count, - activities.retail_networks_count, - activities.horeca_count, - activities.others_count + statistics.manufacturers_count, + statistics.importers_count, + statistics.retail_networks_count, + statistics.horeca_count, + statistics.others_count ], backgroundColor: [ '#87f3b0', @@ -72,18 +62,18 @@ function ActivitiesBarChart() { }; return (
- {activitiesLoading && ( + {isLoading && (
) } - {activitiesError && ( + {error && (
Не вдалося отримати статистику компаній
) } - {!activitiesLoading && !activitiesError && ( - + {!isLoading && !error && ( + ) }
diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx index fb4855e9f..7141408cc 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesStatistics.jsx @@ -5,8 +5,10 @@ import { Descriptions, Segmented, Select, DatePicker } from 'antd'; import Loader from '../../../components/Loader/Loader'; import css from './ProfilesStatistics.module.css'; import React from 'react'; + import ActivitiesBarChart from '../Charts/ActivitiesBarChart'; + const { Option } = Select; async function fetcher(url) { @@ -20,7 +22,6 @@ function ProfilesStatistics() { const [periodType, setPeriodType] = useState(null); const [selectedDate, setSelectedDate] = useState(null); const [tab, setTab] = useState('overall'); - const queryParams = []; if (tab === 'period' && periodType !== 'range' && selectedDate) { queryParams.push(`${periodType}=${selectedDate}`); @@ -130,7 +131,7 @@ function ProfilesStatistics() { items={items} /> )} - +
); } From 8fa2abda092766b1300e7611181c8c1a9532871a Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 19:33:25 +0200 Subject: [PATCH 22/31] Added distinct value to values in statistics view, adjusted tests with adding activities to statistics --- .../tests/test_profile_statistics.py | 49 +++++++++++++++++++ BackEnd/administration/views.py | 18 +++---- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index 448695928..2b0bfde80 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -1,6 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status from administration.factories import AdminUserFactory, AdminProfileFactory +from profiles.models import Activity from utils.unittest_helper import utc_datetime @@ -8,6 +9,17 @@ class TestProfileStatisticsStaff(APITestCase): def setUp(self): self.user = AdminUserFactory() self.client.force_authenticate(self.user) + + self.activities = { + "Виробник": Activity.objects.create(name="Виробник"), + "Імпортер": Activity.objects.create(name="Імпортер"), + "Роздрібна мережа": Activity.objects.create( + name="Роздрібна мережа" + ), + "HORECA": Activity.objects.create(name="HORECA"), + "Інші послуги": Activity.objects.create(name="Інші послуги"), + } + self.test_startup_user = AdminUserFactory(is_staff=False) self.test_investor_user = AdminUserFactory(is_staff=False) self.test_blocked_company_user = AdminUserFactory(is_staff=False) @@ -15,16 +27,27 @@ def setUp(self): person_id=self.test_startup_user.id, is_registered=False ) self.startup_company.created_at = utc_datetime(2023, 12, 7) + + self.startup_company.activities.set( + [self.activities["Виробник"], self.activities["HORECA"]] + ) self.startup_company.save() self.investor_company = AdminProfileFactory( person_id=self.test_investor_user.id, is_startup=False ) self.investor_company.created_at = utc_datetime(2024, 5, 10) + self.investor_company.activities.set( + [self.activities["Імпортер"], self.activities["HORECA"]] + ) + self.investor_company.save() self.blocked_company = AdminProfileFactory( person_id=self.test_blocked_company_user.id, status="blocked" ) self.blocked_company.created_at = utc_datetime(2024, 12, 12) + self.blocked_company.activities.set( + [self.activities["Роздрібна мережа"]] + ) self.blocked_company.save() def test_get_profile_statistics(self): @@ -34,7 +57,13 @@ def test_get_profile_statistics(self): "investors_count": 2, "startups_count": 2, "blocked_companies_count": 1, + "manufacturers_count": 1, + "importers_count": 1, + "retail_networks_count": 1, + "horeca_count": 2, + "others_count": 0 } + print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -45,6 +74,11 @@ def test_get_profile_statistics_filtered_by_year(self): "investors_count": 2, "startups_count": 1, "blocked_companies_count": 1, + "manufacturers_count": 0, + "importers_count": 1, + "retail_networks_count": 1, + "horeca_count": 1, + "others_count": 0 } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -58,6 +92,11 @@ def test_get_profile_statistics_filtered_by_range(self): "investors_count": 2, "startups_count": 1, "blocked_companies_count": 1, + "manufacturers_count": 0, + "importers_count": 1, + "retail_networks_count": 1, + "horeca_count": 1, + "others_count": 0 } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -71,6 +110,11 @@ def test_get_profile_statistics_filtered_by_month(self): "investors_count": 0, "startups_count": 1, "blocked_companies_count": 0, + "manufacturers_count": 1, + "importers_count": 0, + "retail_networks_count": 0, + "horeca_count": 1, + "others_count": 0 } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -84,6 +128,11 @@ def test_get_profile_statistics_filtered_by_day(self): "investors_count": 0, "startups_count": 1, "blocked_companies_count": 0, + "manufacturers_count": 1, + "importers_count": 0, + "retail_networks_count": 0, + "horeca_count": 1, + "others_count": 0 } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 717912d08..84ffdf9e0 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -166,20 +166,20 @@ class ProfileStatisticsView(RetrieveAPIView): def get_object(self): queryset = self.filter_queryset(Profile.objects.all()) return queryset.aggregate( - companies_count=Count("pk"), - investors_count=Count("pk", filter=Q(is_registered=True)), - startups_count=Count("pk", filter=Q(is_startup=True)), - blocked_companies_count=Count("pk", filter=Q(status="blocked")), + companies_count=Count("pk", distinct=True), + investors_count=Count("pk", filter=Q(is_registered=True), distinct=True), + startups_count=Count("pk", filter=Q(is_startup=True),distinct=True), + blocked_companies_count=Count("pk", filter=Q(status="blocked"), distinct=True), manufacturers_count=Count( - "pk", filter=Q(activities__name="Виробник") + "pk", filter=Q(activities__name="Виробник"),distinct=True ), - importers_count=Count("pk", filter=Q(activities__name="Імпортер")), + importers_count=Count("pk", filter=Q(activities__name="Імпортер"),distinct=True), retail_networks_count=Count( - "pk", filter=Q(activities__name="Роздрібна мережа") + "pk", filter=Q(activities__name="Роздрібна мережа"),distinct=True ), - horeca_count=Count("pk", filter=Q(activities__name="HORECA")), + horeca_count=Count("pk", filter=Q(activities__name="HORECA"),distinct=True), others_count=Count( - "pk", filter=Q(activities__name="Інші послуги") + "pk", filter=Q(activities__name="Інші послуги"),distinct=True ), ) From 2664c10a141ee641614a8c27adf468f8f09aa194 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 21:25:27 +0200 Subject: [PATCH 23/31] Deleted unused tests --- .../tests/test_statistics_activities.py | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 BackEnd/administration/tests/test_statistics_activities.py diff --git a/BackEnd/administration/tests/test_statistics_activities.py b/BackEnd/administration/tests/test_statistics_activities.py deleted file mode 100644 index 1c3f80e13..000000000 --- a/BackEnd/administration/tests/test_statistics_activities.py +++ /dev/null @@ -1,63 +0,0 @@ -from rest_framework.test import APITestCase -from administration.factories import AdminUserFactory, AdminProfileFactory -from rest_framework import status -from profiles.models import Activity - - -class ProfileStatisticsActivitiesTest(APITestCase): - def setUp(self): - self.user = AdminUserFactory() - self.activities = { - "Виробник": Activity.objects.create(name="Виробник"), - "Імпортер": Activity.objects.create(name="Імпортер"), - "Роздрібна мережа": Activity.objects.create( - name="Роздрібна мережа" - ), - "HORECA": Activity.objects.create(name="HORECA"), - "Інші послуги": Activity.objects.create(name="Інші послуги"), - } - self.profiles = [] - profile1 = AdminProfileFactory() - profile1.activities.set( - [self.activities["Виробник"], self.activities["HORECA"]] - ) - self.profiles.append(profile1) - - profile2 = AdminProfileFactory() - profile2.activities.set( - [self.activities["Імпортер"], self.activities["HORECA"]] - ) - self.profiles.append(profile2) - - profile3 = AdminProfileFactory() - profile3.activities.set( - [self.activities["Роздрібна мережа"], self.activities["HORECA"]] - ) - self.profiles.append(profile3) - - profile4 = AdminProfileFactory() - profile4.activities.set( - [self.activities["Інші послуги"], self.activities["HORECA"]] - ) - self.profiles.append(profile4) - - profile5 = AdminProfileFactory() - profile5.activities.set([self.activities["Виробник"]]) - self.profiles.append(profile5) - self.url = "/api/admin/profiles/statistics-activities/" - - def test_statistics_view_requires_authentication(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_statistics_view_with_valid_user(self): - self.client.force_authenticate(user=self.user) - response = self.client.get(self.url) - data = { - "manufacturers_count": 2, - "importers_count": 1, - "retail_networks_count": 1, - "horeca_count": 4, - "others_count": 1, - } - self.assertEqual(response.json(), data) From b036d0017b041bd720d18d2f116c8cbbb74dc12d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jan 2025 21:25:43 +0200 Subject: [PATCH 24/31] Formatted files --- BackEnd/administration/serializers.py | 2 +- .../tests/test_profile_statistics.py | 10 +++---- BackEnd/administration/views.py | 29 +++++++++++++------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/BackEnd/administration/serializers.py b/BackEnd/administration/serializers.py index c9af58bb5..b56ade4ed 100644 --- a/BackEnd/administration/serializers.py +++ b/BackEnd/administration/serializers.py @@ -97,7 +97,7 @@ def get_status(self, obj) -> dict: "is_superuser": obj.is_superuser, "is_deleted": obj.email.startswith("is_deleted_"), "is_inactive": not obj.is_active - and not obj.email.startswith("is_deleted_"), + and not obj.email.startswith("is_deleted_"), } return data diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index 2b0bfde80..6338a888d 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -61,7 +61,7 @@ def test_get_profile_statistics(self): "importers_count": 1, "retail_networks_count": 1, "horeca_count": 2, - "others_count": 0 + "others_count": 0, } print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -78,7 +78,7 @@ def test_get_profile_statistics_filtered_by_year(self): "importers_count": 1, "retail_networks_count": 1, "horeca_count": 1, - "others_count": 0 + "others_count": 0, } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -96,7 +96,7 @@ def test_get_profile_statistics_filtered_by_range(self): "importers_count": 1, "retail_networks_count": 1, "horeca_count": 1, - "others_count": 0 + "others_count": 0, } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -114,7 +114,7 @@ def test_get_profile_statistics_filtered_by_month(self): "importers_count": 0, "retail_networks_count": 0, "horeca_count": 1, - "others_count": 0 + "others_count": 0, } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) @@ -132,7 +132,7 @@ def test_get_profile_statistics_filtered_by_day(self): "importers_count": 0, "retail_networks_count": 0, "horeca_count": 1, - "others_count": 0 + "others_count": 0, } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 84ffdf9e0..5902402f1 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -167,21 +167,32 @@ def get_object(self): queryset = self.filter_queryset(Profile.objects.all()) return queryset.aggregate( companies_count=Count("pk", distinct=True), - investors_count=Count("pk", filter=Q(is_registered=True), distinct=True), - startups_count=Count("pk", filter=Q(is_startup=True),distinct=True), - blocked_companies_count=Count("pk", filter=Q(status="blocked"), distinct=True), + investors_count=Count( + "pk", filter=Q(is_registered=True), distinct=True + ), + startups_count=Count( + "pk", filter=Q(is_startup=True), distinct=True + ), + blocked_companies_count=Count( + "pk", filter=Q(status="blocked"), distinct=True + ), manufacturers_count=Count( - "pk", filter=Q(activities__name="Виробник"),distinct=True + "pk", filter=Q(activities__name="Виробник"), distinct=True + ), + importers_count=Count( + "pk", filter=Q(activities__name="Імпортер"), distinct=True ), - importers_count=Count("pk", filter=Q(activities__name="Імпортер"),distinct=True), retail_networks_count=Count( - "pk", filter=Q(activities__name="Роздрібна мережа"),distinct=True + "pk", + filter=Q(activities__name="Роздрібна мережа"), + distinct=True, + ), + horeca_count=Count( + "pk", filter=Q(activities__name="HORECA"), distinct=True ), - horeca_count=Count("pk", filter=Q(activities__name="HORECA"),distinct=True), others_count=Count( - "pk", filter=Q(activities__name="Інші послуги"),distinct=True + "pk", filter=Q(activities__name="Інші послуги"), distinct=True ), - ) From ae476a6fbff382f0b14c42bd090dd5627a4f546d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jan 2025 14:32:11 +0200 Subject: [PATCH 25/31] Changed factories --- .../tests/test_profile_statistics.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index 6338a888d..2766751e5 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -1,7 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status -from administration.factories import AdminUserFactory, AdminProfileFactory -from profiles.models import Activity +from administration.factories import AdminUserFactory +from profiles.factories import ProfileFactory, ActivityFactory from utils.unittest_helper import utc_datetime @@ -11,43 +11,40 @@ def setUp(self): self.client.force_authenticate(self.user) self.activities = { - "Виробник": Activity.objects.create(name="Виробник"), - "Імпортер": Activity.objects.create(name="Імпортер"), - "Роздрібна мережа": Activity.objects.create( + "Виробник": ActivityFactory(name="Виробник"), + "Імпортер": ActivityFactory(name="Імпортер"), + "Роздрібна мережа": ActivityFactory( name="Роздрібна мережа" ), - "HORECA": Activity.objects.create(name="HORECA"), - "Інші послуги": Activity.objects.create(name="Інші послуги"), + "HORECA": ActivityFactory(name="HORECA"), + "Інші послуги": ActivityFactory(name="Інші послуги"), } self.test_startup_user = AdminUserFactory(is_staff=False) self.test_investor_user = AdminUserFactory(is_staff=False) self.test_blocked_company_user = AdminUserFactory(is_staff=False) - self.startup_company = AdminProfileFactory( - person_id=self.test_startup_user.id, is_registered=False + self.startup_company = ProfileFactory( + person_id=self.test_startup_user.id, + is_registered=False, + activities=[self.activities["Виробник"], self.activities["HORECA"]], ) self.startup_company.created_at = utc_datetime(2023, 12, 7) - self.startup_company.activities.set( - [self.activities["Виробник"], self.activities["HORECA"]] - ) self.startup_company.save() - self.investor_company = AdminProfileFactory( - person_id=self.test_investor_user.id, is_startup=False + self.investor_company = ProfileFactory( + person_id=self.test_investor_user.id, + is_startup=False, + activities=[self.activities["Імпортер"], self.activities["HORECA"]] ) self.investor_company.created_at = utc_datetime(2024, 5, 10) - self.investor_company.activities.set( - [self.activities["Імпортер"], self.activities["HORECA"]] - ) self.investor_company.save() - self.blocked_company = AdminProfileFactory( - person_id=self.test_blocked_company_user.id, status="blocked" + self.blocked_company = ProfileFactory( + person_id=self.test_blocked_company_user.id, + status="blocked", + activities=[self.activities["Роздрібна мережа"]], ) self.blocked_company.created_at = utc_datetime(2024, 12, 12) - self.blocked_company.activities.set( - [self.activities["Роздрібна мережа"]] - ) self.blocked_company.save() def test_get_profile_statistics(self): From dfb36f94bc2e569e2c26405b650597cb2b179f23 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jan 2025 15:11:51 +0200 Subject: [PATCH 26/31] Fixed profiles tests --- .../administration/tests/test_profile_statistics.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index 2766751e5..37a23e8c3 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -1,7 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status from administration.factories import AdminUserFactory -from profiles.factories import ProfileFactory, ActivityFactory +from profiles.factories import ProfileFactory, ActivityFactory, ProfileCompanyFactory, ProfileStartupFactory from utils.unittest_helper import utc_datetime @@ -23,23 +23,23 @@ def setUp(self): self.test_startup_user = AdminUserFactory(is_staff=False) self.test_investor_user = AdminUserFactory(is_staff=False) self.test_blocked_company_user = AdminUserFactory(is_staff=False) - self.startup_company = ProfileFactory( + self.startup_company = ProfileStartupFactory( person_id=self.test_startup_user.id, - is_registered=False, activities=[self.activities["Виробник"], self.activities["HORECA"]], ) self.startup_company.created_at = utc_datetime(2023, 12, 7) self.startup_company.save() - self.investor_company = ProfileFactory( + self.investor_company = ProfileCompanyFactory( person_id=self.test_investor_user.id, - is_startup=False, activities=[self.activities["Імпортер"], self.activities["HORECA"]] ) self.investor_company.created_at = utc_datetime(2024, 5, 10) self.investor_company.save() self.blocked_company = ProfileFactory( + is_registered=True, + is_startup=True, person_id=self.test_blocked_company_user.id, status="blocked", activities=[self.activities["Роздрібна мережа"]], From b39b6d2083854e7429d5a544d7c422ee4518886b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 16 Jan 2025 12:30:56 +0200 Subject: [PATCH 27/31] Converted css into scss --- FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx | 2 +- ...vitiesBarChart.module.css => ActivitiesBarChart.module.scss} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename FrontEnd/src/pages/AdminPage/Charts/{ActivitiesBarChart.module.css => ActivitiesBarChart.module.scss} (100%) diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx index fdbde6c52..9473fba03 100644 --- a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx +++ b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.jsx @@ -17,7 +17,7 @@ ChartJS.register( Legend ); import { Bar } from 'react-chartjs-2'; -import css from './ActivitiesBarChart.module.css'; +import css from './ActivitiesBarChart.module.scss'; import Loader from '../../../components/Loader/Loader'; diff --git a/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css b/FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.scss similarity index 100% rename from FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.css rename to FrontEnd/src/pages/AdminPage/Charts/ActivitiesBarChart.module.scss From f4f0ec8646b1a01b8cb7d29625f0c27e41adee2b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jan 2025 12:57:01 +0200 Subject: [PATCH 28/31] Updated branch by develop --- .../tests/test_profile_statistics.py | 21 +++++++++++++------ BackEnd/administration/views.py | 4 ---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index 37a23e8c3..d37b21df1 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -1,7 +1,12 @@ from rest_framework.test import APITestCase from rest_framework import status from administration.factories import AdminUserFactory -from profiles.factories import ProfileFactory, ActivityFactory, ProfileCompanyFactory, ProfileStartupFactory +from profiles.factories import ( + ProfileFactory, + ActivityFactory, + ProfileCompanyFactory, + ProfileStartupFactory, +) from utils.unittest_helper import utc_datetime @@ -13,9 +18,7 @@ def setUp(self): self.activities = { "Виробник": ActivityFactory(name="Виробник"), "Імпортер": ActivityFactory(name="Імпортер"), - "Роздрібна мережа": ActivityFactory( - name="Роздрібна мережа" - ), + "Роздрібна мережа": ActivityFactory(name="Роздрібна мережа"), "HORECA": ActivityFactory(name="HORECA"), "Інші послуги": ActivityFactory(name="Інші послуги"), } @@ -25,14 +28,20 @@ def setUp(self): self.test_blocked_company_user = AdminUserFactory(is_staff=False) self.startup_company = ProfileStartupFactory( person_id=self.test_startup_user.id, - activities=[self.activities["Виробник"], self.activities["HORECA"]], + activities=[ + self.activities["Виробник"], + self.activities["HORECA"], + ], ) self.startup_company.created_at = utc_datetime(2023, 12, 7) self.startup_company.save() self.investor_company = ProfileCompanyFactory( person_id=self.test_investor_user.id, - activities=[self.activities["Імпортер"], self.activities["HORECA"]] + activities=[ + self.activities["Імпортер"], + self.activities["HORECA"], + ], ) self.investor_company.created_at = utc_datetime(2024, 5, 10) diff --git a/BackEnd/administration/views.py b/BackEnd/administration/views.py index 9162913da..891ccd7c4 100644 --- a/BackEnd/administration/views.py +++ b/BackEnd/administration/views.py @@ -25,13 +25,9 @@ CreateAPIView, ) -from rest_framework.response import Response -from rest_framework import status - from administration.serializers import ( AdminRegistrationSerializer, ) -from forum.settings import CONTACTS_INFO from administration.serializers import ( AdminCompanyListSerializer, AdminCompanyDetailSerializer, From d64018d8e1e8d74b57ff7c365f0234ac92c0994b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jan 2025 14:29:36 +0200 Subject: [PATCH 29/31] Changed AdminUserFactory to UserFactory in statistics tests --- BackEnd/administration/tests/test_profile_statistics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index d37b21df1..b76cccb06 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -1,6 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status from administration.factories import AdminUserFactory +from authentication.factories import UserFactory from profiles.factories import ( ProfileFactory, ActivityFactory, @@ -12,7 +13,7 @@ class TestProfileStatisticsStaff(APITestCase): def setUp(self): - self.user = AdminUserFactory() + self.user = UserFactory(is_staff=True) self.client.force_authenticate(self.user) self.activities = { @@ -23,9 +24,9 @@ def setUp(self): "Інші послуги": ActivityFactory(name="Інші послуги"), } - self.test_startup_user = AdminUserFactory(is_staff=False) - self.test_investor_user = AdminUserFactory(is_staff=False) - self.test_blocked_company_user = AdminUserFactory(is_staff=False) + self.test_startup_user = UserFactory(is_staff=False) + self.test_investor_user = UserFactory(is_staff=False) + self.test_blocked_company_user = UserFactory(is_staff=False) self.startup_company = ProfileStartupFactory( person_id=self.test_startup_user.id, activities=[ From 288e413426102f2c6e1386eee1ca478499cbcdf6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Jan 2025 11:07:24 +0200 Subject: [PATCH 30/31] Optimised tests profile statistics --- BackEnd/administration/tests/test_profile_statistics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index b76cccb06..a0556859e 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -24,9 +24,9 @@ def setUp(self): "Інші послуги": ActivityFactory(name="Інші послуги"), } - self.test_startup_user = UserFactory(is_staff=False) - self.test_investor_user = UserFactory(is_staff=False) - self.test_blocked_company_user = UserFactory(is_staff=False) + self.test_startup_user = UserFactory() + self.test_investor_user = UserFactory() + self.test_blocked_company_user = UserFactory() self.startup_company = ProfileStartupFactory( person_id=self.test_startup_user.id, activities=[ From 83640aac8e90f9c8c3521b20786fa0981e694bb6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Jan 2025 11:08:03 +0200 Subject: [PATCH 31/31] Deleted unnecessary print in profile statistics tests --- BackEnd/administration/tests/test_profile_statistics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BackEnd/administration/tests/test_profile_statistics.py b/BackEnd/administration/tests/test_profile_statistics.py index a0556859e..d77100360 100644 --- a/BackEnd/administration/tests/test_profile_statistics.py +++ b/BackEnd/administration/tests/test_profile_statistics.py @@ -70,7 +70,6 @@ def test_get_profile_statistics(self): "horeca_count": 2, "others_count": 0, } - print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data)