diff --git a/applications/admin.py b/applications/admin.py index 99b3fdadb..a67abc3f6 100644 --- a/applications/admin.py +++ b/applications/admin.py @@ -15,8 +15,8 @@ class ApplicationAdmin(admin.ModelAdmin): list_filter = ('status', 'first_timer', 'reimb', 'graduation_year', 'university', 'origin', 'under_age', 'diet') list_per_page = 200 - search_fields = ('user__name', 'user__email', - 'description',) + search_fields = ('user__name__unaccent', 'user__email__unaccent', + 'description__unaccent',) ordering = ('submission_date',) date_hierarchy = 'submission_date' @@ -47,7 +47,7 @@ class OtherApplicationAdmin(admin.ModelAdmin): list_display = ('user', 'name', 'status', 'status_last_updated', 'diet') list_filter = ('status', 'under_age', 'diet') list_per_page = 200 - search_fields = ('user__name', 'user__email',) + search_fields = ('user__name__unaccent', 'user__email__unaccent',) ordering = ('submission_date',) date_hierarchy = 'submission_date' @@ -72,7 +72,7 @@ class SponsorApplicationAdmin(OtherApplicationAdmin): class DraftApplicationAdmin(admin.ModelAdmin): list_display = ('user', 'name') list_per_page = 200 - search_fields = ('user__name', 'user__email',) + search_fields = ('user__name__unaccent', 'user__email__unaccent',) ordering = ('user__name',) def name(self, obj): diff --git a/baggage/admin.py b/baggage/admin.py index 27ae8d3b7..aac323ea5 100644 --- a/baggage/admin.py +++ b/baggage/admin.py @@ -7,7 +7,7 @@ class BaggageRoomAdmin(admin.ModelAdmin): 'room', 'row', 'col', 'door_row', 'door_col' ) search_fields = ( - 'room', + 'room__unaccent', ) def get_actions(self, request): @@ -19,7 +19,8 @@ class BaggageListAdmin(admin.ModelAdmin): 'bid', 'owner', 'status', 'btype', 'color', 'description', 'special', 'time', 'updated' ) search_fields = ( - 'owner__email', 'owner__name', 'status', 'btype', 'color', 'description' + 'owner__email__unaccent', 'owner__name__unaccent', 'status__unaccent', 'btype__unaccent', + 'color__unaccent', 'description__unaccent' ) list_filter = ( 'status', 'btype', 'color', 'special' diff --git a/baggage/tables.py b/baggage/tables.py index 0b657ebe9..5068beed9 100644 --- a/baggage/tables.py +++ b/baggage/tables.py @@ -21,9 +21,12 @@ class BaggageListFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): queryfilter = queryset.annotate(fullpos=Concat('room', 'row', 'col', output_field=CharField())) - return queryfilter.filter((Q(owner__email__icontains=value) | Q(owner__name__icontains=value) | - Q(status__icontains=value) | Q(btype__icontains=value) | Q(color__icontains=value) | - Q(description__icontains=value) | Q(fullpos__icontains=value))) + return queryfilter.filter((Q(owner__email__unaccent__icontains=value) | + Q(owner__name__unaccent__icontains=value) | + Q(status__unaccent__icontains=value) | Q(btype__unaccent__icontains=value) | + Q(color__unaccent__icontains=value) | + Q(description__unaccent__icontains=value) | + Q(fullpos__unaccent__icontains=value))) def search_time(self, queryset, name, value): if name == 'time_from': @@ -39,15 +42,15 @@ class BaggageUsersFilter(django_filters.FilterSet): search = django_filters.CharFilter(method='search_filter', label='Search') def search_filter(self, queryset, name, value): - return queryset.filter(Q(hacker__user__email__icontains=value) | - Q(hacker__user__name__icontains=value) | - Q(volunteer__user__email__icontains=value) | - Q(volunteer__user__name__icontains=value) | - Q(mentor__user__email__icontains=value) | - Q(mentor__user__name__icontains=value) | - Q(sponsor__user__email__icontains=value) | - Q(sponsor__user__name__icontains=value) | - Q(qr_identifier__icontains=value)) + return queryset.filter(Q(hacker__user__email__unaccent__icontains=value) | + Q(hacker__user__name__unaccent__icontains=value) | + Q(volunteer__user__email__unaccent__icontains=value) | + Q(volunteer__user__name__unaccent__icontains=value) | + Q(mentor__user__email__unaccent__icontains=value) | + Q(mentor__user__name__unaccent__icontains=value) | + Q(sponsor__user__email__unaccent__icontains=value) | + Q(sponsor__user__name__unaccent__icontains=value) | + Q(qr_identifier__unaccent__icontains=value)) class Meta: model = CheckIn diff --git a/checkin/admin.py b/checkin/admin.py index f51a45554..2a8f081ac 100644 --- a/checkin/admin.py +++ b/checkin/admin.py @@ -9,9 +9,11 @@ class CheckinAdmin(admin.ModelAdmin): list_display = ( 'user', 'type', 'application', 'update_time' ) - search_fields = ('user__email', 'user__name', 'hacker__user__name', 'hacker__user__email', 'volunteer__user__name', - 'volunteer__user__email', 'mentor__user__name', 'mentor__user__email', 'sponsor__user__name', - 'sponsor__user__email') + search_fields = ('user__email__unaccent', 'user__name__unaccent', 'hacker__user__name__unaccent', + 'hacker__user__email__unaccent', 'volunteer__user__name__unaccent', + 'volunteer__user__email__unaccent', 'mentor__user__name__unaccent', + 'mentor__user__email__unaccent', 'sponsor__user__name__unaccent', + 'sponsor__user__email__unaccent') date_hierarchy = 'update_time' list_filter = ('user', ) actions = ['delete_selected', ] diff --git a/checkin/tables.py b/checkin/tables.py index a414ea2fd..db658679b 100644 --- a/checkin/tables.py +++ b/checkin/tables.py @@ -9,7 +9,7 @@ class ApplicationCheckinFilter(django_filters.FilterSet): search = django_filters.CharFilter(method='search_filter', label='Search') def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | + return queryset.filter(Q(user__email__unaccent__icontains=value) | Q(user__name__unaccent__icontains=value) | Q(uuid__icontains=value.replace('-', ''))) class Meta: @@ -34,8 +34,8 @@ class SponsorApplicationCheckinFilter(django_filters.FilterSet): search = django_filters.CharFilter(method='search_filter', label='Search') def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | - Q(name__icontains=value)) + return queryset.filter(Q(user__email__unaccent__icontains=value) | Q(user__name__unaccent__icontains=value) | + Q(name__unaccent__icontains=value)) class Meta: model = SponsorApplication diff --git a/discord/admin.py b/discord/admin.py index c437bec60..c4345cd07 100644 --- a/discord/admin.py +++ b/discord/admin.py @@ -7,7 +7,7 @@ class DiscordUserAdmin(admin.ModelAdmin): list_display = ( 'user', 'discord_id', 'checked_in' ) - search_fields = ('user__email', 'user__name') + search_fields = ('user__email__unaccent', 'user__name__unaccent') list_filter = ('user',) actions = ['delete_selected', ] diff --git a/discord/tables.py b/discord/tables.py index de7131fc2..44b29c2c9 100644 --- a/discord/tables.py +++ b/discord/tables.py @@ -24,10 +24,10 @@ class DiscordFilter(django_filters.FilterSet): search = django_filters.CharFilter(method='search_filter', label='Search') def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | - Q(user__name__icontains=value) | - Q(discord_username__icontains=value) | - Q(team_name__icontains=value)) + return queryset.filter(Q(user__email__unaccent__icontains=value) | + Q(user__name__unaccent__icontains=value) | + Q(discord_username__unaccent__icontains=value) | + Q(team_name__unaccent__icontains=value)) class Meta: model = DiscordUser diff --git a/hardware/tables.py b/hardware/tables.py index 5d7d26a9c..cef39c6f7 100644 --- a/hardware/tables.py +++ b/hardware/tables.py @@ -32,7 +32,7 @@ def status_filter(self, queryset, name, value): return qs.distinct() def search_filter(self, queryset, name, value): - return queryset.filter(Q(item_type__name__icontains=value) | Q(user__name__icontains=value)) + return queryset.filter(Q(item_type__name__unaccent__icontains=value) | Q(user__name__unaccent__icontains=value)) class Meta: model = Request @@ -50,7 +50,8 @@ def status_filter(self, queryset, name, value): return queryset.get_active() def search_filter(self, queryset, name, value): - return queryset.filter(Q(item__item_type__name__icontains=value) | Q(user__name__icontains=value)) + return queryset.filter(Q(item__item_type__name__unaccent__icontains=value) | + Q(user__name__unaccent__icontains=value)) class Meta: model = Borrowing diff --git a/hardware/views/admin.py b/hardware/views/admin.py index 5cccaa536..4093f14fa 100644 --- a/hardware/views/admin.py +++ b/hardware/views/admin.py @@ -179,7 +179,7 @@ def identify_hacker(self, request): Gets a list of suggestions based on the input (typeahead) """ checkins = CheckIn.objects.filter( - Q(hacker__user__name__icontains=request.POST['query']) | + Q(hacker__user__name__unaccent__icontains=request.POST['query']) | Q(hacker__user__email__startswith=request.POST['query']) | Q(qr_identifier=request.POST['query'])) diff --git a/judging/admin.py b/judging/admin.py index d95315007..8006bce94 100644 --- a/judging/admin.py +++ b/judging/admin.py @@ -5,19 +5,19 @@ class ProjectAdmin(admin.ModelAdmin): list_display = ('title', 'url', 'desired_prizes', 'description', 'university') - search_fields = ['title', 'url', 'university', 'desired_prizes', - 'submitter_first_name', 'submitter_last_name'] + search_fields = ['title__unaccent', 'url', 'university__unaccent', 'desired_prizes__unaccent', + 'submitter_first_name__unaccent', 'submitter_last_name__unaccent'] list_per_page = 100 class ChallengeAdmin(admin.ModelAdmin): list_display = ('name',) - search_fields = ['name'] + search_fields = ['name__unaccent'] class RoomAdmin(admin.ModelAdmin): list_display = ('name', 'challenge', 'main_judge') - search_fields = ['name', 'challenge', 'main_judge'] + search_fields = ['name__unaccent', 'challenge__name__unaccent', 'main_judge__name__unaccent'] class PresentationAdmin(admin.ModelAdmin): diff --git a/meals/admin.py b/meals/admin.py index ca1e52f07..cdf93369b 100644 --- a/meals/admin.py +++ b/meals/admin.py @@ -7,7 +7,7 @@ class MealsMealAdmin(admin.ModelAdmin): 'id', 'name', 'times', 'opened', 'starts', 'ends' ) search_fields = ( - 'name', + 'name__unaccent', ) def get_actions(self, request): @@ -19,7 +19,7 @@ class MealsEatenAdmin(admin.ModelAdmin): 'id', 'meal', 'user', 'time' ) search_fields = ( - 'name', 'user__name', 'user__email' + 'meal__name__unaccent', 'user__name__unaccent', 'user__email__unaccent' ) list_filter = ( 'meal', 'user' diff --git a/meals/tables.py b/meals/tables.py index 3622e0efa..5a104e566 100644 --- a/meals/tables.py +++ b/meals/tables.py @@ -11,7 +11,7 @@ class MealsListFilter(django_filters.FilterSet): kind = django_filters.ChoiceFilter(label='Type', choices=MEAL_TYPE, empty_label='Any') def search_filter(self, queryset, name, value): - return queryset.filter((Q(name__icontains=value) | Q(kind__icontains=value))) + return queryset.filter((Q(name__unaccent__icontains=value) | Q(kind__unaccent__icontains=value))) class Meta: model = Meal @@ -55,8 +55,8 @@ def search_filter(self, queryset, name, value): checkin = CheckIn.objects.get(qr_identifier=value) return queryset.filter(user=checkin.application.user) except CheckIn.DoesNotExist: - return queryset.filter(Q(meal__name__icontains=value) | - Q(user__name__icontains=value) | Q(user__email__icontains=value)) + return queryset.filter(Q(meal__name__unaccent__icontains=value) | + Q(user__name__unaccent__icontains=value) | Q(user__email__unaccent__icontains=value)) class Meta: model = Meal diff --git a/organizers/admin.py b/organizers/admin.py index 9daecb11d..368c4eefe 100644 --- a/organizers/admin.py +++ b/organizers/admin.py @@ -15,7 +15,8 @@ class VoteAdmin(admin.ModelAdmin): list_display = ('application', 'user', 'tech', 'personal', 'calculated_vote') list_per_page = 200 list_filter = ('user', 'application') - search_fields = ('application__user__name', 'application__user__email', 'user__name', 'user__email') + search_fields = ('application__user__name__unaccent', 'application__user__email__unaccent', + 'user__name__unaccent', 'user__email__unaccent') actions = ['delete_selected', ] diff --git a/organizers/tables.py b/organizers/tables.py index 818dc001d..dc16ab557 100755 --- a/organizers/tables.py +++ b/organizers/tables.py @@ -23,10 +23,10 @@ class ApplicationFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(user__email__icontains=value) - | Q(user__name__icontains=value) - | Q(university__icontains=value) - | Q(origin__icontains=value) + Q(user__email__unaccent__icontains=value) + | Q(user__name__unaccent__icontains=value) + | Q(university__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: @@ -51,10 +51,10 @@ class DubiousApplicationFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(user__email__icontains=value) - | Q(user__name__icontains=value) - | Q(university__icontains=value) - | Q(origin__icontains=value) + Q(user__email__unaccent__icontains=value) + | Q(user__name__unaccent__icontains=value) + | Q(university__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: @@ -67,10 +67,10 @@ class BlacklistApplicationFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(user__email__icontains=value) - | Q(user__name__icontains=value) - | Q(university__icontains=value) - | Q(origin__icontains=value) + Q(user__email__unaccent__icontains=value) + | Q(user__name__unaccent__icontains=value) + | Q(university__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: @@ -94,10 +94,10 @@ class InviteFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(user__email__icontains=value) - | Q(user__name__icontains=value) - | Q(university__icontains=value) - | Q(origin__icontains=value) + Q(user__email__unaccent__icontains=value) + | Q(user__name__unaccent__icontains=value) + | Q(university__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: @@ -273,9 +273,9 @@ class SponsorFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(email__icontains=value) - | Q(user__name__icontains=value) - | Q(name__icontains=value) + Q(email__unaccent__icontains=value) + | Q(user__name__unaccent__icontains=value) + | Q(name__unaccent__icontains=value) ) class Meta: @@ -307,7 +307,7 @@ class SponsorUserFilter(django_filters.FilterSet): search = django_filters.CharFilter(method="search_filter", label="Search") def search_filter(self, queryset, name, value): - return queryset.filter(Q(email__icontains=value) | Q(name__icontains=value)) + return queryset.filter(Q(email__unaccent__icontains=value) | Q(name__unaccent__icontains=value)) class Meta: model = User diff --git a/reimbursement/admin.py b/reimbursement/admin.py index ab46141ff..124fd0749 100644 --- a/reimbursement/admin.py +++ b/reimbursement/admin.py @@ -21,7 +21,7 @@ class ReimbursementAdmin(admin.ModelAdmin): ) list_filter = ("status", "origin", "reimbursed_by") - search_fields = ["hacker__name", "hacker__email", "origin"] + search_fields = ["hacker__name__unaccent", "hacker__email__unaccent", "origin__unaccent"] list_per_page = 200 ordering = ("creation_time",) diff --git a/reimbursement/tables.py b/reimbursement/tables.py index 2f241a192..b7dbb55fa 100644 --- a/reimbursement/tables.py +++ b/reimbursement/tables.py @@ -14,9 +14,9 @@ class ReimbursementFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(hacker__email__icontains=value) - | Q(hacker__name__icontains=value) - | Q(origin__icontains=value) + Q(hacker__email__unaccent__icontains=value) + | Q(hacker__name__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: @@ -51,9 +51,9 @@ class SendReimbursementFilter(django_filters.FilterSet): def search_filter(self, queryset, name, value): return queryset.filter( - Q(hacker__email__icontains=value) - | Q(hacker__name__icontains=value) - | Q(origin__icontains=value) + Q(hacker__email__unaccent__icontains=value) + | Q(hacker__name__unaccent__icontains=value) + | Q(origin__unaccent__icontains=value) ) class Meta: diff --git a/teams/admin.py b/teams/admin.py index 346cd266b..205c0d1e5 100644 --- a/teams/admin.py +++ b/teams/admin.py @@ -8,7 +8,7 @@ class TeamAdmin(admin.ModelAdmin): list_display = ('team_code', 'user',) list_per_page = 200 list_filter = ('team_code', 'user') - search_fields = ('team_code', 'user__name') + search_fields = ('team_code', 'user__name__unaccent') actions = ['delete_selected', ] diff --git a/tests/test_unaccent_search.py b/tests/test_unaccent_search.py new file mode 100644 index 000000000..c65b2d057 --- /dev/null +++ b/tests/test_unaccent_search.py @@ -0,0 +1,33 @@ +import pytest +from django.contrib.auth import get_user_model + +from tests.factories import UserFactory + +User = get_user_model() + + +@pytest.mark.django_db +def test_unaccented_query_matches_accented_name(): + UserFactory(name="Gerard Màdrid") + + result = User.objects.filter(name__unaccent__icontains="madrid") + + assert result.count() == 1 + + +@pytest.mark.django_db +def test_accented_query_matches_unaccented_name(): + UserFactory(name="Gerard Madrid") + + result = User.objects.filter(name__unaccent__icontains="mÀdRíD") + + assert result.count() == 1 + + +@pytest.mark.django_db +def test_non_matching_query_returns_nothing(): + UserFactory(name="Gerard Màdrid") + + result = User.objects.filter(name__unaccent__icontains="mdrid") + + assert result.count() == 0 diff --git a/user/admin.py b/user/admin.py index 86b493131..b39d28fe4 100644 --- a/user/admin.py +++ b/user/admin.py @@ -42,7 +42,7 @@ class UserAdmin(admin.ModelAdmin): 'fields': ('email', 'name', 'password1', 'password2',)} ), ) - search_fields = ('email',) + search_fields = ('email__unaccent',) ordering = ('created_time',) date_hierarchy = 'created_time' filter_horizontal = () @@ -57,7 +57,7 @@ class BlacklistUserAdmin(admin.ModelAdmin): list_display = ('email', 'name', 'date_of_ban') list_per_page = 20 list_filter = ('email', 'name') - search_fields = ('email', 'name') + search_fields = ('email__unaccent', 'name__unaccent') actions = ['delete_selected', ] diff --git a/user/apps.py b/user/apps.py index 7fa8d1b41..12313797e 100644 --- a/user/apps.py +++ b/user/apps.py @@ -11,3 +11,5 @@ def ready(self): from .signals import user_organizer, user_verify_email user_organizer user_verify_email + from .lookups import register + register() diff --git a/user/lookups.py b/user/lookups.py new file mode 100644 index 000000000..d2e196d21 --- /dev/null +++ b/user/lookups.py @@ -0,0 +1,27 @@ +import unicodedata + +from django.db.backends.signals import connection_created +from django.db.models import CharField, TextField, Transform + + +def strip_accents(value): + if value is None: + return None + return ''.join(char for char in unicodedata.normalize('NFKD', value) if not unicodedata.combining(char)) + + +class Unaccent(Transform): + bilateral = True + lookup_name = 'unaccent' + function = 'UNACCENT' + + +def register_sqlite_unaccent(connection, **kwargs): + if connection.vendor == 'sqlite': + connection.connection.create_function('unaccent', 1, strip_accents, deterministic=True) + + +def register(): + CharField.register_lookup(Unaccent) + TextField.register_lookup(Unaccent) + connection_created.connect(register_sqlite_unaccent) diff --git a/user/migrations/0020_unaccent_extension.py b/user/migrations/0020_unaccent_extension.py new file mode 100644 index 000000000..7c049eede --- /dev/null +++ b/user/migrations/0020_unaccent_extension.py @@ -0,0 +1,13 @@ +from django.contrib.postgres.operations import UnaccentExtension +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0019_user_mlh_subscribed'), + ] + + operations = [ + UnaccentExtension(), + ]