def test_copy_and_mutate(self): schema = mock.Mock() chain = FilterChain(schema=schema) chain.lookup_descriptions.append(1) chain.base_url = 'http://xyz' chain['foo'] = 'bar' chain['qux'] = 'whee' clone = chain.copy() # Attributes are copied... self.assertEqual(clone.lookup_descriptions, [1]) self.assertEqual(clone.base_url, chain.base_url) self.assertEqual(clone.schema, chain.schema, schema) # ... and mutating them doesn't affect the original. clone.lookup_descriptions.pop() self.assertEqual(chain.lookup_descriptions, [1]) # Likewise, items are copied, and mutating doesn't affect the copy. self.assertEqual(clone['foo'], 'bar') del chain['foo'] self.assertEqual(clone['foo'], 'bar') del clone['qux'] self.assertEqual(chain['qux'], 'whee') # Likewise, clearing. clone.clear() self.assertEqual(clone.items(), []) self.assertEqual(chain['qux'], 'whee')
def test_add_by_place_id__bad(self): chain = FilterChain() from django.http import Http404 self.assertRaises(Http404, chain.add_by_place_id, '') self.assertRaises(Http404, chain.add_by_place_id, 'blah') self.assertRaises(Http404, chain.add_by_place_id, 'b:123.1') self.assertRaises(Http404, chain.add_by_place_id, 'l:9999')
def get_featured_lookups_by_schema(context): """ Get all :ref:`featured_lookups` names and URLs for them; puts in the context as 'featured_lookups', a mapping grouped by schema. Example: .. code-block:: html+django {% get_featured_lookups_by_schema %} {% for schema, lookups in featured_lookups.items %} <ul>{{ schema }} {% for info in lookups %} <a href="{{ info.url }}">{{ info.lookup }}</a> ... {% endfor %} {% endfor %} """ lookups = {} for lookup in Lookup.objects.filter(featured=True).select_related(): sf = lookup.schema_field schema = sf.schema filters = FilterChain(schema=schema) filters.add(sf, lookup) info = {'lookup': lookup.name, 'url': filters.make_url()} lookups.setdefault(schema.slug, []).append(info) context['featured_lookups'] = lookups return u''
def _make_chain(self, url): request = RequestFactory().get(url) crime = models.Schema.objects.get(slug='crime') context = {'schema': crime} chain = FilterChain(request=request, context=context, schema=crime) chain.update_from_request(filter_sf_dict={}) return chain
def render(self, context): filterchain = self.filterchain_var.resolve(context) if isinstance(filterchain, FilterChain): schema = filterchain.schema elif isinstance(filterchain, Schema): schema = filterchain # Note, context['request'] only works if # django.core.context_processors.request is enabled in # TEMPLATE_CONTEXT_PROCESSORS. filterchain = FilterChain(context=context, request=context['request'], schema=schema) else: raise template.TemplateSyntaxError( "%r is neither a FilterChain nor a Schema" % filterchain) removals = [r.resolve(context) for r in self.removals] if self.clear: filterchain = filterchain.copy() filterchain.clear() additions = [] for key, values in self.additions: key = key.resolve(context) additions.append((key, [v.resolve(context) for v in values])) schema = filterchain.schema return filterchain.make_url(additions=additions, removals=removals)
def _make_chain(self, url): request = RequestFactory().get(url) argstring = request.path.split('filter/', 1)[-1] crime = models.Schema.objects.get(slug='crime') context = {'schema': crime} chain = FilterChain(request=request, context=context, schema=crime) chain.update_from_request(argstring=argstring, filter_sf_dict={}) return chain
def test_no_duplicates(self): from ebpub.db.schemafilters import DuplicateFilterError self.assertRaises(DuplicateFilterError, FilterChain, (('foo', 'bar'), ('foo', 'bar2'))) chain = FilterChain() chain['foo'] = 'bar' self.assertRaises(DuplicateFilterError, chain.__setitem__, 'foo', 'bar')
def test_add_date__whole_month(self): # Special syntax for adding a whole month, convenient for templates # where you don't want to have to calculate the end date. chain = FilterChain() import datetime chain.add('date', datetime.date(2011, 8, 13), 'month') self.assertEqual(chain['date'].start_date, datetime.date(2011, 8, 1)) self.assertEqual(chain['date'].end_date, datetime.date(2011, 8, 31))
def test_ordering(self): chain = FilterChain() args = range(10) random.shuffle(args) for i in args: chain[i] = i self.assertEqual(chain.items(), [(i, i) for i in args]) self.assertEqual(chain.keys(), args)
def test_add__pubdate(self): # Key gets overridden. chain = FilterChain() import datetime chain.add('pubdate', datetime.date(2011, 8, 13)) self.assert_(chain.has_key('date')) self.failIf(chain.has_key('pubdate')) self.assertEqual(chain['date'].start_date, datetime.date(2011, 8, 13))
def bigmap_filter(request, slug, args_from_url): s = get_object_or_404(get_schema_manager(request), slug=slug, is_special_report=False) if not s.allow_charting: return HttpResponse(status=404) filter_sf_list = list( SchemaField.objects.filter(schema__id=s.id, is_filter=True).order_by('display_order')) textsearch_sf_list = list( SchemaField.objects.filter( schema__id=s.id, is_searchable=True).order_by('display_order')) # Use SortedDict to preserve the display_order. filter_sf_dict = SortedDict([(sf.name, sf) for sf in filter_sf_list] + [(sf.name, sf) for sf in textsearch_sf_list]) # Determine what filters to apply, based on path and/or query string. filterchain = FilterChain(request=request, schema=s) try: filterchain.update_from_request(args_from_url, filter_sf_dict) filters_need_more = filterchain.validate() except: return HttpResponse(status=404) config = _decode_map_permalink(request, show_default_layers=False, filters=filterchain) new_url = filterchain.make_url( base_url=reverse('bigmap_filter', args=(slug, ))) if new_url != request.get_full_path(): return HttpResponseRedirect(new_url) # add in the filter layer base_url = reverse('ebpub-schema-filter-geojson', args=(slug, )) layer_url = filterchain.make_url(base_url=base_url) custom_layer = { 'url': layer_url, 'params': {}, 'title': "Custom Filter", 'visible': True } config['layers'].append(custom_layer) if config['is_widget']: return eb_render(request, 'richmaps/embed_bigmap.html', {'map_config': simplejson.dumps(config, indent=2)}) else: return eb_render(request, 'richmaps/bigmap.html', {'map_config': simplejson.dumps(config, indent=2)})
def bigmap(request): ''' Big map with all Schemas enabled by default. ''' filterchain = FilterChain(request=request) config = _decode_map_permalink(request, filters=filterchain) if config['is_widget']: return eb_render(request, 'richmaps/embed_bigmap.html', {'map_config': simplejson.dumps(config, indent=2)}) else: return eb_render(request, 'richmaps/bigmap.html', {'map_config': simplejson.dumps(config, indent=2)})
def test_filters_for_display(self): class Dummy(object): def __init__(self, label): self.label = label chain = FilterChain([ ('foo', Dummy('yes')), ('bar', Dummy(None)), ('bat', Dummy('yes also')), ('baz', Dummy(None)), ]) self.assertEqual(len(chain.values()), 4) self.assertEqual(len(chain.filters_for_display()), 2) self.assert_(all([f.label for f in chain.filters_for_display()]))
def test_update_from_request__empty(self): request = mock.Mock() class StubQueryDict(dict): def getlist(self, key): return [] def copy(self): return StubQueryDict(self.items()) request.GET = StubQueryDict() chain = FilterChain(request=request) chain.update_from_request({}) self.assertEqual(len(chain), 0)
def test_add_by_place_id(self, mock_get_object_or_404): chain = FilterChain() from ebpub.streets.models import Block from ebpub.db.schemafilters import BlockFilter block = Block( city='city', street_slug='street_slug', pretty_name='pretty_name', street_pretty_name='street_pretty_name', street='street', from_num='123', to_num='456', ) mock_get_object_or_404.return_value = block chain.add_by_place_id('b:123.1') self.assert_(isinstance(chain['location'], BlockFilter))
def _get_filterchain(self, context): filterchain_or_schema = self.filterchain_var.resolve(context) if isinstance(filterchain_or_schema, FilterChain): filterchain = filterchain_or_schema elif isinstance(filterchain_or_schema, Schema): # Note, context['request'] only works if # django.core.context_processors.request is enabled in # TEMPLATE_CONTEXT_PROCESSORS. filterchain = FilterChain(context=context, request=context.get('request'), schema=filterchain_or_schema) else: raise template.TemplateSyntaxError( "%r is neither a FilterChain nor a Schema" % filterchain_or_schema) if self.clear: filterchain = filterchain.copy() filterchain.clear() return filterchain
def test_sort(self): class Dummy(object): def __init__(self, sort_value): self._sort_value = sort_value dummies = [Dummy(i) for i in range(10)] random.shuffle(dummies) chain = FilterChain() for i in range(10): chain[i] = dummies[i] self.assertNotEqual(range(10), [v._sort_value for v in chain.values()]) normalized = chain.copy() normalized.sort() self.assertEqual(range(10), [v._sort_value for v in normalized.values()])
def test_sort__real_filters(self): req = mock.Mock() qs = mock.Mock() schema = mock.Mock() context = {'newsitem_qs': qs, 'schema': schema} from ebpub.db.schemafilters import TextSearchFilter, BoolFilter from ebpub.db.schemafilters import LookupFilter, LocationFilter from ebpub.db.schemafilters import DateFilter def mock_schemafield(name): # mock.Mock(name='foo') does something magic, but I just # want to set the name attribute. sf = mock.Mock() sf.name = name return sf all_filters = [ TextSearchFilter( req, context, qs, 'hi', schemafield=mock_schemafield(name='mock text sf')), BoolFilter(req, context, qs, 'yes', schemafield=mock_schemafield(name='mock bool sf')), LookupFilter(req, context, qs, schemafield=mock_schemafield(name='mock lookup sf')), LocationFilter(req, context, qs, 'neighborhoods'), DateFilter(req, context, qs, '2011-04-11', '2011-04-12'), ] chain = FilterChain([(item.slug, item) for item in all_filters]) ordered_chain = chain.copy() ordered_chain.sort() self.assertEqual(ordered_chain.keys(), [ 'date', 'mock bool sf', 'location', 'mock lookup sf', 'mock text sf' ])
def bigmap_filter(request, slug): """ Big map with just one Schema (identified by ``slug``) enabled by default. """ s = get_object_or_404(get_schema_manager(request), slug=slug, is_special_report=False) if not s.allow_charting: return HttpResponse(status=404) filter_sf_dict = _get_filter_schemafields(s) # Determine what filters to apply, based on path and/or query string. filterchain = FilterChain(request=request, schema=s) try: filterchain.update_from_request(filter_sf_dict) filters_need_more = filterchain.validate() except: logger.exception("Unhandled error") return HttpResponse(status=404) config = _decode_map_permalink(request, show_default_layers=False, filters=filterchain) # TODO: This can leave in permalink params eg. 'i', even if there # is also 'ids', because it doesn't recognize those as being the # same. new_url = filterchain.make_url( base_url=reverse('bigmap_filter', args=(slug, ))) if new_url != request.get_full_path(): return HttpResponseRedirect(new_url) if config['is_widget']: return eb_render(request, 'richmaps/embed_bigmap.html', {'map_config': simplejson.dumps(config, indent=2)}) else: return eb_render(request, 'richmaps/bigmap.html', {'map_config': simplejson.dumps(config, indent=2)})
def _decode_map_permalink(request, show_default_layers=True, filters=None): """ Permalinks for the big map, with more compact query parameters. Returns a map_config dictionary. Accepted parameters: c - map center, separated by underscore, eg. c=-92.28283_38.95658 z - map zoom, eg. z=12 l - layers to display on load, comma- or dash-separated, eg. l=p13,t32,p1 or eg. l=p12345-t7-t9, where p => place layer and t => schema ("type") layer i - items to load specificially by id, comma- or dash-separated, eg. i=t1234-t456 p - popup center, with underscore, eg. p=-92.3438_38.9658 f - popup feature, eg. f=t1234 or f=p1234 where p = a place and t = a news item start_date - start date (inclusive) %m/%d/%Y end_date - end date (inclusive) %m/%d/%Y d - duration in days (overridden by end date), eg. d=7 x - show as 'widget', just the map and nothign around it. Takes no value, eg. x w - width of map (widget only), in pixels h - height of map (widget only), in pixels v - limits what map controls are displayed (widget only). By default, widget-stype map shows none of these. Possible values, joined with no separator: l - layer switcher h - list of headlines next to map p - permalink eg. to turn them all on: v=lhp """ params = request.GET schemas = set() place_types = set() lids = params.get("l", None) show_custom_layer = False if lids is not None: no_layers_specified = False try: pat = re.compile('(\w\d+)') for lid in pat.findall(lids): layer_type = lid[0] layer_id = int(lid[1:]) if layer_type == 'p': place_types.add(layer_id) elif layer_type == 't': schemas.add(layer_id) elif layer_type == 'c': show_custom_layer = True except: pass else: no_layers_specified = True # map center center = params.get("c", None) if center: try: center = [float(x) for x in center.split('_')][0:2] except: pass # map zoom level zoom = params.get("z", None) if zoom: try: zoom = float(zoom) except: pass # popup popup_info = None popup_center = params.get("p", None) popup_feature = params.get("f", None) if popup_center and popup_feature: try: popup_center = [float(x) for x in popup_center.split('_')][0:2] feature_type = popup_feature[0] feature_id = int(popup_feature[1:]) if feature_type == 'p': openblock_type = 'place' elif feature_type == 't': openblock_type = 'newsitem' popup_info = { 'id': feature_id, 'openblock_type': openblock_type, 'lonlat': [popup_center[0], popup_center[1]] } except: popup_center = None popup_feature = None # start and end date range default_interval = datetime.timedelta(days=7) duration = params.get('d') if duration is not None: try: duration = datetime.timedelta(days=int(duration)) except (TypeError, ValueError): duration = default_interval else: duration = default_interval default_enddate = datetime.date.today() default_startdate = default_enddate - duration startdate = params.get('start_date') if startdate is not None: for format in ('%m/%d/%Y', '%Y-%m-%d'): try: startdate = datetime.datetime.strptime(startdate, format).date() break except ValueError: pass if isinstance(startdate, basestring): startdate = None enddate = params.get('end_date') if enddate is not None: for format in ('%m/%d/%Y', '%Y-%m-%d'): try: enddate = datetime.datetime.strptime(enddate, format).date() break except ValueError: pass if isinstance(enddate, basestring): enddate = None # The filters argument can override startdate & enddate. if startdate is None and enddate is None and filters: date_filter = filters.get('date') or filters.get('pubdate') if date_filter: startdate = date_filter.start_date enddate = date_filter.end_date if startdate is None and enddate is None: enddate = datetime.date.today() startdate = enddate - duration elif startdate is None: startdate = enddate - duration elif enddate is None: enddate = startdate + duration if enddate < startdate: enddate = startdate + duration # inject date range into filters if none was specified: if filters and filters.get('date') is None: filters.add('date', startdate, enddate) api_startdate = startdate.strftime("%Y-%m-%d") api_enddate = (enddate + datetime.timedelta(days=1)).strftime("%Y-%m-%d") layers = [] if (startdate != default_startdate) or (enddate != default_enddate): show_custom_layer = True # All available place layers. for place_type in PlaceType.objects.filter(is_mappable=True).all(): layers.append({ 'id': 'p%d' % place_type.id, 'title': place_type.plural_name, 'url': reverse('place_detail_json', args=[place_type.slug]), 'params': { 'limit': 1000 }, 'minZoom': 15, 'bbox': True, 'visible': place_type.id in place_types # off by default }) # All available NewsItem layers. for schema in get_schema_manager(request).all(): # if filters and 'schema' in filters and filters['schema'].schema == schema: # visible = True if no_layers_specified and show_default_layers and not show_custom_layer: # default on if no 't' param given visible = True elif schemas and schema.id in schemas: visible = True else: visible = False layers.append({ 'id': 't%d' % schema.id, 'title': schema.plural_name, 'url': reverse('map_items_json'), 'params': { 'type': schema.slug, 'limit': 1000, 'startdate': api_startdate, 'enddate': api_enddate }, 'bbox': False, 'visible': visible }) # Explicit filtering by ID. ids = params.get('i') or u'' ids = [i.strip() for i in re.split(r'[^\d]+', ids) if i.strip()] if ids: show_custom_layer = True if filters is None: filters = FilterChain(request) filters.replace('id', *ids) # 'Custom' layer. This is a catch-all for all filtering # that isn't just enabling a default layer with the default # date range. # Not visible unless there is something like that to show. if filters and sorted(filters.keys()) not in ([], ['date'], [ 'date', 'schema' ], ['schema']): show_custom_layer = True if filters is not None: # Don't inspect filters['schema']; that's already covered by schemas above. base_url = reverse('map_items_json') layer_url = filters.make_url(base_url=base_url) # Quick ugly hacks to make the itemquery api happy. # Hooray proliferation of spellings. layer_url = layer_url.replace('locations=', 'locationid=') layer_url = layer_url.replace('start_date=', 'startdate=') layer_url = layer_url.replace('end_date=', 'enddate=') if 'schema' in filters: # Normally, filters.make_url() captures the schema in the # path part of the URL. But map_items_json doesn't, # so we add a query parameter. params = {'type': [s.slug for s in filters['schema'].schemas]} else: params = {} custom_layer = { 'url': layer_url, 'params': params, 'title': u"Custom Filter", 'visible': show_custom_layer, 'id': 'c1', } layers.append(custom_layer) is_widget = params.get('x', None) is not None controls = {} control_list = params.get("v", None) if control_list is not None: if 'l' in control_list: controls['layers'] = True if 'h' in control_list: controls['headline_list'] = True if 'p' in control_list: controls['permalink'] = True width = params.get("w", None) if width: try: width = int(width) except: width = None height = params.get("h", None) if height: try: height = int(height) except: height = None config = { 'center': center or [settings.DEFAULT_MAP_CENTER_LON, settings.DEFAULT_MAP_CENTER_LAT], 'zoom': zoom or settings.DEFAULT_MAP_ZOOM, 'layers': layers, 'is_widget': is_widget, 'permalink_params': { 'start_date': startdate.strftime('%m/%d/%Y'), 'end_date': enddate.strftime('%m/%d/%Y'), }, } if 'id' in filters: # Put them in the params so the js code can construct, well, # permalinks with these ids, on the client side. ids = '-'.join(map(str, filters['id'].ids)) config['permalink_params']['i'] = ids if popup_info: config['popup'] = popup_info if is_widget: config['controls'] = controls if width is not None: config['width'] = width if height is not None: config['height'] = height return config
def test_empty(self): chain = FilterChain() self.assertEqual(chain.items(), [])
def test_add__id(self): from ebpub.db.schemafilters import IdFilter chain = FilterChain() chain.add('id', [1, 2, 3]) self.assert_(isinstance(chain['id'], IdFilter))
def test_add__bogus_keyword_arg(self): chain = FilterChain() self.assertRaises(TypeError, chain.add, 'date', '2011-01-1', foo='bar')
def test_add__no_value(self): chain = FilterChain() self.assertRaises(FilterError, chain.add, 'date')