Commit 4d3dcb6b authored by Raphaël Hertzog's avatar Raphaël Hertzog

Merge branch 'issue-11-search-teams' into 'master'

Add search field to Teams page Closes #11 See merge request !26
parents 2284292f dc61031f
Pipeline #8539 passed with stages
in 13 minutes 16 seconds
......@@ -323,3 +323,16 @@ summary {
summary::-webkit-details-marker {
display: none;
}
form#team-search-form div.input-group {
width: 100%;
}
form#team-search-form .twitter-typeahead {
padding-top: 3px;
width: 100%;
}
form#team-search-form .twitter-typeahead > input {
width: 100% !important;
}
......@@ -71,6 +71,43 @@ $(function() {
}
);
var teams = new Bloodhound({
datumTokenizer: function(data) {
Bloodhound.tokenizers.obj.whitespace(data.slug);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: '/api/teams/search/autocomplete?q=%QUERY',
wildcard: '%QUERY',
rateLimitWait: 500,
transform: function(r) {
return r['teams'];
}
}
});
$('.team-completion').typeahead({
hint: false,
highlight: true,
minLength: 2
},
{
name: 'teams',
displayKey: 'slug',
source: teams,
templates: {
suggestion: function (teams) {
return '<p>' + teams.name + '</p>';
}
}
}
).bind('typeahead:render', function(e) {
var options = $('div.tt-dataset-teams p');
if(options.length == 1){
options.first().addClass('tt-cursor');
}
});
var subscribe_url = $('#subscribe-button').data('url');
var unsubscribe_url = $('#unsubscribe-button').data('url')
var toggle_subscription_buttons = function() {
......
......@@ -14,6 +14,8 @@
{% endblock %}
{% block content %}
{% include 'core/profile-messages.html' %}
{% include 'core/team-search-form.html' with autofocus=1 %}
<ul class="list-group m-t-1">
{% for team in team_list %}
<li class="list-group-item">
......
{% spaceless %}
<div class="row">
<form class="form-inline" id="team-search-form" action="{% url 'dtracker-team-search' %}" method="GET">
<div class="col-lg-6 col-lg-offset-3">
<div class="input-group">
<input type="text" class="form-control team-completion" name="query" id='team-search-input' placeholder="Jump to team..."{% if autofocus %} autofocus='autofocus'{% endif %}>
<div class="input-group-btn">
<button type="submit" class="btn btn-default">Go</button>
</div>
</div>
</div>
</form>
</div>
{% endspaceless %}
......@@ -429,6 +429,70 @@ class PackageAutocompleteViewTest(TestCase):
self.assertEqual(response.status_code, 404)
class TeamAutocompleteViewTest(TestCase):
def setUp(self):
Team.objects.create_with_slug(name='Debian Go Packaging Team')
Team.objects.create_with_slug(name='Debian HPC')
Team.objects.create_with_slug(name='Java Team')
def test_team_autocomplete(self):
"""
Tests the autocomplete functionality when the client asks for teams
"""
response = self.client.get(reverse('dtracker-api-team-autocomplete'),
{'q': 'd'})
response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response['query_string'], 'd')
self.assertEqual(len(response['teams']), 2)
names = [team['name'] for team in response['teams']]
self.assertIn('Debian Go Packaging Team', names)
self.assertIn('Debian HPC', names)
response = self.client.get(reverse('dtracker-api-team-autocomplete'),
{'q': 'team'})
response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response['query_string'], 'team')
self.assertEqual(len(response['teams']), 2)
names = [team['name'] for team in response['teams']]
self.assertIn('Debian Go Packaging Team', names)
self.assertIn('Java Team', names)
def test_team_autocomplete_with_slug(self):
"""
Tests the autocomplete functionality when the client asks for teams
by their slugs
"""
response = self.client.get(reverse('dtracker-api-team-autocomplete'),
{'q': '-team'})
response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response['query_string'], '-team')
self.assertEqual(len(response['teams']), 2)
names = [team['name'] for team in response['teams']]
self.assertIn('Debian Go Packaging Team', names)
self.assertIn('Java Team', names)
def test_query_does_not_match_teams(self):
"""
Tests the autocomplete functionality when the client's query
does not match any team
"""
response = self.client.get(reverse('dtracker-api-team-autocomplete'),
{'q': 'z'})
response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response['query_string'], 'z')
self.assertEqual(len(response['teams']), 0)
def test_no_query_given(self):
"""
Tests the autocomplete when there is no query parameter given.
"""
response = self.client.get(reverse('dtracker-api-package-autocomplete'))
self.assertEqual(response.status_code, 404)
class ActionItemJsonViewTest(TestCase):
"""
Tests for the :class:`distro_tracker.core.views.ActionItemJsonView`.
......@@ -1445,6 +1509,99 @@ class TeamListViewTest(UserAuthMixin, TestCase):
)
class TeamSearchViewTest(TestCase):
"""
Tests for the
:class:`distro_tracker.core.views.TeamSearchView`.
"""
def setUp(self):
self.team_1 = Team.objects.create_with_slug(name='Debian HPC')
self.team_2 = Team.objects.create_with_slug(
name='Debian Go Packaging Team')
def assert_not_found_team(self, response, query="new-team"):
self.assertRedirects(response, reverse('dtracker-team-list'))
message = list(response.context['messages'])[0]
self.assertEqual(message.level_tag, "danger")
self.assertIn(
("No team could be identified with the query string %s" % query),
message.message
)
def test_search_for_existing_team_by_slug(self):
"""
Tests the search for existing team by slug
"""
response = self.client.get(reverse('dtracker-team-search'), {
'query': self.team_1.slug
})
self.assertRedirects(response, self.team_1.get_absolute_url())
def test_search_for_existing_team_by_name(self):
"""
Tests the search for existing team by name
"""
response = self.client.get(reverse('dtracker-team-search'), {
'query': self.team_1.name
})
self.assertRedirects(response, self.team_1.get_absolute_url())
def test_search_for_team_by_partial_name_or_slug(self):
"""
Tests the search for team by partial name or slug
when it identify a single matching team
"""
response = self.client.get(reverse('dtracker-team-search'), {
'query': 'Debian H'
})
self.assertRedirects(response, self.team_1.get_absolute_url())
response = self.client.get(reverse('dtracker-team-search'), {
'query': 'debian-h'
})
self.assertRedirects(response, self.team_1.get_absolute_url())
def test_fail_to_search_for_team_by_partial_name_or_slug(self):
"""
Tests the search for team by partial name or slug
when it identifies multiple matching teams
"""
response = self.client.get(reverse('dtracker-team-search'), {
'query': 'Debian'
}, follow=True)
self.assert_not_found_team(response, 'Debian')
response = self.client.get(reverse('dtracker-team-search'), {
'query': 'debian-'
}, follow=True)
self.assert_not_found_team(response, 'debian-')
def test_team_does_not_exist(self):
"""
Tests the team search when the given team does not exist.
"""
slug = 'does-not-exist'
response = self.client.get(
reverse('dtracker-team-search'), {'query': slug}, follow=True)
self.assert_not_found_team(response, slug)
def test_case_insensitive_team_search(self):
"""
Tests that team search is case insensitive
"""
slug = 'debian-HPC'
response = self.client.get(
reverse('dtracker-team-search'), {'query': slug}, follow=True)
self.assertRedirects(response, self.team_1.get_absolute_url())
def test_search_without_query_parameter(self):
"""
Tests error response when the query parameter is missing
"""
response = self.client.get(reverse('dtracker-package-search'))
self.assertEqual(response.status_code, 404)
class SetMuteTeamViewTest(UserAuthMixin, TestCase):
"""
Tests for the
......
......@@ -668,6 +668,64 @@ class EditMembershipView(LoginRequiredMixin, ListView):
return context
class TeamAutocompleteView(View):
"""
A view which responds to team auto-complete queries.
Renders a JSON list of team names matching the given query, meaning
their name contains the given query parameter.
"""
@method_decorator(cache_control(must_revalidate=True, max_age=3600))
def get(self, request):
if 'q' not in request.GET:
raise Http404
query_string = request.GET['q']
filtered = Team.objects.filter(
Q(name__icontains=query_string) | Q(slug__icontains=query_string))
# Extract only the name and slug of the team.
filtered = filtered.values('name', 'slug')
# Limit the number of teams returned from the autocomplete
AUTOCOMPLETE_ITEMS_LIMIT = 100
filtered = filtered[:AUTOCOMPLETE_ITEMS_LIMIT]
return render_to_json_response({
'query_string': query_string,
'teams': list(filtered)
})
class TeamSearchView(View):
"""
A view which responds to team search queries.
"""
def get(self, request):
if 'query' not in self.request.GET:
raise Http404
query = self.request.GET.get('query')
team = self.find_team(query)
if team is not None:
return redirect(team)
else:
messages.error(
request,
("No team could be identified with the query string %s" % query)
)
return redirect(reverse('dtracker-team-list'))
def find_team(self, query):
if Team.objects.filter(slug=query).exists():
return Team.objects.filter(slug=query).first()
elif Team.objects.filter(name=query).exists():
return Team.objects.filter(name=query).first()
elif Team.objects.filter(
Q(name__icontains=query) | Q(slug__icontains=query)
).count() == 1:
return Team.objects.filter(
Q(name__icontains=query) | Q(slug__icontains=query)).first()
return None
class IndexView(TemplateView):
template_name = 'core/index.html'
......
......@@ -60,6 +60,8 @@ from distro_tracker.core.views import (
PackageAutocompleteView,
PackageNews,
PackageSearchView,
TeamAutocompleteView,
TeamSearchView,
RemovePackageFromTeamView,
RemoveTeamMember,
SetMembershipKeywords,
......@@ -100,6 +102,8 @@ urlpatterns = [
name='dtracker-api-action-item'),
url(r'^api/keywords/$', KeywordsView.as_view(),
name='dtracker-api-keywords'),
url(r'^api/teams/search/autocomplete$', TeamAutocompleteView.as_view(),
name='dtracker-api-team-autocomplete'),
url(r'^admin/', admin.site.urls),
......@@ -211,6 +215,8 @@ urlpatterns = [
url(r'^teams/\+confirm/(?P<confirmation_key>.+)/$',
ConfirmMembershipView.as_view(),
name='dtracker-team-confirm-membership'),
url(r'^team/\+search$', TeamSearchView.as_view(),
name='dtracker-team-search'),
url(r'^teams/(?P<slug>.+)/\+mute/$', SetMuteTeamView.as_view(action='mute'),
name='dtracker-team-mute'),
url(r'^teams/(?P<slug>.+)/\+unmute/$',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment