Repository: carpentries/amy

Issue:
Find instructors function only finding badged instructors?
In the More -> Find Instructors function, it seems like it's only finding people who are certified. That default makes sense, since instructors require certification. 

Sometimes we want to find all associated people in a given region though, to potentially contact as helpers or let them know a workshop is going on. Is there a way to search for all People associated with a given airport code?



Discussion:
Yeah, we can make this form a little more universal.

Thanks, I think that would be useful. Maybe taking away the default filters and giving more filter options?

I think main queries are by airport and then we want to know if people are certified instructors, not-yet-certified instructors, helpers or learners. This will probably start to be even more important as we try to foster local communities.  


Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -88,7 +88,7 @@ class BootstrapHelperFormsetInline(BootstrapHelper):
 bootstrap_helper_inline_formsets = BootstrapHelperFormsetInline()
 
 
-class InstructorsForm(forms.Form):
+class WorkshopStaffForm(forms.Form):
     '''Represent instructor matching form.'''
 
     latitude = forms.FloatField(label='Latitude',
@@ -127,13 +127,21 @@ class InstructorsForm(forms.Form):
     GENDER_CHOICES = ((None, '---------'), ) + Person.GENDER_CHOICES
     gender = forms.ChoiceField(choices=GENDER_CHOICES, required=False)
 
+    was_helper = forms.BooleanField(
+        required=False, label='Was helper at least once before')
+    was_organizer = forms.BooleanField(
+        required=False, label='Was organizer at least once before')
+    is_in_progress_trainee = forms.BooleanField(
+        required=False, label='Is an in-progress instructor trainee')
+
     def __init__(self, *args, **kwargs):
         '''Build form layout dynamically.'''
-        super(InstructorsForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
         # dynamically build choices for country field
-        only = Airport.objects.distinct().values_list('country', flat=True)
-        only = [c for c in only if c]
+        only = Airport.objects.distinct().exclude(country='') \
+                                         .exclude(country=None) \
+                                         .values_list('country', flat=True)
         countries = Countries()
         countries.only = only
 
@@ -146,49 +154,47 @@ def __init__(self, *args, **kwargs):
         self.helper.form_method = 'get'
         self.helper.layout = Layout(
             Div(
-                Div(
-                    'latitude',
-                    'longitude',
-                    css_class='panel-body'
-                ),
-                css_class='panel panel-default ',
-            ),
-            HTML('<p>OR</p>'),
-            Div(
-                Div(
-                    'airport',
-                    css_class='panel-body'
-                ),
-                css_class='panel panel-default ',
-            ),
-            HTML('<p>OR</p>'),
-            Div(
-                Div(
-                    'country',
-                    css_class='panel-body'
-                ),
+                Div(HTML('Location close to'), css_class='panel-heading'),
+                Div('airport', css_class='panel-body'),
+                Div(HTML('<b>OR</b>'), css_class='panel-footer'),
+                Div('country', css_class='panel-body'),
+                Div(HTML('<b>OR</b>'), css_class='panel-footer'),
+                Div('latitude', 'longitude', css_class='panel-body'),
                 css_class='panel panel-default ',
             ),
+            'instructor_badges',
+            'was_helper',
+            'was_organizer',
+            'is_in_progress_trainee',
             'gender',
             'lessons',
-            'instructor_badges',
             FormActions(
                 Submit('submit', 'Submit'),
             ),
         )
 
     def clean(self):
-        cleaned_data = super(InstructorsForm, self).clean()
-        airport = cleaned_data.get('airport')
-        lat = cleaned_data.get('latitude')
-        long = cleaned_data.get('longitude')
-        country = cleaned_data.get('country')
-
-        sum = bool(airport) + bool(lat and long) + bool(country)
-        # user can specify only one: either airport, or lat&long, or country
-        if sum != 1:
-            raise forms.ValidationError('Must specify an airport, or latitude'
-                                        ' and longitude, or a country.')
+        cleaned_data = super().clean()
+        lat = bool(cleaned_data.get('latitude'))
+        lng = bool(cleaned_data.get('longitude'))
+        airport = bool(cleaned_data.get('airport'))
+        country = bool(cleaned_data.get('country'))
+        latlng = lat and lng
+
+        # if searching by coordinates, then there must be both lat & lng
+        # present
+        if lat ^ lng:
+            raise forms.ValidationError(
+                'Must specify both latitude and longitude if searching by '
+                'coordinates')
+
+        # User must search by airport, or country, or coordinates, or none
+        # of them. Sum of boolean elements must be equal 0 (if general search)
+        # or 1 (if searching by airport OR country OR lat/lng).
+        if sum([airport, country, latlng]) not in [0, 1]:
+            raise forms.ValidationError(
+                'Must specify an airport OR a country, OR use coordinates, OR '
+                'none of them.')
         return cleaned_data
 
 
diff --git a/workshops/urls.py b/workshops/urls.py
--- a/workshops/urls.py
+++ b/workshops/urls.py
@@ -69,7 +69,7 @@
 
     url(r'^trainings/?$', views.all_trainings, name='all_trainings'),
 
-    url(r'^instructors/?$', views.instructors, name='instructors'),
+    url(r'^workshop_staff/?$', views.workshop_staff, name='workshop_staff'),
 
     url(r'^search/?$', views.search, name='search'),
 
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -54,7 +54,7 @@
     EventSubmission as EventSubmissionModel,
 )
 from workshops.forms import (
-    SearchForm, DebriefForm, InstructorsForm, PersonForm, PersonBulkAddForm,
+    SearchForm, DebriefForm, WorkshopStaffForm, PersonForm, PersonBulkAddForm,
     EventForm, TaskForm, TaskFullForm, bootstrap_helper, bootstrap_helper_get,
     bootstrap_helper_with_add, BadgeAwardForm, PersonAwardForm,
     PersonPermissionsForm, bootstrap_helper_filter, PersonsSelectionForm,
@@ -1628,41 +1628,52 @@ def all_trainings(request):
 
 
 @login_required
-def instructors(request):
-    '''Search for instructors.'''
+def workshop_staff(request):
+    '''Search for workshop staff.'''
     instructor_badges = Badge.objects.instructor_badges()
-    instructors = Person.objects\
-                        .filter(badges__in=instructor_badges) \
-                        .filter(airport__isnull=False) \
-                        .annotate(is_swc_instructor=Sum(
-                                      Case(When(badges__name='swc-instructor',
-                                                then=1),
-                                           default=0,
-                                           output_field=IntegerField())),
-                                  is_dc_instructor=Sum(
-                                      Case(When(badges__name='dc-instructor',
-                                                then=1),
-                                           default=0,
-                                           output_field=IntegerField()))) \
-                        .select_related('airport') \
-                        .prefetch_related('lessons')
-    instructors = instructors.annotate(
+    TTT = Tag.objects.get(name='TTT')
+    stalled = Tag.objects.get(name='stalled')
+
+    people = Person.objects.filter(airport__isnull=False) \
+                           .select_related('airport') \
+                           .prefetch_related('badges', 'lessons')
+
+    trainees = Task.objects.filter(event__tags=TTT) \
+                           .filter(role__name='learner') \
+                           .filter(person__airport__isnull=False) \
+                           .exclude(event__tags=stalled) \
+                           .exclude(person__badges__in=instructor_badges) \
+                           .values_list('person__pk', flat=True)
+
+    # we need to count number of specific roles users had
+    # and if they are SWC/DC instructors
+    people = people.annotate(
         num_taught=Count(
             Case(
-                When(
-                    task__role__name='instructor',
-                    then=Value(1)
-                ),
+                When(task__role__name='instructor', then=Value(1)),
+                output_field=IntegerField()
+            )
+        ),
+        num_helper=Count(
+            Case(
+                When(task__role__name='helper', then=Value(1)),
+                output_field=IntegerField()
+            )
+        ),
+        num_organizer=Count(
+            Case(
+                When(task__role__name='organizer', then=Value(1)),
                 output_field=IntegerField()
             )
         )
     )
-    form = InstructorsForm()
+
+    form = WorkshopStaffForm()
 
     lessons = list()
 
     if 'submit' in request.GET:
-        form = InstructorsForm(request.GET)
+        form = WorkshopStaffForm(request.GET)
         if form.is_valid():
             data = form.cleaned_data
 
@@ -1672,7 +1683,7 @@ def instructors(request):
                 # not any lesson within the list (as it would be with
                 # `.filter(lessons_in=lessons)`)
                 for lesson in lessons:
-                    instructors = instructors.filter(
+                    people = people.filter(
                         qualification__lesson=lesson
                     )
 
@@ -1680,40 +1691,55 @@ def instructors(request):
                 x = data['airport'].latitude
                 y = data['airport'].longitude
                 # using Euclidean distance just because it's faster and easier
-                complex_F = ((F('airport__latitude') - x) ** 2
-                             + (F('airport__longitude') - y) ** 2)
-                instructors = instructors.annotate(distance=complex_F) \
-                                         .order_by('distance', 'family')
+                complex_F = ((F('airport__latitude') - x) ** 2 +
+                             (F('airport__longitude') - y) ** 2)
+                people = people.annotate(distance=complex_F) \
+                               .order_by('distance', 'family')
 
             if data['latitude'] and data['longitude']:
                 x = data['latitude']
                 y = data['longitude']
                 # using Euclidean distance just because it's faster and easier
-                complex_F = ((F('airport__latitude') - x) ** 2
-                             + (F('airport__longitude') - y) ** 2)
-                instructors = instructors.annotate(distance=complex_F) \
-                                         .order_by('distance', 'family')
+                complex_F = ((F('airport__latitude') - x) ** 2 +
+                             (F('airport__longitude') - y) ** 2)
+                people = people.annotate(distance=complex_F) \
+                               .order_by('distance', 'family')
 
             if data['country']:
-                instructors = instructors.filter(
+                people = people.filter(
                     airport__country__in=data['country']
                 ).order_by('family')
 
             if data['gender']:
-                instructors = instructors.filter(gender=data['gender'])
+                people = people.filter(gender=data['gender'])
 
             if data['instructor_badges']:
                 for badge in data['instructor_badges']:
-                    instructors = instructors.filter(badges__name=badge)
+                    people = people.filter(badges__name=badge)
+
+            # it's faster to count role=helper occurences than to check if user
+            # had a role=helper
+            if data['was_helper']:
+                people = people.filter(num_helper__gte=1)
+
+            if data['was_organizer']:
+                people = people.filter(num_organizer__gte=1)
+
+            if data['is_in_progress_trainee']:
+                q = Q(task__event__tags=TTT) & ~Q(task__event__tags=stalled)
+                people = people.filter(q, task__role__name='learner') \
+                               .exclude(badges__in=instructor_badges)
 
-    instructors = get_pagination_items(request, instructors)
+    people = get_pagination_items(request, people)
     context = {
-        'title': 'Find Instructors',
+        'title': 'Find Workshop Staff',
         'form': form,
-        'persons': instructors,
+        'persons': people,
         'lessons': lessons,
+        'instructor_badges': instructor_badges,
+        'trainees': trainees,
     }
-    return render(request, 'workshops/instructors.html', context)
+    return render(request, 'workshops/workshop_staff.html', context)
 
 #------------------------------------------------------------
 


Test patch:
diff --git a/workshops/test/base.py b/workshops/test/base.py
--- a/workshops/test/base.py
+++ b/workshops/test/base.py
@@ -148,15 +148,17 @@ def _setUpNonInstructors(self):
         self.spiderman = Person.objects.create(
             personal='Peter', middle='Q.', family='Parker',
             email='peter@webslinger.net', gender='O', may_contact=True,
-            username="spiderman")
+            username="spiderman", airport=self.airport_55_105)
 
         self.ironman = Person.objects.create(
             personal='Tony', middle=None, family='Stark', email='me@stark.com',
-            gender='M', may_contact=True, username="ironman")
+            gender='M', may_contact=True, username="ironman",
+            airport=self.airport_50_100)
 
         self.blackwidow = Person.objects.create(
             personal='Natasha', middle=None, family='Romanova', email=None,
-            gender='F', may_contact=False, username="blackwidow")
+            gender='F', may_contact=False, username="blackwidow",
+            airport=self.airport_0_50)
 
     def _setUpUsersAndLogin(self):
         """Set up one account for administrator that can log into the website.
diff --git a/workshops/test/test_airport.py b/workshops/test/test_airport.py
--- a/workshops/test/test_airport.py
+++ b/workshops/test/test_airport.py
@@ -23,6 +23,9 @@ def test_airport_delete(self):
             self.hermione.pk,
             self.harry.pk,
             self.ron.pk,
+            self.spiderman.pk,
+            self.ironman.pk,
+            self.blackwidow.pk,
         ]
 
         for iata in L1[:3]:
diff --git a/workshops/test/test_event.py b/workshops/test/test_event.py
--- a/workshops/test/test_event.py
+++ b/workshops/test/test_event.py
@@ -17,6 +17,7 @@ class TestEvent(TestBase):
     "Tests for the event model and its manager."
 
     def setUp(self):
+        self._setUpAirports()
         self._setUpNonInstructors()
         self._setUpUsersAndLogin()
 
@@ -204,6 +205,7 @@ class TestEventViews(TestBase):
 
     def setUp(self):
         self._setUpUsersAndLogin()
+        self._setUpAirports()
         self._setUpNonInstructors()
 
         self.learner = Role.objects.get_or_create(name='learner')[0]
diff --git a/workshops/test/test_instructors.py b/workshops/test/test_instructors.py
deleted file mode 100644
--- a/workshops/test/test_instructors.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from django.core.urlresolvers import reverse
-from .base import TestBase
-
-
-class TestLocateInstructors(TestBase):
-    '''Test cases for locating instructors.'''
-
-    def setUp(self):
-        super().setUp()
-        self._setUpUsersAndLogin()
-
-    def test_non_instructors_not_returned_by_search(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'airport_1': self.airport_0_0.pk, 'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" in content
-        assert "Ron Weasley" in content
-
-    def test_match_instructors_on_one_skill(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'airport_1': self.airport_50_100.pk, 'lessons': [self.git.pk],
-             'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" not in content
-        assert "Ron Weasley" in content
-
-    def test_match_instructors_on_two_skills(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'airport_1': self.airport_50_100.pk,
-             'lessons': [self.git.pk, self.sql.pk],
-             'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" not in content
-        assert "Ron Weasley" not in content
-
-    def test_match_by_country(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'country': ['AL'], 'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" not in content
-        assert "Ron Weasley" not in content
-
-    def test_match_by_multiple_countries(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'country': ['AL', 'BG'], 'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" in content
-        assert "Ron Weasley" not in content
-
-    def test_match_gender(self):
-        response = self.client.get(
-            reverse('instructors'),
-            {'latitude': 1, 'longitude': 1, 'gender': 'F', 'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" in content
-        assert "Harry Potter" not in content
-        assert "Ron Weasley" not in content
-
-        response = self.client.get(
-            reverse('instructors'),
-            {'latitude': 1, 'longitude': 1, 'gender': 'M', 'submit': 'Submit'}
-        )
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert "Hermione Granger" not in content
-        assert "Harry Potter" in content
-        assert "Ron Weasley" in content
diff --git a/workshops/test/test_todos.py b/workshops/test/test_todos.py
--- a/workshops/test/test_todos.py
+++ b/workshops/test/test_todos.py
@@ -11,6 +11,7 @@ class TestTodoItemViews(TestBase):
     """Tests for the TodoItem model."""
 
     def setUp(self):
+        self._setUpAirports()
         self._setUpNonInstructors()
         self._setUpUsersAndLogin()
         self._setUpEvents()
diff --git a/workshops/test/test_workshop_staff_searching.py b/workshops/test/test_workshop_staff_searching.py
new file mode 100644
--- /dev/null
+++ b/workshops/test/test_workshop_staff_searching.py
@@ -0,0 +1,288 @@
+import unittest
+
+from django.core.urlresolvers import reverse
+
+from .base import TestBase
+from ..models import Task, Role, Event, Tag, Host
+
+
+class TestLocateWorkshopStaff(TestBase):
+    '''Test cases for locating workshop staff.'''
+
+    def setUp(self):
+        super().setUp()
+        self._setUpTags()
+        self._setUpRoles()
+        self._setUpUsersAndLogin()
+
+    def test_non_instructors_and_instructors_returned_by_search(self):
+        """Ensure search returns everyone with defined airport."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport_1': self.airport_0_0.pk, 'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertIn(self.harry, response.context['persons'])
+        self.assertIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertIn(self.spiderman, response.context['persons'])
+        self.assertIn(self.ironman, response.context['persons'])
+        self.assertIn(self.blackwidow, response.context['persons'])
+
+    def test_match_on_one_skill(self):
+        """Ensure people with correct skill are returned."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport_1': self.airport_50_100.pk, 'lessons': [self.git.pk],
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        # lessons
+        self.assertIn(self.git, response.context['lessons'])
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertNotIn(self.harry, response.context['persons'])
+        self.assertIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertNotIn(self.blackwidow, response.context['persons'])
+
+    def test_match_instructors_on_two_skills(self):
+        """Ensure people with correct skills are returned."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport_1': self.airport_50_100.pk,
+             'lessons': [self.git.pk, self.sql.pk],
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        # lessons
+        self.assertIn(self.git, response.context['lessons'])
+        self.assertIn(self.sql, response.context['lessons'])
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertNotIn(self.harry, response.context['persons'])
+        self.assertNotIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertNotIn(self.blackwidow, response.context['persons'])
+
+    def test_match_by_country(self):
+        """Ensure people with airports in Bulgaria are returned."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'country': ['BG'], 'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # instructors
+        self.assertNotIn(self.hermione, response.context['persons'])
+        self.assertIn(self.harry, response.context['persons'])
+        self.assertNotIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertIn(self.blackwidow, response.context['persons'])
+
+    def test_match_by_multiple_countries(self):
+        """Ensure people with airports in Albania and Bulgaria are returned."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'country': ['AL', 'BG'], 'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertIn(self.harry, response.context['persons'])
+        self.assertNotIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertIn(self.blackwidow, response.context['persons'])
+
+    def test_match_gender(self):
+        """Ensure only people with specific gender are returned."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk, 'gender': 'F', 'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertNotIn(self.harry, response.context['persons'])
+        self.assertNotIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertIn(self.blackwidow, response.context['persons'])
+
+    def test_instructor_badges(self):
+        """Ensure people with instructor badges are returned by search."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'instructor_badges': ['swc-instructor', 'dc-instructor'],
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # instructors
+        self.assertIn(self.hermione, response.context['persons'])
+        self.assertIn(self.harry, response.context['persons'])
+        # Ron doesn't have a DC badge
+        self.assertNotIn(self.ron, response.context['persons'])
+        # non-instructors
+        self.assertNotIn(self.spiderman, response.context['persons'])
+        self.assertNotIn(self.ironman, response.context['persons'])
+        self.assertNotIn(self.blackwidow, response.context['persons'])
+
+    def test_roles(self):
+        """Ensure people with at least one helper/organizer roles are returned
+        by search."""
+        # prepare events and tasks
+        self._setUpEvents()
+        helper_role = Role.objects.get(name='helper')
+        organizer_role = Role.objects.get(name='organizer')
+
+        Task.objects.create(role=helper_role, person=self.spiderman,
+                            event=Event.objects.first())
+        Task.objects.create(role=organizer_role, person=self.blackwidow,
+                            event=Event.objects.first())
+
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'was_helper': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(list(response.context['persons']), [self.spiderman])
+
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'was_organizer': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(list(response.context['persons']), [self.blackwidow])
+
+    def test_form_logic(self):
+        """Check if logic preventing searching from multiple fields,
+        except lat+lng pair, and allowing searching from no location field,
+        works."""
+        test_vectors = [
+            (True, {'latitude': 1, 'longitude': 2}),
+            (True, dict()),
+            (False, {'latitude': 1}),
+            (False, {'longitude': 1, 'country': ['BG']}),
+            (False, {'latitude': 1, 'longitude': 2, 'country': ['BG']}),
+            (False, {'latitude': 1, 'longitude': 2, 'country': ['BG'],
+                     'airport': self.airport_0_0.pk}),
+        ]
+
+        for form_pass, data in test_vectors:
+            with self.subTest(data=data):
+                params = dict(submit='Submit')
+                params.update(data)
+                rv = self.client.get(reverse('workshop_staff'), params)
+                form = rv.context['form']
+                self.assertEqual(form.is_valid(), form_pass, form.errors)
+
+    def test_searching_trainees(self):
+        """Make sure finding trainees works."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'is_in_progress_trainee': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(list(response.context['persons']), [])
+
+        TTT = Tag.objects.get(name='TTT')
+        stalled = Tag.objects.get(name='stalled')
+        e1 = Event.objects.create(slug='TTT-event', host=Host.objects.first())
+        e1.tags = [TTT]
+        e2 = Event.objects.create(slug='stalled-TTT-event',
+                                  host=Host.objects.first())
+        e2.tags = [TTT, stalled]
+
+        learner = Role.objects.get(name='learner')
+        # Ron is an instructor, so he should not be available as a trainee
+        Task.objects.create(person=self.ron, event=e1, role=learner)
+        Task.objects.create(person=self.ron, event=e2, role=learner)
+        # Black Widow, on the other hand, is now practising to become certified
+        # SWC instructor!
+        Task.objects.create(person=self.blackwidow, event=e1, role=learner)
+        # Spiderman tried to became an instructor, but hasn't finished it yet
+        Task.objects.create(person=self.spiderman, event=e2, role=learner)
+
+        # repeat the query
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'is_in_progress_trainee': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(list(response.context['persons']), [self.blackwidow])
+
+    @unittest.expectedFailure
+    def test_searching_complicated_trainee(self):
+        """Ensure person who took part in one stalled TTT event and one
+        non-stalled TTT event is correctly marked as in-progress trainee.
+
+        This test should fail because Django ORM fails with annotations.
+        See https://github.com/swcarpentry/amy/pull/776#discussion_r63953993
+        and https://github.com/swcarpentry/amy/issues/804."""
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'is_in_progress_trainee': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(list(response.context['persons']), [])
+
+        TTT = Tag.objects.get(name='TTT')
+        stalled = Tag.objects.get(name='stalled')
+        e1 = Event.objects.create(slug='TTT-event', host=Host.objects.first())
+        e1.tags = [TTT]
+        e2 = Event.objects.create(slug='stalled-TTT-event',
+                                  host=Host.objects.first())
+        e2.tags = [TTT, stalled]
+
+        learner = Role.objects.get(name='learner')
+
+        # Ron is an instructor, so he should not be available as a trainee
+        Task.objects.create(person=self.ron, event=e1, role=learner)
+        Task.objects.create(person=self.ron, event=e2, role=learner)
+        # Black Widow, on the other hand, is now practising to become certified
+        # SWC instructor!
+        Task.objects.create(person=self.blackwidow, event=e1, role=learner)
+        # Ironman tried to became an instructor via one event, but didn't
+        # finish and now is taking part in another TTT event
+        Task.objects.create(person=self.ironman, event=e1, role=learner)
+        Task.objects.create(person=self.ironman, event=e2, role=learner)
+
+        # repeat the query
+        response = self.client.get(
+            reverse('workshop_staff'),
+            {'airport': self.airport_0_0.pk,
+             'is_in_progress_trainee': 'on',
+             'submit': 'Submit'}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.ironman.pk, response.context['trainees'])
+        self.assertEqual(list(response.context['persons']),
+                         [self.blackwidow, self.ironman])