Changeset f9de3a071de31c2330c09829345ebe75ff4453e1


Ignore:
Timestamp:
01/18/12 19:39:25 (4 months ago)
Author:
Paul Winkler <slinkp@…>
Children:
2251676f7059cf73b5b11b110f24cb2d6e734901
Parents:
8e5456649376cd5502e368c808d7e01aed7163b0
git-committer:
Paul Winkler <slinkp@…> (01/18/12 19:39:25)
Message:

Rewrite of URLs used by schemafilters: use query strings instead of weird "matrix" URIs.
Closes #266. All tests pass.

Location:
ebpub/ebpub/db
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • ebpub/ebpub/db/schemafilters.py

    r732439 rf9de3a  
    5555import ebpub.streets.models 
    5656import logging 
     57import posixpath 
    5758import re 
    5859import urllib 
     
    7374 
    7475    slug = None   # ID for this type of filter. Used with FilterChain.add() / .remove() 
    75     url = None  # Actually a URL fragment. 
    7676    value = None  # Value fed to the filter, for display. 
    7777    label = None  # Human-readable name of the filter, for display. 
    7878    short_value = None  # Shorter version of value fed to the filter, for display. 
     79    argname = None  # For generating query parameters for URLs. 
     80    query_param_value = None  # For generating query parameters for URLs. 
    7981 
    8082    def __init__(self, request, context, queryset=None, *args, **kw): 
     
    111113        except AttributeError: 
    112114            raise KeyError(key) 
     115 
     116    def get_query_params(self): 
     117        """ 
     118        Suitable for composing query strings out of dictionaries. 
     119        """ 
     120        return {self.argname: self.query_param_value or ''} 
    113121 
    114122 
     
    172180        self.slug = self.schemafield.name 
    173181        self.argname = 'by-%s' % self.slug 
    174         self.url = 'by-%s=' % self.slug 
    175182        self.value = self.short_value = ''  # Descriptions of this filter, for display. 
    176183        self.label = self.schemafield.pretty_name 
     
    188195            else: 
    189196                str_att_value = str(self.att_value) 
    190             self.url += str_att_value 
     197            self.query_param_value = str_att_value 
    191198 
    192199    def apply(self): 
     
    209216        self.short_value = self.query 
    210217        self.value = self.query 
    211         self.url = 'by-%s=%s' % (self.schemafield.name, self.query) 
     218        self.argname = 'by-' + self.schemafield.name 
     219        self.query_param_value = ','.join(args) 
    212220 
    213221    def apply(self): 
     
    234242            if self.real_val not in (True, False, None): 
    235243                raise FilterError('Invalid boolean value %r' % self.boolslug) 
    236             self.url = 'by-%s=%s' % (self.schemafield.name, self.boolslug) 
     244            self.argname = 'by-%s' % self.schemafield.name 
     245            self.query_param_value = self.boolslug 
    237246            self._got_args = True 
    238247        else: 
     
    290299            self.value = ', '.join([lk.name for lk in self.lookups]) 
    291300            self.short_value = self.value 
    292             self.url = 'by-%s=%s' % (self.schemafield.name, ','.join(slugs)) 
     301            self.query_param_value = ','.join(slugs) 
    293302 
    294303    def validate(self): 
     
    315324 
    316325    slug = 'location' 
     326    argname = 'locations' 
    317327 
    318328    def __init__(self, request, context, queryset, *args, **kwargs): 
     
    331341                    raise FilterError("not enough args") 
    332342                self.location_type_slug = args[0] 
    333             self.url = 'locations=%s' % self.location_type_slug 
    334343            self.value = 'Choose %s' % self.location_type_slug.title() 
    335344            try: 
     
    350359        self.short_value = loc.name 
    351360        self.value = loc.name 
    352         self.url = 'locations=%s,%s' % (self.location_type_slug, self.location_slug) 
    353361        self.location_name = loc.name 
    354362        self.location_object = loc 
     363        self.query_param_value = '%s,%s' % (self.location_type_slug, 
     364                                            self.location_slug) 
     365 
    355366 
    356367    def validate(self): 
     
    397408        self.short_value = value 
    398409        self.value = value 
    399         self.url = 'streets=' 
     410        self.argname = 'streets' 
     411        self.query_param_value = [] 
    400412        if get_metro()['multiple_cities']: 
    401             self.url += self.city_slug + ',' 
    402         self.url += '%s,%s%s,%s' % (block.street_slug, 
    403                                     block.number(), block.dir_url_bit(), 
    404                                     radius_slug(self.block_radius)) 
     413            self.query_param_value.append(self.city_slug) 
     414        self.query_param_value.extend([block.street_slug, 
     415                                       block.number() + block.dir_url_bit(), 
     416                                       radius_slug(self.block_radius)]) 
     417        self.query_param_value = ','.join(self.query_param_value) 
    405418        self.location_name = block.pretty_name 
    406419 
     
    438451                # view. 
    439452                xy_radius, block_radius, cookies_to_set = block_radius_value(request) 
    440                 radius_url = u'%s,%s/' % (request.path.rstrip('/'), 
    441                                           radius_slug(block_radius)) 
     453                radius_param = urllib.quote(',' + radius_slug(block_radius)) 
     454                radius_url = request.get_full_path() + radius_param 
    442455                raise FilterError('missing radius', url=radius_url) 
    443456 
     
    477490    """Filters on NewsItem.item_date. 
    478491    The start_date and end_date args are *inclusive*. 
     492    They can be the same; missing end_date implies start == end. 
    479493    """ 
    480494 
    481495    slug = 'date' 
    482496    date_field_name = 'item_date' 
    483     _argname = 'by-date' 
     497    argname = 'by-date' 
    484498 
    485499    _sort_value = 1.0 
     
    498512        gte_kwarg = '%s__gte' % self.date_field_name 
    499513        lt_kwarg = '%s__lt' % self.date_field_name 
    500         try: 
    501             start_date, end_date = args 
    502             if isinstance(start_date, basestring): 
    503                 start_date = datetime.date(*map(int, start_date.split('-'))) 
    504             self.start_date = start_date 
    505             if isinstance(end_date, basestring): 
    506                 end_date = datetime.date(*map(int, end_date.split('-'))) 
    507             self.end_date = end_date 
    508         except (IndexError, ValueError, TypeError): 
    509             raise FilterError("Missing or invalid date range") 
     514        if not args: 
     515            raise FilterError("Missing date range") 
     516 
     517        start_date = args[0] 
     518        end_date = args[-1] 
     519 
     520        def _parse(date): 
     521            # Ugh, papering over wild proliferation of date formats. 
     522            try: 
     523                date = parse_date(date, '%m/%d/%Y') 
     524            except ValueError: 
     525                try: 
     526                    date = parse_date(date, '%Y/%m/%d') 
     527                except ValueError: 
     528                    try: 
     529                        date = datetime.date(*map(int, date.split('-'))) 
     530                    except ValueError: 
     531                        raise BadDateException("Unknown date format on %r" % date) 
     532            return date 
     533 
     534        assert end_date is not None 
     535 
     536        if isinstance(start_date, basestring): 
     537            start_date = _parse(start_date) 
     538        if isinstance(end_date, basestring): 
     539            end_date = _parse(end_date) 
     540        elif end_date is None: 
     541            end_date = start_date 
     542 
     543        for d in (start_date, end_date): 
     544            if d and d.year < 1900: 
     545                # This prevents strftime from throwing a ValueError. 
     546                raise BadDateException('Dates before 1900 are not supported.') 
     547 
     548        assert end_date is not None 
     549        self.start_date = start_date 
     550        self.end_date = end_date 
    510551 
    511552        self.kwargs = { 
     
    519560            self.value = u'%s - %s' % (dateformat.format(self.start_date, 'N j, Y'), dateformat.format(self.end_date, 'N j, Y')) 
    520561 
    521         self.short_value = self.value 
    522         self.url = '%s=%s,%s' % (self._argname, 
    523                                  self.start_date.strftime('%Y-%m-%d'), 
    524                                  self.end_date.strftime('%Y-%m-%d')) 
    525  
    526  
     562    def get_query_params(self): 
     563        return {'start_date': self.start_date.strftime('%Y-%m-%d'), 
     564                'end_date': self.end_date.strftime('%Y-%m-%d')} 
    527565 
    528566    def validate(self): 
     
    541579    """ 
    542580 
    543     _argname = 'by-pub-date' 
     581    argname = 'by-pubdate' 
    544582    date_field_name = 'pub_date' 
    545583 
     
    549587        DateFilter.__init__(self, request, context, queryset, *args, **kwargs) 
    550588        self.label = 'date published' 
     589 
     590    def get_query_params(self): 
     591        return {'start_pubdate': self.start_date.strftime('%Y-%m-%d'), 
     592                'end_pubdate': self.end_date.strftime('%Y-%m-%d')} 
    551593 
    552594 
     
    607649            self[k] = v 
    608650 
    609     def update_from_request(self, argstring, filter_sf_dict): 
    610         """Update the list of filters based on the path and/or query string. 
    611  
    612         ``argstring`` is the portion of the path that describes the 
    613         filters (or None, in the case of "/filter/"). 
     651    def update_from_request(self, filter_sf_dict): 
     652        """Update the list of filters based on the request params. 
     653 
     654        After this is called, it's recommended to redirect to a 
     655        normalized form of the URL, which you can get via self.sort(); 
     656        self.make_url() 
    614657 
    615658        ``filter_sf_dict`` is a mapping of name -> SchemaField which have 
     
    620663        """ 
    621664        request, context = self.request, self.context 
    622         # TODO: can we remove some args now that we're not using 
    623         # get_place_info_for_request? 
    624         argstring = urllib.unquote((argstring or '').rstrip('/')) 
    625         argstring = argstring.replace('+', ' ') 
    626         args = [] 
    627  
    628         if argstring and argstring != 'filter': 
    629             for arg in argstring.split(';'): 
    630                 try: 
    631                     argname, argvalues = arg.split('=', 1) 
    632                 except ValueError: 
    633                     raise FilterError('Invalid filter parameter %r, no equals sign' % arg) 
    634                 argname = argname.strip() 
    635                 argvalues = [v.strip() for v in argvalues.split(',')] 
    636                 if argname: 
    637                     args.append((argname, argvalues)) 
    638         else: 
    639             # No filters specified. Do nothing? 
    640             pass 
    641  
    642665        qs = self.qs 
    643         while args: 
    644             argname, argvalues = args.pop(0) 
    645             argvalues = [v for v in argvalues if v] 
    646             # Date range 
    647             if argname == 'by-date': 
    648                 self['date'] = DateFilter(request, context, qs, *argvalues, schema=self.schema) 
    649             elif argname == 'by-pub-date': 
    650                 self['date'] = PubDateFilter(request, context, qs, *argvalues, schema=self.schema) 
    651  
    652             # Street/address 
    653             elif argname.startswith('streets'): 
    654                 self['location'] = BlockFilter(request, context, qs, *argvalues) 
    655             # Location filtering 
    656             elif argname.startswith('locations'): 
    657                 self['location'] = LocationFilter(request, context, qs, *argvalues) 
    658  
    659             # Attribute filtering 
    660             elif argname.startswith('by-'): 
    661                 sf_name = argname[3:] 
    662                 try: 
    663                     sf = filter_sf_dict.pop(sf_name) 
    664                 except KeyError: 
    665                     raise FilterError('Invalid or duplicate SchemaField name %r' % sf_name) 
    666                 self.add_by_schemafield(sf, *argvalues, _replace=True) 
    667  
    668             else: 
    669                 raise FilterError('Invalid filter type') 
    670  
    671         self.update_from_query_params(request) 
    672         self.sort() 
    673         return self 
    674  
    675     def update_from_query_params(self, request): 
    676         """ 
    677         Update the filters based on query parameters. 
    678  
    679         After this is called, it's recommended to redirect 
    680         to a normalized form of the URL, eg. self.sort(); self.make_url() 
    681  
    682         This takes care to preserve query parameters that aren't used 
    683         by any of the NewsitemFilters. 
    684         """ 
    685         # Make a mutable copy so we can leave only the params that FilterChain 
    686         # doesn't know about. 
    687666        params = request.GET.copy() 
    688         def pop_key(key): 
    689             # request.GET.pop() returns a sequence. 
    690             # We only want a single value, stripped. 
    691             val = params.pop(key, [''])[0] 
    692             return val.strip() 
    693  
    694         address = pop_key('address') 
     667 
     668        def pop_key(key, single=False): 
     669            """ 
     670            Pop the value(s) from params, treat it as a 
     671            comma-separated list of values, and split that into a 
     672            list. So ?foo=bar,baz is equivalent to ?foo=bar&foo=baz. 
     673 
     674            If single==True, return only the first one; in the example 
     675            we'd return 'bar'.  Otherwise, by default, return the 
     676            list; in the example we'd return ['bar', 'baz'] 
     677            """ 
     678            result = [] 
     679            for value in params.pop(key, [u'']): 
     680                value = value.replace(u'+', u' ') # XXX does django do this already? 
     681                values = [s.strip() for s in value.split(u',')] 
     682                result.extend(values) 
     683            result = [r for r in result if r] 
     684            if single: 
     685                return result[0] if result else u'' 
     686            return result 
     687 
     688        # Address. 
     689        address = pop_key('address', single=True) 
    695690        if address: 
    696691            xy_radius, block_radius, cookies_to_set = block_radius_value(request) 
    697             params.pop('radius', None) 
     692            pop_key('radius')  # Just to remove it, block_radius_value() used it. 
    698693            result = None 
    699694            try: 
     
    703698            except (GeocodingException, ParsingError): 
    704699                raise BadAddressException(address, block_radius, address_choices=()) 
    705  
    706700            assert result 
    707701            if result['block']: 
     
    722716            self.replace('location', block, block_radius) 
    723717 
    724  
    725         start_date = pop_key('start_date') 
    726         end_date = pop_key('end_date') 
    727         if start_date and end_date: 
    728             try: 
    729                 start_date = parse_date(start_date, '%m/%d/%Y') 
    730                 end_date = parse_date(end_date, '%m/%d/%Y') 
    731             except ValueError, e: 
    732                 old_e = str(e) 
    733                 del(e) 
    734                 try: 
    735                     # Ugh, papering over wild proliferation of date formats. 
    736                     start_date = parse_date(start_date, '%Y/%m/%d') 
    737                     end_date = parse_date(end_date, '%Y/%m/%d') 
    738                 except ValueError, e: 
    739                     raise BadDateException("%s; %s" % (str(e), old_e)) 
    740             if start_date.year < 1900 or end_date.year < 1900: 
    741                 # This prevents strftime from throwing a ValueError. 
    742                 raise BadDateException('Dates before 1900 are not supported.') 
    743  
    744             self.replace('date', start_date, end_date) 
    745  
    746         lookup_name = pop_key('textsearch') 
    747         search_string = pop_key('q') 
     718        # Dates. 
     719        # For hysterical reasons we support several ways of passing 
     720        # these in.  TODO: consolidate these into ONLY the start_ and 
     721        # end_ variants, no more of the comma-separated by-date stuff. 
     722        # The latter are more compact, but a) more work on the client 
     723        # side, and b) look uglier in URLs due to the url-quoted 
     724        # comma. 
     725        pub_start_and_end = [pop_key('start_pubdate', single=True), 
     726                             pop_key('end_pubdate', single=True)] 
     727        pub_start_and_end = [s for s in pub_start_and_end if s] 
     728        pub_dates = pop_key('by-pubdate') or pub_start_and_end 
     729 
     730        start_and_end = [pop_key('start_date', single=True), 
     731                         pop_key('end_date', single=True)] 
     732        start_and_end = [s for s in start_and_end if s] 
     733        dates = pop_key('by-date') or start_and_end 
     734 
     735        if dates and pub_dates: 
     736            raise DuplicateFilterError("You can only filter by one set of dates.") 
     737        elif dates: 
     738            self['date'] = DateFilter(request, context, qs, *dates, 
     739                                      schema=self.schema) 
     740        elif pub_dates: 
     741            self['date'] = PubDateFilter(request, context, qs, *pub_dates, 
     742                                         schema=self.schema) 
     743 
     744        # Text searches. Apparently we only support one at a time. 
     745        lookup_name = pop_key('textsearch', single=True) 
     746        search_string = pop_key('q', single=True) 
    748747        if lookup_name and search_string: 
    749748            # Can raise DoesNotExist. Should that be FilterError? 
     
    752751            self.replace(schemafield, search_string) 
    753752 
    754         # Stash away all query params we didn't consume. 
     753        # All remaining args. 
     754        for argname in params.keys(): 
     755 
     756            # Street/address 
     757            if argname.startswith('streets'): 
     758                argvalues = pop_key(argname) 
     759                self['location'] = BlockFilter(request, context, qs, *argvalues) 
     760 
     761            # Location filtering 
     762            elif argname.startswith('locations'): 
     763                argvalues = pop_key(argname) 
     764                self['location'] = LocationFilter(request, context, qs, *argvalues) 
     765 
     766            # Attribute filtering 
     767            elif argname.startswith('by-'): 
     768                argvalues = pop_key(argname) 
     769                sf_name = argname[3:] 
     770                try: 
     771                    sf = filter_sf_dict.pop(sf_name) 
     772                except KeyError: 
     773                    raise FilterError('Invalid or duplicate SchemaField name %r' % sf_name) 
     774                self.add_by_schemafield(sf, *argvalues, _replace=True) 
     775            else: 
     776                # Unknown param, ignore it. 
     777                pass 
     778 
     779 
     780        self.sort() 
     781        # Stash any un-consumed query params for URL construction. 
    755782        self.other_query_params = params 
     783        return self 
     784 
    756785 
    757786    def validate(self): 
     
    935964        return self 
    936965 
    937  
    938966    def make_breadcrumbs(self, additions=(), removals=(), stop_at=None,  
    939967                         base_url=None): 
     
    964992        relevant to the FilterChain. 
    965993 
     994        In all URLs, query parameters will be sorted alphabetically by 
     995        name. 
    966996        """ 
    967997        # TODO: Can filter_reverse leverage this? Or vice-versa? 
     
    9771007 
    9781008        base_url = base_url or clone.base_url 
     1009        base_url = posixpath.normpath(base_url) + '/' 
    9791010 
    9801011        crumbs = [] 
    981         filter_params = [] 
    982         other_query_params = urllib.urlencode(sorted(self.other_query_params.items()), 
    983                                               doseq=True) 
     1012        params_so_far = self.other_query_params.copy() 
    9841013        for key, filt in clone._items_with_labels(): 
    9851014            # I'm not sure why we prefer short_value to label, but that's what 
     
    9881017            assert label is not None 
    9891018            label = label.title() 
    990             if label and getattr(filt, 'url', None) is not None: 
    991                 filter_params.append(filt.url) 
    992                 url = '%s%s/' % (base_url, ';'.join(filter_params)) 
    993                 if other_query_params: 
    994                     url = '%s?%s' % (url, other_query_params) 
     1019            if label: 
     1020                params_so_far.update(filt.get_query_params()) 
     1021                # We need doseq=True in case any of other_query_params have multiple values. 
     1022                query_string = urllib.urlencode(sorted(params_so_far.items()), 
     1023                                                doseq=True) 
     1024                url = '%s?%s' % (base_url, query_string) 
    9951025                crumbs.append((label, url)) 
    9961026            if key == stop_at: 
     
    10071037            return crumbs[-1][1] 
    10081038        else: 
    1009             return base_url or self.base_url 
     1039            if self.other_query_params: 
     1040                return '%s?%s' % (base_url or self.base_url, 
     1041                                  urllib.urlencode(self.other_query_params)) 
     1042            else: 
     1043                return base_url or self.base_url 
    10101044 
    10111045    def add_by_place_id(self, pid): 
     
    10491083 
    10501084 
    1051 class BadDateException(Exception): 
     1085class BadDateException(FilterError): 
    10521086    pass 
    10531087 
  • ebpub/ebpub/db/tests/test_models.py

    r03d55a rf9de3a  
    206206    def test_top_lookups__m2m(self): 
    207207        from ebpub.db.models import SchemaField 
    208         sf = SchemaField.objects.get(name='tags many-to-many') 
     208        sf = SchemaField.objects.get(name='tag') 
    209209        # from ebpub.db.bin.update_aggregates import update_aggregates 
    210210        # update_aggregates(sf.schema.id) 
     
    254254        from ebpub.db.models import NewsItem, SchemaField, Lookup 
    255255        by_attribute = NewsItem.objects.by_attribute 
    256         sf = SchemaField.objects.get(name='tags many-to-many') 
     256        sf = SchemaField.objects.get(name='tag') 
    257257        lookups = Lookup.objects.filter(schema_field=sf, code__in=['1', '2']) 
    258258        qs = by_attribute(sf, lookups, is_lookup=True) 
     
    272272        from ebpub.db.models import NewsItem, SchemaField 
    273273        by_attribute = NewsItem.objects.by_attribute 
    274         sf = SchemaField.objects.get(name='tags many-to-many') 
     274        sf = SchemaField.objects.get(name='tag') 
    275275        qs = by_attribute(sf, ['1', '2'], is_lookup=True) 
    276276        self.assertEqual(qs.count(), 3) 
  • ebpub/ebpub/db/tests/test_schemafilters.py

    re0f4b3 rf9de3a  
    3333import random 
    3434 
    35  
    3635class TestNewsitemFilter(TestCase): 
    3736 
     
    104103        return filt 
    105104 
    106     def test_filter__errors(self): 
     105    def test_filter__empty(self): 
    107106        self.assertRaises(FilterError, self._make_filter) 
    108107 
     
    135134        # TODO: have some NewsItems overlapping this location? 
    136135 
     136        self.assertEqual(filt.get_query_params(), 
     137                         {'locations': 'neighborhoods,hood-1'}) 
    137138 
    138139class TestBlockFilter(TestCase): 
     
    202203        self.assertRaises(FilterError, self._make_filter, 'by-date', 'bogus') 
    203204        self.assertRaises(FilterError, self._make_filter, 'by-date', 'bogus', 'bogus') 
    204         self.assertRaises(FilterError, self._make_filter, 'by-date', '2011-04-07') 
    205205 
    206206    def test_filter__ok(self): 
     
    211211        self.assertEqual(self.mock_qs.filter.call_args, ((), filt.kwargs)) 
    212212 
     213    def test__single_arg_date(self): 
     214        filt = self._make_filter('by-date', '2011-04-07') 
     215        filt.apply() 
     216        self.assertEqual(filt.value, u'April 7, 2011') 
     217 
    213218 
    214219    def test_filter__ok__one_day(self): 
     
    220225 
    221226    def test_pub_date_filter(self): 
    222         filt = self._make_filter('by-pub-date', '2006-11-08', '2006-11-09') 
     227        filt = self._make_filter('by-pubdate', '2006-11-08', '2006-11-09') 
    223228        from ebpub.db.schemafilters import PubDateFilter 
    224229        filt2 = PubDateFilter(filt.request, filt.context, filt.qs, 
     
    247252        request = context = None 
    248253        filter = AttributeFilter(request, context, qs, schemafield=schemafield) 
    249         self.assertEqual(filter.url, 'by-mock-sf=') 
     254        self.assertEqual(filter.get_query_params(), 
     255                         {'by-mock-sf': ''}) 
    250256 
    251257 
     
    257263        request = context = None 
    258264        filter = AttributeFilter(request, context, qs, when, schemafield=schemafield) 
    259         self.assertEqual(filter.url, 'by-mock-sf=2009-01-23') 
     265        self.assertEqual(filter.get_query_params(), 
     266                         {'by-mock-sf': '2009-01-23'}) 
    260267 
    261268    def test_init__with_datetime(self): 
     
    266273        request = context = None 
    267274        filter = AttributeFilter(request, context, qs, when, schemafield=schemafield) 
    268         self.assertEqual(filter.url, 'by-mock-sf=2009-01-23T22:40:00') 
     275        self.assertEqual(filter.get_query_params(), 
     276                         {'by-mock-sf': '2009-01-23T22:40:00'}) 
    269277 
    270278    def test_init__with_time(self): 
     
    275283        request = context = None 
    276284        filter = AttributeFilter(request, context, qs, when, schemafield=schemafield) 
    277         self.assertEqual(filter.url, 'by-mock-sf=22:40:00') 
     285        self.assertEqual(filter.get_query_params(),  
     286                         {'by-mock-sf': '22:40:00'}) 
    278287 
    279288 
     
    370379        self.assertEqual(filt.label, 'Beat') 
    371380        self.assertEqual(filt.argname, 'by-beat') 
     381        self.assertEqual(filt.get_query_params(), 
     382                         {'by-beat': 'beat-214,beat-64'}) 
    372383 
    373384 
     
    538549        self.assertRaises(FilterError, chain.add, 'date') 
    539550 
    540     def test_update_from_request__bad(self): 
    541         chain = FilterChain() 
    542         argstring = 'foobar' 
    543         self.assertRaises(FilterError, chain.update_from_request, argstring, {}) 
     551    def test_update_from_request__empty(self): 
     552        request = mock.Mock() 
     553        request.GET = {} 
     554        chain = FilterChain(request=request) 
     555        chain.update_from_request({}) 
     556        self.assertEqual(len(chain), 0) 
    544557 
    545558    def test_add_by_place_id__bad(self): 
     
    588601    def _make_chain(self, url): 
    589602        request = RequestFactory().get(url) 
    590         argstring = request.path.split('filter/', 1)[-1] 
    591603        crime = models.Schema.objects.get(slug='crime') 
    592604        context = {'schema': crime} 
    593605        chain = FilterChain(request=request, context=context, schema=crime) 
    594         chain.update_from_request(argstring=argstring, filter_sf_dict={}) 
     606        chain.update_from_request(filter_sf_dict={}) 
    595607        return chain 
    596608 
     
    692704        url += '?start_date=2010/12/01&end_date=2011/01/01' 
    693705        chain = self._make_chain(url) 
    694         expected = filter_reverse('crime', [('by-date', '2010-12-01', '2011-01-01')]) 
     706        expected = filter_reverse('crime', [('start_date', '2010-12-01'), 
     707                                            ('end_date', '2011-01-01')]) 
    695708        self.assertEqual(chain.make_url(), expected) 
    696709 
     
    708721        self.assertEqual(chain.make_url(), expected) 
    709722 
    710     def test_make_url__both_args_and_query(self): 
    711         url = filter_reverse('crime', [('by-date', '2011-04-05', '2011-04-06')]) 
    712         url += '?textsearch=status&q=bar' 
     723    def test_make_url__preserves_other_query_params_sorted(self): 
     724        url = filter_reverse('crime', [('start_date', '2011-04-05'), 
     725                                       ('end_date', '2011-04-06')]) 
     726        url += '&textsearch=status&q=bar' 
     727        # Add the extra params. 
     728        url += '&zzz=yes&A=no' 
    713729        chain = self._make_chain(url) 
    714         expected = filter_reverse('crime', [('by-date', '2011-04-05', '2011-04-06'), 
     730        expected = filter_reverse('crime', [('start_date', '2011-04-05'), 
     731                                            ('end_date', '2011-04-06'), 
    715732                                            ('by-status', 'bar')]) 
     733        # The extra params should end up sorted alphanumerically. 
     734        expected = expected.replace('?', '?A=no&') + '&zzz=yes' 
    716735        self.assertEqual(chain.make_url(), expected) 
    717736 
    718  
    719     def test_make_url__preserves_other_query_params_sorted(self): 
    720         url = filter_reverse('crime', [('by-date', '2011-04-05', '2011-04-06')]) 
    721         url += '?textsearch=status&q=bar' 
    722         # Add some params that we don't know about. 
    723         url += '&B=no&A=yes' 
    724         chain = self._make_chain(url) 
    725         expected = filter_reverse('crime', [('by-date', '2011-04-05', '2011-04-06'), 
    726                                             ('by-status', 'bar')]) 
    727         expected += '?A=yes&B=no' 
    728         self.assertEqual(chain.make_url(), expected) 
    729  
  • ebpub/ebpub/db/tests/test_views.py

    ree6774 rf9de3a  
    273273        self.assertEqual(response['location'], 'http://testserver/crime/') 
    274274 
    275     @mock.patch('ebpub.db.schemafilters.FilterChain.update_from_query_params') 
     275    def test_filter__bad_params(self): 
     276        url = filter_reverse('crime', [('by-foo', 'bar')]) 
     277        url = url.replace(urllib.quote('='), 'X') 
     278        response = self.client.get(url) 
     279        self.assertEqual(response.status_code, 404) 
     280 
     281    @mock.patch('ebpub.db.schemafilters.FilterChain.update_from_request') 
    276282    def test_filter__bad_date(self, mock_update): 
    277283        from ebpub.db.views import BadDateException 
     
    281287        self.assertEqual(response.status_code, 404) 
    282288 
    283     def test_filter__bad_params(self): 
    284         url = filter_reverse('crime', [('by-foo', 'bar')]) 
    285         url = url.replace(urllib.quote('='), 'X') 
    286         response = self.client.get(url) 
    287         self.assertEqual(response.status_code, 404) 
    288  
    289289    def test_filter_by_daterange(self): 
    290         url = filter_reverse('crime', [('by-date', '2006-11-01', '2006-11-30')]) 
     290        url = filter_reverse('crime', [('start_date', '2006-11-01'), 
     291                                       ('end_date', '2006-11-30')]) 
    291292        response = self.client.get(url) 
    292293        self.assertContains(response, 'Clear') 
     
    296297 
    297298    def test_filter_by_pubdate_daterange(self): 
    298         url = filter_reverse('crime', [('by-pub-date', '2006-11-01', '2006-11-30')]) 
     299        url = filter_reverse('crime', [('start_pubdate', '2006-11-01'), 
     300                                       ('end_pubdate', '2006-11-30')]) 
    299301        response = self.client.get(url) 
    300302        self.assertContains(response, 'Clear') 
     
    306308        url = filter_reverse('crime', 
    307309                             [('by-date', '2006-11-01', '2006-11-30'), 
    308                               ('by-pub-date', '2006-11-01', '2006-11-30')]) 
    309         response = self.client.get(url) 
    310         self.assertEqual(response.status_code, 404) 
    311  
     310                              ('by-pubdate', '2006-11-01', '2006-11-30')]) 
     311        response = self.client.get(url) 
     312        self.assertEqual(response.status_code, 404) 
     313 
     314 
     315    def test_filter__date_missing(self): 
     316        url = filter_reverse('crime', [('start_date', '')]) 
     317        response = self.client.get(url) 
     318        self.assertEqual(response.status_code, 302) 
     319        self.assert_(response['location'].endswith('/crime/filter/')) 
    312320 
    313321    def test_filter__invalid_daterange(self): 
    314         url = filter_reverse('crime', [('by-date', '')]) 
    315         response = self.client.get(url) 
    316         self.assertEqual(response.status_code, 404) 
    317         url = filter_reverse('crime', [('by-date', 'whoops', 'ouchie')]) 
    318         response = self.client.get(url) 
    319         self.assertEqual(response.status_code, 404) 
    320         url = filter_reverse('crime', [('by-date', '2006-11-30', 'ouchie')]) 
     322        url = filter_reverse('crime', [('start_date', 'whoops'), 
     323                                       ('end_date', 'ouchie')]) 
     324        response = self.client.get(url) 
     325        self.assertEqual(response.status_code, 404) 
     326        url = filter_reverse('crime', [('start_date', '2006-11-30'), 
     327                                       ('end_date', 'ouchie')]) 
    321328        response = self.client.get(url) 
    322329        self.assertEqual(response.status_code, 404) 
     
    324331 
    325332    def test_filter_by_day(self): 
    326         url = filter_reverse('crime', [('by-date', '2006-09-26', '2006-09-26')]) 
     333        url = filter_reverse('crime', [('start_date', '2006-09-26'), 
     334                                       ('end_date', '2006-09-26')]) 
    327335        response = self.client.get(url) 
    328336        self.assertContains(response, "crime title 1") 
    329337        self.assertNotContains(response, "crime title 2") 
    330338        self.assertNotContains(response, "crime title 3") 
     339 
     340 
     341    def test_filter__by_date__legacy_redirects(self): 
     342        base = 'http://testserver' + filter_reverse('crime', []) 
     343        expected_to_actual = ( 
     344            (filter_reverse('crime', [('by-date', '2011-09-25', '2012-10-31')]), 
     345             base + '?end_date=2012-10-31&start_date=2011-09-25' 
     346             ), 
     347            (filter_reverse('crime', [('by-pubdate', '2011-09-25', '2012-10-31')]), 
     348             base + '?end_pubdate=2012-10-31&start_pubdate=2011-09-25' 
     349             ), 
     350 
     351            ) 
     352        for expected, actual in expected_to_actual: 
     353            response = self.client.get(expected) 
     354            self.assertEqual(response.status_code, 302) 
     355            self.assertEqual(response['location'], actual) 
     356            response2 = self.client.get(actual) 
     357            self.assertEqual(response2.status_code, 200) 
     358 
    331359 
    332360 
     
    384412        fixed_url = filter_reverse('crime', [ 
    385413                ('streets', 'wabash-ave', '216-299n-s', '8-blocks')]) 
    386         self.assert_(response['location'].endswith(urllib.unquote(fixed_url))) 
     414        self.assertEqual(response['location'], 'http://testserver' + fixed_url) 
    387415 
    388416    @mock.patch('ebpub.streets.models.proper_city') 
     
    475503 
    476504    def test_filter__invalid_argname(self): 
     505        # These are ignored. 
    477506        url = filter_reverse('crime', [('bogus-key', 'bogus-value')]) 
    478507        response = self.client.get(url) 
    479         self.assertEqual(response.status_code, 404) 
    480  
     508        self.assertEqual(response.status_code, 200) 
    481509 
    482510    @mock.patch('ebpub.db.schemafilters.logger') 
     
    500528    def test_filter__pagination__invalid_page(self): 
    501529        url = filter_reverse('crime', [('by-status', 'status 9-19')]) 
    502         url += '?page=oops' 
     530        url += '&page=oops' 
    503531        response = self.client.get(url) 
    504532        self.assertEqual(response.status_code, 404) 
     
    506534    def test_filter__pagination__empty_page(self): 
    507535        url = filter_reverse('crime', [('by-status', 'status 9-19')]) 
    508         url += '?page=99' 
     536        url += '&page=99' 
    509537        response = self.client.get(url) 
    510538        self.assertEqual(response.status_code, 404) 
     
    513541    def test_filter__pagination__has_more(self, mock_chain): 
    514542        url = filter_reverse('crime', [('by-status', 'status 9-19')]) 
    515         url += '?page=2' 
     543        url += '&page=2' 
    516544        # We can mock the FilterChain to get a very long list of NewsItems 
    517545        # without actually creating them in the db, but it means 
  • ebpub/ebpub/db/urlresolvers.py

    r44188d rf9de3a  
    1919from django.core import urlresolvers 
    2020import posixpath 
     21import urllib 
    2122 
    2223def filter_reverse(slug, args): 
     
    2930                name = a[0] 
    3031                values = ','.join(a[1:]) 
    31                 args[i] = '%s=%s' % (name, values) 
     32                args[i] = (name, values) 
    3233            else: 
     34                # No values. 
    3335                # This is allowed eg. for showing a list of available 
    3436                # Blocks, or Lookup values, etc. 
    35                 args[i] = a[0] 
     37                args[i] = (a[0], '') 
    3638        else: 
    37             assert isinstance(a, basestring) 
    38     #argstring = urllib.quote(';'.join(args)) #['%s=%s' % (k, v) for (k, v) in args]) 
    39     argstring = ';'.join(args) #['%s=%s' % (k, v) for (k, v) in args]) 
     39            raise TypeError("Need a list or tuple, got: %s" % a) 
    4040 
    41     if not argstring.lstrip('/').startswith('filter'): 
    42         argstring = 'filter/%s' % argstring 
    4341    url = urlresolvers.reverse('ebpub-schema-filter', args=[slug]) 
    44     url = '%s/%s/' % (url, argstring) 
     42    # TODO: does this really need hardcodign here? 
     43    if not url.rstrip('/').endswith('filter'): 
     44        url = '%s/filter/' % url 
    4545    # Normalize duplicate slashes, dots, and the like. 
    4646    url = posixpath.normpath(url) + '/' 
     47    if args: 
     48        # Normalize a bit. 
     49        args = sorted(args) 
     50        querystring = urllib.urlencode(args) 
     51        url = '%s?%s' % (url, querystring) 
    4752    return url 
    4853 
  • ebpub/ebpub/db/views.py

    r704a4d rf9de3a  
    241241        # As an optimization, limit the NewsItems to those published in the 
    242242        # last few days. 
    243         filters.update_from_query_params(request) 
     243        filter_sf_dict = _get_filter_schemafields(schema) 
     244        filters.update_from_request(filter_sf_dict) 
    244245        if not filters.has_key('date'): 
    245246            end_date = today() 
     
    630631 
    631632 
     633def _get_filter_schemafields(schema): 
     634    """Given a Schema, get a sorted mapping of schemafield names to 
     635    SchemaField instances. 
     636 
     637    Only SchemaFields that have is_searchable or is_filter enabled 
     638    will be returned. 
     639    """ 
     640    filter_sf_list = list(SchemaField.objects.filter(schema=schema, is_filter=True).order_by('display_order')) 
     641    textsearch_sf_list = list(SchemaField.objects.filter(schema=schema, is_searchable=True).order_by('display_order')) 
     642    # Use SortedDict to preserve the display_order. 
     643    filter_sf_dict = SortedDict([(sf.name, sf) for sf in filter_sf_list] + [(sf.name, sf) for sf in textsearch_sf_list]) 
     644    return filter_sf_dict 
     645 
     646 
    632647def schema_filter_geojson(request, slug, args_from_url): 
    633648    s = get_object_or_404(get_schema_manager(request), slug=slug, is_special_report=False) 
     
    635650        return HttpResponse(status=404) 
    636651 
    637     filter_sf_list = list(SchemaField.objects.filter(schema__id=s.id, is_filter=True).order_by('display_order')) 
    638     textsearch_sf_list = list(SchemaField.objects.filter(schema__id=s.id, is_searchable=True).order_by('display_order')) 
    639  
    640     # Use SortedDict to preserve the display_order. 
    641     filter_sf_dict = SortedDict([(sf.name, sf) for sf in filter_sf_list] + [(sf.name, sf) for sf in textsearch_sf_list]) 
    642  
    643652    # Determine what filters to apply, based on path and/or query string. 
    644653    filterchain = FilterChain(request=request, schema=s) 
     654    filter_sf_dict = _get_filter_schemafields(s) 
    645655    try: 
    646         filterchain.update_from_request(args_from_url, filter_sf_dict) 
     656        filterchain.update_from_request(filter_sf_dict) 
    647657        filters_need_more = filterchain.validate() 
    648658    except FilterError: 
     
    710720    context['breadcrumbs'] = breadcrumbs.schema_filter(context) 
    711721 
    712     filter_sf_list = list(SchemaField.objects.filter(schema__id=s.id, is_filter=True).order_by('display_order')) 
    713     textsearch_sf_list = list(SchemaField.objects.filter(schema__id=s.id, is_searchable=True).order_by('display_order')) 
    714  
    715     # Use SortedDict to preserve the display_order. 
    716     filter_sf_dict = SortedDict([(sf.name, sf) for sf in filter_sf_list] + [(sf.name, sf) for sf in textsearch_sf_list]) 
     722    filter_sf_dict = _get_filter_schemafields(s) 
    717723 
    718724    # Determine what filters to apply, based on path and/or query string. 
     
    720726    context['filters'] = filterchain 
    721727    try: 
    722         filterchain.update_from_request(args_from_url, filter_sf_dict) 
     728        filterchain.update_from_request(filter_sf_dict) 
    723729        filters_need_more = filterchain.validate() 
    724730    except FilterError, e: 
Note: See TracChangeset for help on using the changeset viewer.