Hours of Operation
Dec 16, 2008

As you probably know, I've been working on a Django-based re-build of BostonChefs.com (the new version of which is actually live now, but due to DNS propagation issues isn't yet available to 100% of people which is why I haven't yet written a post about it). Among other things, BostonChefs.com provides information on some of the fantastic restaurants in the Boston area. One piece of information it provides is the hours of operation of those restaurants. In order to store this information I created a model called HoursOfOperation. It looks like this: class HoursOfOperation(models.Model): DAY_CHOICES = ( ('0', 'Sun'), ('1', 'Mon'), ('2', 'Tue'), ('3', 'Wed'), ('4', 'Thur'), ('5', 'Fri'), ('6', 'Sat'), ) restaurant = models.ForeignKey("Restaurant") meal_period = models.ForeignKey("MealPeriod") day = models.CharField(max_length=3, choices=DAY_CHOICES) open_time = models.TimeField(default=datetime.datetime.now) close_time = models.TimeField(default=datetime.datetime.now) def _get_hours(self): return "%s - %s" % (self.open_time.strftime('%I:%M%p'), self.close_time.strftime('%I:%M %p')) hours = property(_get_hours) As you can see, each 'hour' is related to a restaurant and a meal period, which allows us to display the information in a manner similar to that you might find on a store's front sign. For example, if you go to the Grill 23 & Bar page (my personal favorite restaurant in Boston, although Craigie on Main is a decent challenger), you'll see something like this: DINNER * Sun: 5:30 p.m.-10 p.m. * Mon-Thur: 5:30 p.m.-10:30 p.m. * Fri: 5:30 p.m.-11 p.m. * Sat: 5 p.m.-11 p.m. Building a list like that out of the above model proved slightly more difficult that I might have hoped. It required quite a lot of template logic, including writing a custom filter. The block of template code necessary to generate that list looks like this:
{% regroup restaurant.hoursofoperation_set.all by meal_period as periods %} {% for period in periods %}
{{ period.grouper }}
{% regroup period.list by hours as hour_list %}
    {% for hour in hour_list %}
  • {{ hour.list|collapsedays }}
  • {% endfor %}
{% endfor %}
As you can see, somewhat complex. Those nested {% regroup %}s can be nasty to wrap your head around, if nothing else. But basically it's taking the set of HoursOfOperation objects related to the restaurant, grouping them by meal period, then taking the subset of those objects for each meal period, and grouping those by the hours of the day they represent. So what you're then left with is a list of all the different time periods (still represented as HoursOfOperation objects) that the restaurant is open for a given meal period, and the days on which it is open during those hours. As you can see above, the days are represented by number of the day of the week (0 for Sunday through 6 for Saturday). Converting that list integers into something like 'Mon, Wed-Fri' was not very easy, and certainly not something I wanted to try to tackle using Django's template tags. I ended up drawing heavily on my hazy memories of CS 127 (many thanks to Dave who taught me all about recursion way back then) and creating a filter that considers the list of HoursOfOperation objects as a list of those integers, then recursively converts it into a list of lists representing the subsets of contiguous days in the list. So if you start out with [1, 3, 4, 5] you end up with [[1, 1], [3, 5]] which is then converted into 'Mon, Wed-Fri'. After several false starts I ended up with this beauty of a Django template filter: from django.template import Library from django.template.defaultfilters import time from types import ListType register = Library() def simplify(index, found, days): high = index+1 mid = index low = index-1 if not found: days[low] = [days[low], days[low]] if high >= len(days): if not isinstance(days[-1], ListType): if days[-1] == days[-2][1]: days.pop(-1) else: days[-1] = [days[-1], days[-1]] return days if int(days[high].day) - int(days[mid].day) == 1 and (found or int(days[mid].day) - int(days[low][0].day) == 1): days[low][1] = days[high] days.pop(mid) high = high-1 found = True else: if found: days.pop(mid) found = False return simplify(high, found, days) @register.filter def collapsedays(value): hours = "%s-%s" % (time(value[0].open_time), time(value[0].close_time)) days = simplify(1, False, value) for i in range(len(days)): if days[i][0] == days[i][1]: days[i] = days[i][0].get_day_display() else: days[i] = "%s-%s" % (days[i][0].get_day_display(), days[i][1].get_day_display()) return "%s: %s" % (', '.join(days), hours)
blog comments powered by Disqus