def autocomplete_species(request, place_id): place = get_object_or_404(Place, pk = place_id) q = request.GET.get('q', '') results = list(search_species(q, 30)) # We're going to build three groups - species that have been sighted at # this place, species that have been sighted somewhere else, and species # that have not been sighted at all. seen_here_group = [] seen_elsewhere_group = [] not_seen_group = [] for result in results: # If it's in our Species database, annotate with a bunch of stuff species = None try: species = Species.objects.get(freebase_id = result['freebase_id']) except Species.DoesNotExist: pass if not species: result['select_id'] = 'x_%s' % result['search_id'] not_seen_group.append(result) continue # Matches a known species result['select_id'] = 's_%s' % species.id # Annotate with photo photos = Photo.objects.filter( sightings__species = species, is_visible = True ).distinct() result['photo'] = photos and unicode(photos[0].photo.thumbnail) or '' # Assign to a group and annotate with number of previous sightings num_sightings_here = Sighting.objects.filter( species = species, place = place ).count() if num_sightings_here: result['num_sightings'] = num_sightings_here seen_here_group.append(result) continue num_sightings = Sighting.objects.filter( species__freebase_id = result['freebase_id'] ).count() if num_sightings: result['num_sightings'] = num_sightings seen_elsewhere_group.append(result) continue # Not seen anywhere before not_seen_group.append(result) # Order each group by number of sightings, with most at the top seen_here_group.sort(key = lambda r: r['num_sightings'], reverse=True) seen_elsewhere_group.sort( key = lambda r: r['num_sightings'], reverse=True ) return render(request, 'trips/autocomplete_species.html', { 'seen_here': seen_here_group, 'seen_elsewhere': seen_elsewhere_group, 'not_seen': not_seen_group, 'q': q, })
def pick_sightings(request, redirect_to): """ A replacement for the old add_sightings view. This one is designed to sit in between two different parts of the interface. It narrows down the list of entered text to concrete species as before, then forwards the user on to the redirect_to URL with concrete species IDs attached. For example, the resulting redirect may look like this: /gb/london-zoo/add-trip/?saw=x:2ab1&saw=s:123&saw=A+baby+tiger Note that there are now three types of saw IDs: x:* = a Xapian search ID s:* = an animals_species table ID * = free text not matched in our database Our sightings recording mechanism is now expected to be able to deal with free text, which will be recorded using species_inexact on a Sighting. The INPUT to this view should start off as something like this: ../pick-sightings/?saw.1.s=tiger&saw.2.s=pony If there are any ?saw= items, the view redirects straight away turning them in to that format. So, the easiest way to get started with the process is to send someone to: ../pick-sightings/?saw=tiger&saw=pony If called with no arguments, starts off with an empty "I saw" form Additionally, if you include a `trip` parameter in the query string, that's the id of the trip we'll add to directly; this just gets passed through to (This is an ugly implementation in terms of URLs, but was the easiest.) """ if request.GET.getlist('saw'): # Convert ?saw= arguments to ?saw.1.s= type things return Redirect(request.path + '?' + urllib.urlencode([ ('saw.%d.s' % i, saw.strip()) for i, saw in enumerate(request.GET.getlist('saw')) if saw.strip() ])) saw = DotExpandedDict(request.GET).get('saw') if request.GET and not saw: # Clear out the invalid query string return Redirect(request.path) if not saw: assert False, "Jump straight to showing the empty form" # saw should now of format {'0': {'s': 'tiger', 'o': 'x:ba3'}...} # Essentially a dictionary of key/dict pairs. The key doesn't actually # matter, it's just used to group values. The dict looks like this: # s = The text the user entered (required) # o = The option they picked to fulfill it (optional) # r = The replacement search they want to run (defaults to same as s) # The 'o' value comes from a radio select and can be of these formats: # x_* = a Xapian search ID they have selected # s_* = a Species ID they have selected # cancel = don't save this at all (e.g. "I made a mistake") # as-is = save it using species_inexact # search-again = run the replacement search instead # Are we done yet? The aim is for every key to have a valid option # provided that isn't 'search-again'. if saw and is_valid_saw_list(saw.values()): next_vars = [ urllib.urlencode({ 'saw': d['o'] == 'as-is' and d.get('s') or d['o'] }) for d in saw.values() if d.get('s') and (d['o'] != 'cancel') ] if request.GET.get('trip', None): next_vars.append( urllib.urlencode({ 'trip': request.GET['trip'] }) ) return Redirect(redirect_to + '?' + '&'.join(next_vars)) # We aren't done, so we need to build up all of the information required # to construct our big form full of options, search results etc sections = [] section_id = 0 label_id = 0 if saw: for key in sorted(saw.keys(), key = lambda k: int(k)): d = saw[key] search = d.get('r', d['s']).strip() if not search: continue results = list(search_species(search, 20)) db_matches = list(Species.objects.filter(freebase_id__in = [ r['freebase_id'] for r in results ])) choices = [] is_first = True for row in results: try: id = 's_%s' % [ s for s in db_matches if s.freebase_id == row['freebase_id'] ][0].id except IndexError: id = 'x_%s' % row['search_id'] choices.append({ 'id': id, 'common_name': row['common_name'], 'scientific_name': row['scientific_name'], 'label_id': label_id, 'checked': (d.get('o') == id or ( is_first and d.get('o') != 'cancel' )), }) label_id += 1 is_first = False sections.append({ 'id': section_id, 'search': d.get('r', d['s']), # Pick up replacement search 'options': choices, 'o': d.get('o'), }) section_id += 1 return render(request, 'trips/pick_sightings.html', { 'sections': sections, 'redirect_to': redirect_to, 'bonus_label_id': section_id, 'trip_id': request.GET.get('trip', None), })