Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amélioration de l'autocomplétion #6645

Merged
merged 5 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 6 additions & 30 deletions assets/js/autocompletion.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@
jsonData
.done(function(data) {
self.updateCache(data.results)
self.updateDropdown(self.sortList(data.results, search))
self.updateDropdown(data.results)
})
.fail(function() {
console.error('[Autocompletition] Something went wrong...')
console.error('[Autocompletion] Something went wrong...')
})
this.updateDropdown(this.sortList(this.searchCache(search), search))
this.updateDropdown(this.searchCache(search))
this.showDropdown()
}
}
Expand Down Expand Up @@ -236,7 +236,9 @@
self.handleInput()
}

list = self.filterData(list, self.extractWords(this.$input.val()))
// Exclude already selected items but the last one (it's the prefix of the item we are looking for)
const alreadyChosenWords = self.extractWords(this.$input.val()).slice(0, -1)
Situphen marked this conversation as resolved.
Show resolved Hide resolved
list = self.filterData(list, alreadyChosenWords)

if (list.length > this.options.limit) list = list.slice(0, this.options.limit)

Expand All @@ -263,32 +265,6 @@
if (!selected) { this.select($list.find('li').first().attr('data-autocomplete-id')) }
},

sortList: function(list, search) {
const bestMatches = []
const otherMatches = []

for (let i = 0; i < list.length; i++) {
if (list[i][this.options.fieldname].indexOf(search) === 0) {
bestMatches.push(list[i])
} else {
otherMatches.push(list[i])
}
}

const sortFn = function(a, b) {
const valueA = a[this.options.fieldname].toLowerCase()
const valueB = b[this.options.fieldname].toLowerCase()
if (valueA < valueB) { return -1 }
if (valueA > valueB) { return 1 }
return 0
}

bestMatches.sort(sortFn.bind(this))
otherMatches.sort(sortFn.bind(this))

return bestMatches.concat(otherMatches)
},

fetchData: function(input, excludeTerms) {
let data = this.options.url.replace('%s', input)
data = data.replace('%e', excludeTerms)
Expand Down
23 changes: 23 additions & 0 deletions zds/member/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ def test_search_without_results_in_list_of_users(self):
self.assertIsNone(response.data.get("next"))
self.assertIsNone(response.data.get("previous"))

def test_search_with_results_in_right_order(self):
"""
Gets list of users corresponding to part of a username and
verifies that this list is in the right order, which is:
1. "is equal" case sensitive
2. "is equal" ignoring the case
3. "starts with" case sensitive
4. "starts with" ignoring the case
5. "contains" case sensitive
6. "contains" ignoring the case

The test also checks that:
- usernames containing letters of the searched word (here: 'a', 'n', 'd' and 'r')
but NOT the searched word ("andr") are not returned
- usernames containing non-ascii letters (eg with accents) can be returned as well
"""
for username in ("pierre", "andr", "Radon", "alexandre", "MisterAndrew", "andré", "dragon", "Andromède"):
ProfileFactory(user__username=username)

response = self.client.get(reverse("api:member:list") + "?search=Andr")
list_of_usernames = [item.get("username") for item in response.data.get("results")]
self.assertEqual(list_of_usernames, ["andr", "Andromède", "andré", "MisterAndrew", "alexandre"])

def test_register_new_user(self):
"""
Registers a new user in the database.
Expand Down
25 changes: 22 additions & 3 deletions zds/member/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.utils.translation import gettext_lazy as _
from django.core.cache import cache
from django.db.models import Case, When, IntegerField, Value
from django.db.models.signals import post_save, post_delete
from dry_rest_permissions.generics import DRYPermissions
from rest_framework import filters
Expand Down Expand Up @@ -81,12 +82,30 @@ class MemberListAPI(ListCreateAPIView, ProfileCreate, TokenGenerator):
Profile resource to list and register.
"""

filter_backends = (filters.SearchFilter,)
search_fields = ("user__username",)
list_key_func = PagingSearchListKeyConstructor()

def get_queryset(self):
return Profile.objects.contactable_members()
queryset = Profile.objects.contactable_members()
search_param = self.request.query_params.get("search", None)

if search_param:
queryset = (
queryset.filter(user__username__icontains=search_param)
.annotate(
priority=Case(
When(user__username=search_param, then=Value(1)),
When(user__username__iexact=search_param, then=Value(2)),
When(user__username__startswith=search_param, then=Value(3)),
When(user__username__istartswith=search_param, then=Value(4)),
When(user__username__contains=search_param, then=Value(5)),
default=Value(6),
output_field=IntegerField(),
)
)
.order_by("priority", "user__username")
)

return queryset

@etag(list_key_func)
@cache_response(key_func=list_key_func)
Expand Down