From 04fc5a56c761324b40e23dbac74cc1342a4dbce8 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Tue, 10 Sep 2024 10:59:21 +0300 Subject: [PATCH 1/7] add filter for username and name fields --- src/backend/api/v1/profile/filters.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/backend/api/v1/profile/filters.py b/src/backend/api/v1/profile/filters.py index 6ae6e1e..b10bbd4 100644 --- a/src/backend/api/v1/profile/filters.py +++ b/src/backend/api/v1/profile/filters.py @@ -1,3 +1,4 @@ +from django.db.models import Case, IntegerField, Q, Value, When from django_filters.rest_framework import FilterSet, filters from apps.general.constants import LEVEL_CHOICES @@ -19,6 +20,7 @@ class ProfileFilter(FilterSet): field_name="specialists__skills", queryset=Skill.objects.all() ) is_favorite = filters.NumberFilter(method="filter_is_favorite_profile") + search = filters.CharFilter(method="filter_search") class Meta: model = Profile @@ -35,3 +37,31 @@ def filter_is_favorite_profile(self, queryset, name, value): if user.is_authenticated and value == 1: return queryset.filter(favorited_by=user) return queryset + + def filter_search(self, queryset, name, value): + value = value.strip() + if not value and len(value) < 3: + return queryset + + exact_match = Q(name__iexact=value) | Q(user__username__iexact=value) + partial_match = Q(name__icontains=value) | Q( + user__username__icontains=value + ) + + exact_match_case = When(exact_match, then=Value(0)) + partial_match_case = When(partial_match, then=Value(1)) + + queryset = ( + queryset.annotate( + match_priority=Case( + exact_match_case, + partial_match_case, + default=Value(2), + output_field=IntegerField(), + ) + ) + .filter(exact_match | partial_match) + .order_by("match_priority", "user_id") + ) + + return queryset From 15c869e0634664e392319108b754f4e775d6bde1 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:00:16 +0300 Subject: [PATCH 2/7] add pg_trgm to postgres settings --- .../migrations/0009_add_trigramextension.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/backend/apps/profile/migrations/0009_add_trigramextension.py diff --git a/src/backend/apps/profile/migrations/0009_add_trigramextension.py b/src/backend/apps/profile/migrations/0009_add_trigramextension.py new file mode 100644 index 0000000..7cded2b --- /dev/null +++ b/src/backend/apps/profile/migrations/0009_add_trigramextension.py @@ -0,0 +1,14 @@ +# Generated by Badmajor 5.0.1 on 2024-10-24 23:55 + +from django.contrib.postgres.operations import TrigramExtension +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("profile", "0008_alter_profile_portfolio_link"), + ] + + operations = [ + TrigramExtension(), + ] From 079bd13a01ce6a3d10161bcd44f81c199005b82d Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:02:49 +0300 Subject: [PATCH 3/7] add postgres in install apps for trigram search --- src/backend/config/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/config/settings/base.py b/src/backend/config/settings/base.py index 28f97ee..3dcac9d 100644 --- a/src/backend/config/settings/base.py +++ b/src/backend/config/settings/base.py @@ -27,6 +27,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.postgres", ] THIRD_PARTY_APPS = [ From 6c3073f24c5925b1a40b7876b35378a636852ab6 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:10:35 +0300 Subject: [PATCH 4/7] refactor filter --- src/backend/api/v1/profile/filters.py | 42 +++++++++++---------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/backend/api/v1/profile/filters.py b/src/backend/api/v1/profile/filters.py index b10bbd4..37044a8 100644 --- a/src/backend/api/v1/profile/filters.py +++ b/src/backend/api/v1/profile/filters.py @@ -1,4 +1,4 @@ -from django.db.models import Case, IntegerField, Q, Value, When +from django.contrib.postgres.search import TrigramSimilarity from django_filters.rest_framework import FilterSet, filters from apps.general.constants import LEVEL_CHOICES @@ -20,7 +20,7 @@ class ProfileFilter(FilterSet): field_name="specialists__skills", queryset=Skill.objects.all() ) is_favorite = filters.NumberFilter(method="filter_is_favorite_profile") - search = filters.CharFilter(method="filter_search") + user_search = filters.CharFilter(method="user_filter_search") class Meta: model = Profile @@ -30,6 +30,7 @@ class Meta: "specialization", "skills", "is_favorite", + "user_search", ) def filter_is_favorite_profile(self, queryset, name, value): @@ -38,30 +39,21 @@ def filter_is_favorite_profile(self, queryset, name, value): return queryset.filter(favorited_by=user) return queryset - def filter_search(self, queryset, name, value): - value = value.strip() - if not value and len(value) < 3: - return queryset + def user_filter_search(self, queryset, name, value): + if value: + trigram_similarity = TrigramSimilarity( + "name", value + ) + TrigramSimilarity("user__username", value) - exact_match = Q(name__iexact=value) | Q(user__username__iexact=value) - partial_match = Q(name__icontains=value) | Q( - user__username__icontains=value - ) - - exact_match_case = When(exact_match, then=Value(0)) - partial_match_case = When(partial_match, then=Value(1)) - - queryset = ( - queryset.annotate( - match_priority=Case( - exact_match_case, - partial_match_case, - default=Value(2), - output_field=IntegerField(), - ) + annotated_queryset = queryset.annotate( + similarity=trigram_similarity + ) + # Нужно поиграться со значением similarity__gte, + # это совпадение в процентах, а не в символах + # GPT рекомендует 0.1. + # Тестово оставил 0.05 для удобства тестирования на небольшой базе + return annotated_queryset.filter(similarity__gte=0.05).order_by( + "-similarity" ) - .filter(exact_match | partial_match) - .order_by("match_priority", "user_id") - ) return queryset From 09e066add3909dd3f3557e5277ae2ed5a75d8ab7 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:24:08 +0300 Subject: [PATCH 5/7] refactor migrations with TrigramExtension --- src/backend/api/v1/profile/filters.py | 3 ++- ...09_add_trigramextension.py => 0010_add_trigramextension.py} | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename src/backend/apps/profile/migrations/{0009_add_trigramextension.py => 0010_add_trigramextension.py} (82%) diff --git a/src/backend/api/v1/profile/filters.py b/src/backend/api/v1/profile/filters.py index 37044a8..c35dad5 100644 --- a/src/backend/api/v1/profile/filters.py +++ b/src/backend/api/v1/profile/filters.py @@ -40,7 +40,7 @@ def filter_is_favorite_profile(self, queryset, name, value): return queryset def user_filter_search(self, queryset, name, value): - if value: + if value and len(value) > 2: trigram_similarity = TrigramSimilarity( "name", value ) + TrigramSimilarity("user__username", value) @@ -48,6 +48,7 @@ def user_filter_search(self, queryset, name, value): annotated_queryset = queryset.annotate( similarity=trigram_similarity ) + # Нужно поиграться со значением similarity__gte, # это совпадение в процентах, а не в символах # GPT рекомендует 0.1. diff --git a/src/backend/apps/profile/migrations/0009_add_trigramextension.py b/src/backend/apps/profile/migrations/0010_add_trigramextension.py similarity index 82% rename from src/backend/apps/profile/migrations/0009_add_trigramextension.py rename to src/backend/apps/profile/migrations/0010_add_trigramextension.py index 7cded2b..524c877 100644 --- a/src/backend/apps/profile/migrations/0009_add_trigramextension.py +++ b/src/backend/apps/profile/migrations/0010_add_trigramextension.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("profile", "0008_alter_profile_portfolio_link"), + ("profile", "0009_alter_profile_about"), ] operations = [ From b33d586e0c6cdf85b34e39a4fe0c8ebd388c6860 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:37:52 +0300 Subject: [PATCH 6/7] add constant --- src/backend/api/v1/profile/filters.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/api/v1/profile/filters.py b/src/backend/api/v1/profile/filters.py index c35dad5..2932fa8 100644 --- a/src/backend/api/v1/profile/filters.py +++ b/src/backend/api/v1/profile/filters.py @@ -5,6 +5,8 @@ from apps.general.models import Profession, Skill from apps.profile.models import Profile +SIMILARITY_CF: float = 0.05 + class ProfileFilter(FilterSet): """Класс фильтрации профилей специалистов.""" @@ -53,8 +55,8 @@ def user_filter_search(self, queryset, name, value): # это совпадение в процентах, а не в символах # GPT рекомендует 0.1. # Тестово оставил 0.05 для удобства тестирования на небольшой базе - return annotated_queryset.filter(similarity__gte=0.05).order_by( - "-similarity" - ) + return annotated_queryset.filter( + similarity__gte=SIMILARITY_CF + ).order_by("-similarity") return queryset From a45a0ca1092b4426af714358580f8b46f45bce65 Mon Sep 17 00:00:00 2001 From: Badmajor Date: Fri, 25 Oct 2024 12:38:46 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=88=D0=B5=D0=BB=D1=81?= =?UTF-8?q?=D1=8F=20black=20check=20=D0=B2=20github=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BF=D1=83=D1=81=D0=BA=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/api/v1/projects/serializers.py | 5 +---- src/backend/api/v1/projects/views.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/backend/api/v1/projects/serializers.py b/src/backend/api/v1/projects/serializers.py index 203456c..f1d2090 100644 --- a/src/backend/api/v1/projects/serializers.py +++ b/src/backend/api/v1/projects/serializers.py @@ -382,10 +382,7 @@ def validate_project(self, value): ) .first() ) - if ( - user == project.creator - or user == project.owner - ): + if user == project.creator or user == project.owner: raise serializers.ValidationError( "Вы не можете создать заявку на участие в проекте, в котором " "уже участвуете." diff --git a/src/backend/api/v1/projects/views.py b/src/backend/api/v1/projects/views.py index dd30304..f1eb22f 100644 --- a/src/backend/api/v1/projects/views.py +++ b/src/backend/api/v1/projects/views.py @@ -303,9 +303,9 @@ def get_queryset(self) -> QuerySet["ParticipationRequest"]: status_filter = self.request.query_params.get("status") if status_filter: queryset = queryset.filter(status=status_filter) - return queryset.filter( - Q(user=self.request.user) - ).only(*PROJECT_PARTICIPATION_REQUEST_ONLY_FIELDS.get(self.action, ())) + return queryset.filter(Q(user=self.request.user)).only( + *PROJECT_PARTICIPATION_REQUEST_ONLY_FIELDS.get(self.action, ()) + ) def get_object(self) -> ParticipationRequest: """Метод получения объекта запроса на участие в проекте."""