Commits

Luke Plant committed c24595d

Improved date drill down by making it show context for all values e.g. month for day, year for month

This involved changing 'only choice' to 'display', and reworking filterset
rendering logic

  • Participants
  • Parent commits 0f8a9f5

Comments (0)

Files changed (4)

File django_easyfilters/filters.py

 
 FILTER_ADD = 'add'
 FILTER_REMOVE = 'remove'
-FILTER_ONLY_CHOICE = 'only'
+FILTER_DISPLAY = 'display'
 
 FilterChoice = namedtuple('FilterChoice', 'label count params link_type')
 
     A mixin for filters where the field conceptually has just one value.
     """
     def normalize_add_choices(self, choices):
-        if len(choices) == 1 and not self.field_obj.null:
+        addchoices = [(i, choice) for i, choice in enumerate(choices)
+                      if choice.link_type == FILTER_ADD]
+        if len(addchoices) == 1 and not self.field_obj.null:
             # No point giving people a choice of one, since all the results will
             # already have the selected value (apart from nullable fields, which
             # might have null)
-            choices = [FilterChoice(label=choices[0].label,
-                                    count=choices[0].count,
-                                    link_type=FILTER_ONLY_CHOICE,
-                                    params=None)]
+            for i, c in addchoices:
+                choices[i] = FilterChoice(label=choices[i].label,
+                                          count=choices[i].count,
+                                          link_type=FILTER_DISPLAY,
+                                          params=None)
         return choices
 
 
     Filter for ForeignKey fields.
     """
     def display_choice(self, choice):
+
         lookup = {self.rel_field.name: choice}
         try:
             obj = self.rel_model.objects.get(**lookup)
                 for choice in chosen if choice in obj_dict]
 
 
-DateRangeTypeBase = namedtuple('DateRangeTypeBase', 'level single label regex')
-class DateRangeType(DateRangeTypeBase):
+class DateRangeType(object):
 
     all = {} # Keep a cache, so that we have unique instances
 
-    def __init__(self, *args):
-        super(DateRangeType, self).__init__(*args)
-        DateRangeType.all[(self.level, self.single)] = self
+    def __init__(self, level, single, label, regex):
+        self.level, self.single, self.label, self.regex = level, single, label, regex
+        DateRangeType.all[(level, single)] = self
+
+    def __repr__(self):
+        return '<DateRange %d %s %s>' % (self.level,
+                                         "single" if self.single else "multi",
+                                         self.label)
+
+    def __cmp__(self, other):
+        if other is None:
+            return 1
+        else:
+            return cmp((self.level, self.single),
+                       (other.level, other.single))
+
+    @classmethod
+    def get(cls, level, single):
+        return cls.all[(level, single)]
 
     @property
     def dateattr(self):
         """
         Return the same but with 'single=True'
         """
-        return DateRangeType.all[(self.level, True)]
+        return DateRangeType.get(self.level, True)
 
     def to_multi(self):
         """
         Return the same but with 'single=False'
         """
-        return DateRangeType.all[(self.level, False)]
+        return DateRangeType.get(self.level, False)
 
     def drilldown(self):
         if self is DAY:
         else:
             # We always drill down to 'single', and then generate
             # ranges (i.e. multi) if appropriate.
-            return DateRangeType.all[(self.level + 1, True)]
+            return DateRangeType.get(self.level + 1, True)
 
 YEARGROUP   = DateRangeType(1, False, 'year',  re.compile(r'^(\d{4})..(\d{4})$'))
 YEAR        = DateRangeType(1, True,  'year',  re.compile(r'^(\d{4})$'))
             value = self.values[0]
             parts = value.split('-')
             if self.range_type == YEAR:
-                return value
+                return parts[0]
             elif self.range_type == MONTH:
                 from django.utils.dates import MONTHS
                 return unicode(MONTHS[int(parts[1])])
         date_choice_counts = self.collapse_results(results, range_type)
 
         choices = []
+        # Additional display links, to give context for choices if necessary.
+        if len(date_choice_counts) > 0:
+            choices.extend(self.bridge_choices(chosen, date_choice_counts))
+
         for date_choice, count in date_choice_counts:
             if date_choice in chosen:
                 continue
             date_choice_counts = [(DateChoice.from_datetime(range_type, dt), count)
                                   for dt, count in results]
         return date_choice_counts
+
+    def bridge_choices(self, chosen, choices):
+        if len(choices) == 0:
+            return []
+        if len(chosen) == 0:
+            chosen_level = 0
+        else:
+            chosen_level = chosen[-1].range_type.level - 1
+
+        # first choice in list can act as template, as it will have all the
+        # values we need.
+        new_choice = choices[0][0]
+        new_level = new_choice.range_type.level
+
+        retval = []
+        while chosen_level < new_level - 1:
+            chosen_level += 1
+            date_choice = DateChoice(DateRangeType.get(chosen_level, True),
+                                     new_choice.values)
+            retval.append(FilterChoice(date_choice.display(),
+                                       None,
+                                       None,
+                                       FILTER_DISPLAY))
+        return retval
+

File django_easyfilters/filterset.py

 from django.utils.html import escape
 from django.utils.text import capfirst
 
