OLD | NEW |
(Empty) | |
| 1 from __future__ import unicode_literals |
| 2 |
| 3 import datetime |
| 4 |
| 5 from django.conf import settings |
| 6 from django.core.exceptions import ImproperlyConfigured |
| 7 from django.db import models |
| 8 from django.http import Http404 |
| 9 from django.utils import timezone |
| 10 from django.utils.encoding import force_str, force_text |
| 11 from django.utils.functional import cached_property |
| 12 from django.utils.translation import ugettext as _ |
| 13 from django.views.generic.base import View |
| 14 from django.views.generic.detail import ( |
| 15 BaseDetailView, SingleObjectTemplateResponseMixin, |
| 16 ) |
| 17 from django.views.generic.list import ( |
| 18 MultipleObjectMixin, MultipleObjectTemplateResponseMixin, |
| 19 ) |
| 20 |
| 21 |
| 22 class YearMixin(object): |
| 23 """ |
| 24 Mixin for views manipulating year-based data. |
| 25 """ |
| 26 year_format = '%Y' |
| 27 year = None |
| 28 |
| 29 def get_year_format(self): |
| 30 """ |
| 31 Get a year format string in strptime syntax to be used to parse the |
| 32 year from url variables. |
| 33 """ |
| 34 return self.year_format |
| 35 |
| 36 def get_year(self): |
| 37 """ |
| 38 Return the year for which this view should display data. |
| 39 """ |
| 40 year = self.year |
| 41 if year is None: |
| 42 try: |
| 43 year = self.kwargs['year'] |
| 44 except KeyError: |
| 45 try: |
| 46 year = self.request.GET['year'] |
| 47 except KeyError: |
| 48 raise Http404(_("No year specified")) |
| 49 return year |
| 50 |
| 51 def get_next_year(self, date): |
| 52 """ |
| 53 Get the next valid year. |
| 54 """ |
| 55 return _get_next_prev(self, date, is_previous=False, period='year') |
| 56 |
| 57 def get_previous_year(self, date): |
| 58 """ |
| 59 Get the previous valid year. |
| 60 """ |
| 61 return _get_next_prev(self, date, is_previous=True, period='year') |
| 62 |
| 63 def _get_next_year(self, date): |
| 64 """ |
| 65 Return the start date of the next interval. |
| 66 |
| 67 The interval is defined by start date <= item date < next start date. |
| 68 """ |
| 69 try: |
| 70 return date.replace(year=date.year + 1, month=1, day=1) |
| 71 except ValueError: |
| 72 raise Http404(_("Date out of range")) |
| 73 |
| 74 def _get_current_year(self, date): |
| 75 """ |
| 76 Return the start date of the current interval. |
| 77 """ |
| 78 return date.replace(month=1, day=1) |
| 79 |
| 80 |
| 81 class MonthMixin(object): |
| 82 """ |
| 83 Mixin for views manipulating month-based data. |
| 84 """ |
| 85 month_format = '%b' |
| 86 month = None |
| 87 |
| 88 def get_month_format(self): |
| 89 """ |
| 90 Get a month format string in strptime syntax to be used to parse the |
| 91 month from url variables. |
| 92 """ |
| 93 return self.month_format |
| 94 |
| 95 def get_month(self): |
| 96 """ |
| 97 Return the month for which this view should display data. |
| 98 """ |
| 99 month = self.month |
| 100 if month is None: |
| 101 try: |
| 102 month = self.kwargs['month'] |
| 103 except KeyError: |
| 104 try: |
| 105 month = self.request.GET['month'] |
| 106 except KeyError: |
| 107 raise Http404(_("No month specified")) |
| 108 return month |
| 109 |
| 110 def get_next_month(self, date): |
| 111 """ |
| 112 Get the next valid month. |
| 113 """ |
| 114 return _get_next_prev(self, date, is_previous=False, period='month') |
| 115 |
| 116 def get_previous_month(self, date): |
| 117 """ |
| 118 Get the previous valid month. |
| 119 """ |
| 120 return _get_next_prev(self, date, is_previous=True, period='month') |
| 121 |
| 122 def _get_next_month(self, date): |
| 123 """ |
| 124 Return the start date of the next interval. |
| 125 |
| 126 The interval is defined by start date <= item date < next start date. |
| 127 """ |
| 128 if date.month == 12: |
| 129 try: |
| 130 return date.replace(year=date.year + 1, month=1, day=1) |
| 131 except ValueError: |
| 132 raise Http404(_("Date out of range")) |
| 133 else: |
| 134 return date.replace(month=date.month + 1, day=1) |
| 135 |
| 136 def _get_current_month(self, date): |
| 137 """ |
| 138 Return the start date of the previous interval. |
| 139 """ |
| 140 return date.replace(day=1) |
| 141 |
| 142 |
| 143 class DayMixin(object): |
| 144 """ |
| 145 Mixin for views manipulating day-based data. |
| 146 """ |
| 147 day_format = '%d' |
| 148 day = None |
| 149 |
| 150 def get_day_format(self): |
| 151 """ |
| 152 Get a day format string in strptime syntax to be used to parse the day |
| 153 from url variables. |
| 154 """ |
| 155 return self.day_format |
| 156 |
| 157 def get_day(self): |
| 158 """ |
| 159 Return the day for which this view should display data. |
| 160 """ |
| 161 day = self.day |
| 162 if day is None: |
| 163 try: |
| 164 day = self.kwargs['day'] |
| 165 except KeyError: |
| 166 try: |
| 167 day = self.request.GET['day'] |
| 168 except KeyError: |
| 169 raise Http404(_("No day specified")) |
| 170 return day |
| 171 |
| 172 def get_next_day(self, date): |
| 173 """ |
| 174 Get the next valid day. |
| 175 """ |
| 176 return _get_next_prev(self, date, is_previous=False, period='day') |
| 177 |
| 178 def get_previous_day(self, date): |
| 179 """ |
| 180 Get the previous valid day. |
| 181 """ |
| 182 return _get_next_prev(self, date, is_previous=True, period='day') |
| 183 |
| 184 def _get_next_day(self, date): |
| 185 """ |
| 186 Return the start date of the next interval. |
| 187 |
| 188 The interval is defined by start date <= item date < next start date. |
| 189 """ |
| 190 return date + datetime.timedelta(days=1) |
| 191 |
| 192 def _get_current_day(self, date): |
| 193 """ |
| 194 Return the start date of the current interval. |
| 195 """ |
| 196 return date |
| 197 |
| 198 |
| 199 class WeekMixin(object): |
| 200 """ |
| 201 Mixin for views manipulating week-based data. |
| 202 """ |
| 203 week_format = '%U' |
| 204 week = None |
| 205 |
| 206 def get_week_format(self): |
| 207 """ |
| 208 Get a week format string in strptime syntax to be used to parse the |
| 209 week from url variables. |
| 210 """ |
| 211 return self.week_format |
| 212 |
| 213 def get_week(self): |
| 214 """ |
| 215 Return the week for which this view should display data |
| 216 """ |
| 217 week = self.week |
| 218 if week is None: |
| 219 try: |
| 220 week = self.kwargs['week'] |
| 221 except KeyError: |
| 222 try: |
| 223 week = self.request.GET['week'] |
| 224 except KeyError: |
| 225 raise Http404(_("No week specified")) |
| 226 return week |
| 227 |
| 228 def get_next_week(self, date): |
| 229 """ |
| 230 Get the next valid week. |
| 231 """ |
| 232 return _get_next_prev(self, date, is_previous=False, period='week') |
| 233 |
| 234 def get_previous_week(self, date): |
| 235 """ |
| 236 Get the previous valid week. |
| 237 """ |
| 238 return _get_next_prev(self, date, is_previous=True, period='week') |
| 239 |
| 240 def _get_next_week(self, date): |
| 241 """ |
| 242 Return the start date of the next interval. |
| 243 |
| 244 The interval is defined by start date <= item date < next start date. |
| 245 """ |
| 246 try: |
| 247 return date + datetime.timedelta(days=7 - self._get_weekday(date)) |
| 248 except OverflowError: |
| 249 raise Http404(_("Date out of range")) |
| 250 |
| 251 def _get_current_week(self, date): |
| 252 """ |
| 253 Return the start date of the current interval. |
| 254 """ |
| 255 return date - datetime.timedelta(self._get_weekday(date)) |
| 256 |
| 257 def _get_weekday(self, date): |
| 258 """ |
| 259 Return the weekday for a given date. |
| 260 |
| 261 The first day according to the week format is 0 and the last day is 6. |
| 262 """ |
| 263 week_format = self.get_week_format() |
| 264 if week_format == '%W': # week starts on Monday |
| 265 return date.weekday() |
| 266 elif week_format == '%U': # week starts on Sunday |
| 267 return (date.weekday() + 1) % 7 |
| 268 else: |
| 269 raise ValueError("unknown week format: %s" % week_format) |
| 270 |
| 271 |
| 272 class DateMixin(object): |
| 273 """ |
| 274 Mixin class for views manipulating date-based data. |
| 275 """ |
| 276 date_field = None |
| 277 allow_future = False |
| 278 |
| 279 def get_date_field(self): |
| 280 """ |
| 281 Get the name of the date field to be used to filter by. |
| 282 """ |
| 283 if self.date_field is None: |
| 284 raise ImproperlyConfigured("%s.date_field is required." % self.__cla
ss__.__name__) |
| 285 return self.date_field |
| 286 |
| 287 def get_allow_future(self): |
| 288 """ |
| 289 Returns `True` if the view should be allowed to display objects from |
| 290 the future. |
| 291 """ |
| 292 return self.allow_future |
| 293 |
| 294 # Note: the following three methods only work in subclasses that also |
| 295 # inherit SingleObjectMixin or MultipleObjectMixin. |
| 296 |
| 297 @cached_property |
| 298 def uses_datetime_field(self): |
| 299 """ |
| 300 Return `True` if the date field is a `DateTimeField` and `False` |
| 301 if it's a `DateField`. |
| 302 """ |
| 303 model = self.get_queryset().model if self.model is None else self.model |
| 304 field = model._meta.get_field(self.get_date_field()) |
| 305 return isinstance(field, models.DateTimeField) |
| 306 |
| 307 def _make_date_lookup_arg(self, value): |
| 308 """ |
| 309 Convert a date into a datetime when the date field is a DateTimeField. |
| 310 |
| 311 When time zone support is enabled, `date` is assumed to be in the |
| 312 current time zone, so that displayed items are consistent with the URL. |
| 313 """ |
| 314 if self.uses_datetime_field: |
| 315 value = datetime.datetime.combine(value, datetime.time.min) |
| 316 if settings.USE_TZ: |
| 317 value = timezone.make_aware(value, timezone.get_current_timezone
()) |
| 318 return value |
| 319 |
| 320 def _make_single_date_lookup(self, date): |
| 321 """ |
| 322 Get the lookup kwargs for filtering on a single date. |
| 323 |
| 324 If the date field is a DateTimeField, we can't just filter on |
| 325 date_field=date because that doesn't take the time into account. |
| 326 """ |
| 327 date_field = self.get_date_field() |
| 328 if self.uses_datetime_field: |
| 329 since = self._make_date_lookup_arg(date) |
| 330 until = self._make_date_lookup_arg(date + datetime.timedelta(days=1)
) |
| 331 return { |
| 332 '%s__gte' % date_field: since, |
| 333 '%s__lt' % date_field: until, |
| 334 } |
| 335 else: |
| 336 # Skip self._make_date_lookup_arg, it's a no-op in this branch. |
| 337 return {date_field: date} |
| 338 |
| 339 |
| 340 class BaseDateListView(MultipleObjectMixin, DateMixin, View): |
| 341 """ |
| 342 Abstract base class for date-based views displaying a list of objects. |
| 343 """ |
| 344 allow_empty = False |
| 345 date_list_period = 'year' |
| 346 |
| 347 def get(self, request, *args, **kwargs): |
| 348 self.date_list, self.object_list, extra_context = self.get_dated_items() |
| 349 context = self.get_context_data(object_list=self.object_list, |
| 350 date_list=self.date_list) |
| 351 context.update(extra_context) |
| 352 return self.render_to_response(context) |
| 353 |
| 354 def get_dated_items(self): |
| 355 """ |
| 356 Obtain the list of dates and items. |
| 357 """ |
| 358 raise NotImplementedError('A DateView must provide an implementation of
get_dated_items()') |
| 359 |
| 360 def get_ordering(self): |
| 361 """ |
| 362 Returns the field or fields to use for ordering the queryset; uses the |
| 363 date field by default. |
| 364 """ |
| 365 return '-%s' % self.get_date_field() if self.ordering is None else self.
ordering |
| 366 |
| 367 def get_dated_queryset(self, **lookup): |
| 368 """ |
| 369 Get a queryset properly filtered according to `allow_future` and any |
| 370 extra lookup kwargs. |
| 371 """ |
| 372 qs = self.get_queryset().filter(**lookup) |
| 373 date_field = self.get_date_field() |
| 374 allow_future = self.get_allow_future() |
| 375 allow_empty = self.get_allow_empty() |
| 376 paginate_by = self.get_paginate_by(qs) |
| 377 |
| 378 if not allow_future: |
| 379 now = timezone.now() if self.uses_datetime_field else timezone_today
() |
| 380 qs = qs.filter(**{'%s__lte' % date_field: now}) |
| 381 |
| 382 if not allow_empty: |
| 383 # When pagination is enabled, it's better to do a cheap query |
| 384 # than to load the unpaginated queryset in memory. |
| 385 is_empty = len(qs) == 0 if paginate_by is None else not qs.exists() |
| 386 if is_empty: |
| 387 raise Http404(_("No %(verbose_name_plural)s available") % { |
| 388 'verbose_name_plural': force_text(qs.model._meta.verbose_nam
e_plural) |
| 389 }) |
| 390 |
| 391 return qs |
| 392 |
| 393 def get_date_list_period(self): |
| 394 """ |
| 395 Get the aggregation period for the list of dates: 'year', 'month', or 'd
ay'. |
| 396 """ |
| 397 return self.date_list_period |
| 398 |
| 399 def get_date_list(self, queryset, date_type=None, ordering='ASC'): |
| 400 """ |
| 401 Get a date list by calling `queryset.dates/datetimes()`, checking |
| 402 along the way for empty lists that aren't allowed. |
| 403 """ |
| 404 date_field = self.get_date_field() |
| 405 allow_empty = self.get_allow_empty() |
| 406 if date_type is None: |
| 407 date_type = self.get_date_list_period() |
| 408 |
| 409 if self.uses_datetime_field: |
| 410 date_list = queryset.datetimes(date_field, date_type, ordering) |
| 411 else: |
| 412 date_list = queryset.dates(date_field, date_type, ordering) |
| 413 if date_list is not None and not date_list and not allow_empty: |
| 414 name = force_text(queryset.model._meta.verbose_name_plural) |
| 415 raise Http404(_("No %(verbose_name_plural)s available") % |
| 416 {'verbose_name_plural': name}) |
| 417 |
| 418 return date_list |
| 419 |
| 420 |
| 421 class BaseArchiveIndexView(BaseDateListView): |
| 422 """ |
| 423 Base class for archives of date-based items. |
| 424 |
| 425 Requires a response mixin. |
| 426 """ |
| 427 context_object_name = 'latest' |
| 428 |
| 429 def get_dated_items(self): |
| 430 """ |
| 431 Return (date_list, items, extra_context) for this request. |
| 432 """ |
| 433 qs = self.get_dated_queryset() |
| 434 date_list = self.get_date_list(qs, ordering='DESC') |
| 435 |
| 436 if not date_list: |
| 437 qs = qs.none() |
| 438 |
| 439 return (date_list, qs, {}) |
| 440 |
| 441 |
| 442 class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView
): |
| 443 """ |
| 444 Top-level archive of date-based items. |
| 445 """ |
| 446 template_name_suffix = '_archive' |
| 447 |
| 448 |
| 449 class BaseYearArchiveView(YearMixin, BaseDateListView): |
| 450 """ |
| 451 List of objects published in a given year. |
| 452 """ |
| 453 date_list_period = 'month' |
| 454 make_object_list = False |
| 455 |
| 456 def get_dated_items(self): |
| 457 """ |
| 458 Return (date_list, items, extra_context) for this request. |
| 459 """ |
| 460 year = self.get_year() |
| 461 |
| 462 date_field = self.get_date_field() |
| 463 date = _date_from_string(year, self.get_year_format()) |
| 464 |
| 465 since = self._make_date_lookup_arg(date) |
| 466 until = self._make_date_lookup_arg(self._get_next_year(date)) |
| 467 lookup_kwargs = { |
| 468 '%s__gte' % date_field: since, |
| 469 '%s__lt' % date_field: until, |
| 470 } |
| 471 |
| 472 qs = self.get_dated_queryset(**lookup_kwargs) |
| 473 date_list = self.get_date_list(qs) |
| 474 |
| 475 if not self.get_make_object_list(): |
| 476 # We need this to be a queryset since parent classes introspect it |
| 477 # to find information about the model. |
| 478 qs = qs.none() |
| 479 |
| 480 return (date_list, qs, { |
| 481 'year': date, |
| 482 'next_year': self.get_next_year(date), |
| 483 'previous_year': self.get_previous_year(date), |
| 484 }) |
| 485 |
| 486 def get_make_object_list(self): |
| 487 """ |
| 488 Return `True` if this view should contain the full list of objects in |
| 489 the given year. |
| 490 """ |
| 491 return self.make_object_list |
| 492 |
| 493 |
| 494 class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView): |
| 495 """ |
| 496 List of objects published in a given year. |
| 497 """ |
| 498 template_name_suffix = '_archive_year' |
| 499 |
| 500 |
| 501 class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): |
| 502 """ |
| 503 List of objects published in a given month. |
| 504 """ |
| 505 date_list_period = 'day' |
| 506 |
| 507 def get_dated_items(self): |
| 508 """ |
| 509 Return (date_list, items, extra_context) for this request. |
| 510 """ |
| 511 year = self.get_year() |
| 512 month = self.get_month() |
| 513 |
| 514 date_field = self.get_date_field() |
| 515 date = _date_from_string(year, self.get_year_format(), |
| 516 month, self.get_month_format()) |
| 517 |
| 518 since = self._make_date_lookup_arg(date) |
| 519 until = self._make_date_lookup_arg(self._get_next_month(date)) |
| 520 lookup_kwargs = { |
| 521 '%s__gte' % date_field: since, |
| 522 '%s__lt' % date_field: until, |
| 523 } |
| 524 |
| 525 qs = self.get_dated_queryset(**lookup_kwargs) |
| 526 date_list = self.get_date_list(qs) |
| 527 |
| 528 return (date_list, qs, { |
| 529 'month': date, |
| 530 'next_month': self.get_next_month(date), |
| 531 'previous_month': self.get_previous_month(date), |
| 532 }) |
| 533 |
| 534 |
| 535 class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView
): |
| 536 """ |
| 537 List of objects published in a given month. |
| 538 """ |
| 539 template_name_suffix = '_archive_month' |
| 540 |
| 541 |
| 542 class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView): |
| 543 """ |
| 544 List of objects published in a given week. |
| 545 """ |
| 546 |
| 547 def get_dated_items(self): |
| 548 """ |
| 549 Return (date_list, items, extra_context) for this request. |
| 550 """ |
| 551 year = self.get_year() |
| 552 week = self.get_week() |
| 553 |
| 554 date_field = self.get_date_field() |
| 555 week_format = self.get_week_format() |
| 556 week_start = { |
| 557 '%W': '1', |
| 558 '%U': '0', |
| 559 }[week_format] |
| 560 date = _date_from_string(year, self.get_year_format(), |
| 561 week_start, '%w', |
| 562 week, week_format) |
| 563 |
| 564 since = self._make_date_lookup_arg(date) |
| 565 until = self._make_date_lookup_arg(self._get_next_week(date)) |
| 566 lookup_kwargs = { |
| 567 '%s__gte' % date_field: since, |
| 568 '%s__lt' % date_field: until, |
| 569 } |
| 570 |
| 571 qs = self.get_dated_queryset(**lookup_kwargs) |
| 572 |
| 573 return (None, qs, { |
| 574 'week': date, |
| 575 'next_week': self.get_next_week(date), |
| 576 'previous_week': self.get_previous_week(date), |
| 577 }) |
| 578 |
| 579 |
| 580 class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView): |
| 581 """ |
| 582 List of objects published in a given week. |
| 583 """ |
| 584 template_name_suffix = '_archive_week' |
| 585 |
| 586 |
| 587 class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView): |
| 588 """ |
| 589 List of objects published on a given day. |
| 590 """ |
| 591 def get_dated_items(self): |
| 592 """ |
| 593 Return (date_list, items, extra_context) for this request. |
| 594 """ |
| 595 year = self.get_year() |
| 596 month = self.get_month() |
| 597 day = self.get_day() |
| 598 |
| 599 date = _date_from_string(year, self.get_year_format(), |
| 600 month, self.get_month_format(), |
| 601 day, self.get_day_format()) |
| 602 |
| 603 return self._get_dated_items(date) |
| 604 |
| 605 def _get_dated_items(self, date): |
| 606 """ |
| 607 Do the actual heavy lifting of getting the dated items; this accepts a |
| 608 date object so that TodayArchiveView can be trivial. |
| 609 """ |
| 610 lookup_kwargs = self._make_single_date_lookup(date) |
| 611 qs = self.get_dated_queryset(**lookup_kwargs) |
| 612 |
| 613 return (None, qs, { |
| 614 'day': date, |
| 615 'previous_day': self.get_previous_day(date), |
| 616 'next_day': self.get_next_day(date), |
| 617 'previous_month': self.get_previous_month(date), |
| 618 'next_month': self.get_next_month(date) |
| 619 }) |
| 620 |
| 621 |
| 622 class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView): |
| 623 """ |
| 624 List of objects published on a given day. |
| 625 """ |
| 626 template_name_suffix = "_archive_day" |
| 627 |
| 628 |
| 629 class BaseTodayArchiveView(BaseDayArchiveView): |
| 630 """ |
| 631 List of objects published today. |
| 632 """ |
| 633 |
| 634 def get_dated_items(self): |
| 635 """ |
| 636 Return (date_list, items, extra_context) for this request. |
| 637 """ |
| 638 return self._get_dated_items(datetime.date.today()) |
| 639 |
| 640 |
| 641 class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView
): |
| 642 """ |
| 643 List of objects published today. |
| 644 """ |
| 645 template_name_suffix = "_archive_day" |
| 646 |
| 647 |
| 648 class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailV
iew): |
| 649 """ |
| 650 Detail view of a single object on a single date; this differs from the |
| 651 standard DetailView by accepting a year/month/day in the URL. |
| 652 """ |
| 653 def get_object(self, queryset=None): |
| 654 """ |
| 655 Get the object this request displays. |
| 656 """ |
| 657 year = self.get_year() |
| 658 month = self.get_month() |
| 659 day = self.get_day() |
| 660 date = _date_from_string(year, self.get_year_format(), |
| 661 month, self.get_month_format(), |
| 662 day, self.get_day_format()) |
| 663 |
| 664 # Use a custom queryset if provided |
| 665 qs = self.get_queryset() if queryset is None else queryset |
| 666 |
| 667 if not self.get_allow_future() and date > datetime.date.today(): |
| 668 raise Http404(_( |
| 669 "Future %(verbose_name_plural)s not available because " |
| 670 "%(class_name)s.allow_future is False." |
| 671 ) % { |
| 672 'verbose_name_plural': qs.model._meta.verbose_name_plural, |
| 673 'class_name': self.__class__.__name__, |
| 674 }) |
| 675 |
| 676 # Filter down a queryset from self.queryset using the date from the |
| 677 # URL. This'll get passed as the queryset to DetailView.get_object, |
| 678 # which'll handle the 404 |
| 679 lookup_kwargs = self._make_single_date_lookup(date) |
| 680 qs = qs.filter(**lookup_kwargs) |
| 681 |
| 682 return super(BaseDetailView, self).get_object(queryset=qs) |
| 683 |
| 684 |
| 685 class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView): |
| 686 """ |
| 687 Detail view of a single object on a single date; this differs from the |
| 688 standard DetailView by accepting a year/month/day in the URL. |
| 689 """ |
| 690 template_name_suffix = '_detail' |
| 691 |
| 692 |
| 693 def _date_from_string(year, year_format, month='', month_format='', day='', day_
format='', delim='__'): |
| 694 """ |
| 695 Helper: get a datetime.date object given a format string and a year, |
| 696 month, and day (only year is mandatory). Raise a 404 for an invalid date. |
| 697 """ |
| 698 format = delim.join((year_format, month_format, day_format)) |
| 699 datestr = delim.join((year, month, day)) |
| 700 try: |
| 701 return datetime.datetime.strptime(force_str(datestr), format).date() |
| 702 except ValueError: |
| 703 raise Http404(_("Invalid date string '%(datestr)s' given format '%(forma
t)s'") % { |
| 704 'datestr': datestr, |
| 705 'format': format, |
| 706 }) |
| 707 |
| 708 |
| 709 def _get_next_prev(generic_view, date, is_previous, period): |
| 710 """ |
| 711 Helper: Get the next or the previous valid date. The idea is to allow |
| 712 links on month/day views to never be 404s by never providing a date |
| 713 that'll be invalid for the given view. |
| 714 |
| 715 This is a bit complicated since it handles different intervals of time, |
| 716 hence the coupling to generic_view. |
| 717 |
| 718 However in essence the logic comes down to: |
| 719 |
| 720 * If allow_empty and allow_future are both true, this is easy: just |
| 721 return the naive result (just the next/previous day/week/month, |
| 722 regardless of object existence.) |
| 723 |
| 724 * If allow_empty is true, allow_future is false, and the naive result |
| 725 isn't in the future, then return it; otherwise return None. |
| 726 |
| 727 * If allow_empty is false and allow_future is true, return the next |
| 728 date *that contains a valid object*, even if it's in the future. If |
| 729 there are no next objects, return None. |
| 730 |
| 731 * If allow_empty is false and allow_future is false, return the next |
| 732 date that contains a valid object. If that date is in the future, or |
| 733 if there are no next objects, return None. |
| 734 """ |
| 735 date_field = generic_view.get_date_field() |
| 736 allow_empty = generic_view.get_allow_empty() |
| 737 allow_future = generic_view.get_allow_future() |
| 738 |
| 739 get_current = getattr(generic_view, '_get_current_%s' % period) |
| 740 get_next = getattr(generic_view, '_get_next_%s' % period) |
| 741 |
| 742 # Bounds of the current interval |
| 743 start, end = get_current(date), get_next(date) |
| 744 |
| 745 # If allow_empty is True, the naive result will be valid |
| 746 if allow_empty: |
| 747 if is_previous: |
| 748 result = get_current(start - datetime.timedelta(days=1)) |
| 749 else: |
| 750 result = end |
| 751 |
| 752 if allow_future or result <= timezone_today(): |
| 753 return result |
| 754 else: |
| 755 return None |
| 756 |
| 757 # Otherwise, we'll need to go to the database to look for an object |
| 758 # whose date_field is at least (greater than/less than) the given |
| 759 # naive result |
| 760 else: |
| 761 # Construct a lookup and an ordering depending on whether we're doing |
| 762 # a previous date or a next date lookup. |
| 763 if is_previous: |
| 764 lookup = {'%s__lt' % date_field: generic_view._make_date_lookup_arg(
start)} |
| 765 ordering = '-%s' % date_field |
| 766 else: |
| 767 lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg
(end)} |
| 768 ordering = date_field |
| 769 |
| 770 # Filter out objects in the future if appropriate. |
| 771 if not allow_future: |
| 772 # Fortunately, to match the implementation of allow_future, |
| 773 # we need __lte, which doesn't conflict with __lt above. |
| 774 if generic_view.uses_datetime_field: |
| 775 now = timezone.now() |
| 776 else: |
| 777 now = timezone_today() |
| 778 lookup['%s__lte' % date_field] = now |
| 779 |
| 780 qs = generic_view.get_queryset().filter(**lookup).order_by(ordering) |
| 781 |
| 782 # Snag the first object from the queryset; if it doesn't exist that |
| 783 # means there's no next/previous link available. |
| 784 try: |
| 785 result = getattr(qs[0], date_field) |
| 786 except IndexError: |
| 787 return None |
| 788 |
| 789 # Convert datetimes to dates in the current time zone. |
| 790 if generic_view.uses_datetime_field: |
| 791 if settings.USE_TZ: |
| 792 result = timezone.localtime(result) |
| 793 result = result.date() |
| 794 |
| 795 # Return the first day of the period. |
| 796 return get_current(result) |
| 797 |
| 798 |
| 799 def timezone_today(): |
| 800 """ |
| 801 Return the current date in the current time zone. |
| 802 """ |
| 803 if settings.USE_TZ: |
| 804 return timezone.localdate() |
| 805 else: |
| 806 return datetime.date.today() |
OLD | NEW |