-from django_easyfilters.filters import FILTER_ADD, FILTER_REMOVE, FILTER_ONLY_CHOICE, \
+from django_easyfilters.filters import FILTER_ADD, FILTER_REMOVE, FILTER_DISPLAY, \
     ValuesFilter, ChoicesFilter, ForeignKeyFilter, ManyToManyFilter, DateTimeFilter
 
 
 
     template = """
 <div class="filterline"><span class="filterlabel">{{ filterlabel }}:</span>
-{% for choice in remove_choices %}
-  <span class="removefilter"><a href="{{ choice.url }}" title="Remove filter">{{ choice.label }}&nbsp;&laquo;&nbsp;</a></span>
-{% endfor %}
-{% for choice in add_choices %}
-  <span class="addfilter"><a href="{{ choice.url }}" class="addfilter" title="Add filter">{{ choice.label }}</a>&nbsp;({{ choice.count }})</span>&nbsp;&nbsp;
-{% endfor %}
-{% for choice in only_choices %}
-  <span class="onlychoice">{{ choice.label }}</span>
+{% for choice in choices %}
+  {% if choice.link_type == 'add' %}
+    <span class="addfilter"><a href="{{ choice.url }}" title="Add filter">{{ choice.label }}&nbsp;({{ choice.count }})</a></span>&nbsp;&nbsp;
+  {% else %}
+    {% if choice.link_type == 'remove' %}
+    <span class="removefilter"><a href="{{ choice.url }}" title="Remove filter">{{ choice.label }}&nbsp;&laquo;&nbsp;</a></span>
+    {% else %}
+      <span class="displayfilter">{{ choice.label }}</span>
+    {% endif %}
+  {% endif %}
 {% endfor %}
 </div>
 """
         field_obj = self.model._meta.get_field(filter_.field)
         choices = filter_.get_choices(qs)
         ctx = {'filterlabel': capfirst(field_obj.verbose_name)}
-        ctx['remove_choices'] = [dict(label=non_breaking_spaces(c.label),
-                                      url=u'?' + c.params.urlencode())
-                                 for c in choices if c.link_type == FILTER_REMOVE]
-        ctx['add_choices'] = [dict(label=non_breaking_spaces(c.label),
-                                   url=u'?' + c.params.urlencode(),
-                                   count=c.count)
-                              for c in choices if c.link_type == FILTER_ADD]
-        ctx['only_choices'] = [dict(label=non_breaking_spaces(c.label),
-                                    count=c.count)
-                               for c in choices if c.link_type == FILTER_ONLY_CHOICE]
-
+        ctx['choices'] = [dict(label=non_breaking_spaces(c.label),
+                               url=u'?' + c.params.urlencode() \
+                                   if c.link_type != FILTER_DISPLAY else None,
+                               link_type=c.link_type,
+                               count=c.count)
+                          for c in choices]
         return self.get_template().render(template.Context(ctx))
 
     def get_template(self):

File django_easyfilters/tests/filterset.py

 
 from django_easyfilters.filterset import FilterSet
 from django_easyfilters.filters import \
-    FILTER_ADD, FILTER_REMOVE, FILTER_ONLY_CHOICE, \
+    FILTER_ADD, FILTER_REMOVE, FILTER_DISPLAY, \
     ForeignKeyFilter, ValuesFilter, ChoicesFilter, ManyToManyFilter, DateTimeFilter
 
 from models import Book, Genre, Author, BINDING_CHOICES
         qs = Book.objects.filter(date_published__year=1975)
         self.assertEqual(len(qs), 1)
 
-        filter1 = DateTimeFilter('date_published', Book, MultiValueDict())
+        filter1 = ChoicesFilter('binding', Book, MultiValueDict())
         choices1 = filter1.get_choices(qs)
         self.assertEqual(len(choices1), 1)
-        self.assertEqual(choices1[0].link_type, FILTER_ONLY_CHOICE)
+        self.assertEqual(choices1[0].link_type, FILTER_DISPLAY)
 
-        filter2 = ChoicesFilter('binding', Book, MultiValueDict())
-        choices2 = filter1.get_choices(qs)
+        filter2 = ForeignKeyFilter('genre', Book, MultiValueDict())
+        choices2 = filter2.get_choices(qs)
         self.assertEqual(len(choices2), 1)
-        self.assertEqual(choices2[0].link_type, FILTER_ONLY_CHOICE)
-
-        filter3 = ForeignKeyFilter('genre', Book, MultiValueDict())
-        choices3 = filter3.get_choices(qs)
-        self.assertEqual(len(choices3), 1)
-        self.assertEqual(choices3[0].link_type, FILTER_ONLY_CHOICE)
-
+        self.assertEqual(choices2[0].link_type, FILTER_DISPLAY)
 
     def test_manytomany_filter(self):
         """
 
         self.assertTrue("16" in [c.label for c in add_choices])
 
+    def test_datetime_filter_start_at_year(self):
+        # Tests that the first filter shown is a year, not a day,
+        # even if initial query gets you down to a day.
+        params = MultiValueDict()
+        qs = Book.objects.filter(id=1)
+        f = DateTimeFilter('date_published', Book, params, max_links=10)
+
+        choices = f.get_choices(qs)
+        self.assertEqual(len(choices), 3)
+
+        self.assertEqual(choices[0].link_type, FILTER_DISPLAY)
+        self.assertEqual(choices[0].label, str(qs[0].date_published.year))
+
+        self.assertEqual(choices[1].link_type, FILTER_DISPLAY)
+
+        self.assertEqual(choices[2].link_type, FILTER_DISPLAY)
+        self.assertEqual(choices[2].label, str(qs[0].date_published.day))
+
+
     def test_datetime_filter_invalid_query(self):
         self.do_invalid_query_param_test(lambda params: DateTimeFilter('date_published', Book, params, max_links=10),
                                          MultiValueDict({'date_published':['1818xx']}))

File django_easyfilters/tests/templates/books.html

     color: white;
 }
 
+.displayfilter {
+    border-radius: 4px;
+    padding: 0px 3px 2px 3px;
+    border: 1px solid #113399;
+}
+
   </style>
   <body>