1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
|
import datetime
import math
from calendar import Calendar
from collections import defaultdict
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.views.generic.dates import (
DateMixin,
MonthMixin,
YearMixin,
_date_from_string,
)
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
DAYS = (
_("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday"),
)
def daterange(start_date, end_date):
"""
Returns an iterator of dates between two provided ones
"""
for n in range(int((end_date - start_date).days + 1)):
yield start_date + datetime.timedelta(n)
class BaseCalendarMonthView(DateMixin, YearMixin, MonthMixin, BaseListView):
"""
A base view for displaying a calendar month
"""
first_of_week = 0 # 0 = Monday, 6 = Sunday
paginate_by = None # We don't want to use this part of MultipleObjectMixin
date_field = None
end_date_field = None # For supporting events with duration
def get_paginate_by(self, queryset):
if self.paginate_by is not None:
raise ImproperlyConfigured(
"'%s' cannot be paginated, it is a calendar view"
% self.__class__.__name__
)
return None
def get_allow_future(self):
return True
def get_end_date_field(self):
"""
Returns the model field to use for end dates
"""
return self.end_date_field
def get_start_date(self, obj):
"""
Returns the start date for a model instance
"""
obj_date = getattr(obj, self.get_date_field())
try:
obj_date = obj_date.date()
except AttributeError:
# It's a date rather than datetime, so we use it as is
pass
return obj_date
def get_end_date(self, obj):
"""
Returns the end date for a model instance
"""
obj_date = getattr(obj, self.get_end_date_field())
try:
obj_date = obj_date.date()
except AttributeError:
# It's a date rather than datetime, so we use it as is
pass
return obj_date
def get_first_of_week(self):
"""
Returns an integer representing the first day of the week.
0 represents Monday, 6 represents Sunday.
"""
if self.first_of_week is None:
raise ImproperlyConfigured(
"%s.first_of_week is required." % self.__class__.__name__
)
if self.first_of_week not in range(7):
raise ImproperlyConfigured(
"%s.first_of_week must be an integer between 0 and 6."
% self.__class__.__name__
)
return self.first_of_week
def get_queryset(self):
"""
Returns a queryset of models for the month requested
"""
qs = super().get_queryset()
year = self.get_year()
month = self.get_month()
date_field = self.get_date_field()
end_date_field = self.get_end_date_field()
date = _date_from_string(
year, self.get_year_format(), month, self.get_month_format()
)
since = date
until = self.get_next_month(date)
# Adjust our start and end dates to allow for next and previous
# month edges
if since.weekday() != self.get_first_of_week():
diff = math.fabs(since.weekday() - self.get_first_of_week())
since = since - datetime.timedelta(days=diff)
if until.weekday() != ((self.get_first_of_week() + 6) % 7):
diff = math.fabs(((self.get_first_of_week() + 6) % 7) - until.weekday())
until = until + datetime.timedelta(days=diff)
if end_date_field:
# 5 possible conditions for showing an event:
# 1) Single day event, starts after 'since'
# 2) Multi-day event, starts after 'since' and ends before 'until'
# 3) Starts before 'since' and ends after 'since' and before 'until'
# 4) Starts after 'since' but before 'until' and ends after 'until'
# 5) Starts before 'since' and ends after 'until'
predicate1 = Q(**{"%s__gte" % date_field: since, end_date_field: None})
predicate2 = Q(
**{"%s__gte" % date_field: since, "%s__lt" % end_date_field: until}
)
predicate3 = Q(
**{
"%s__lt" % date_field: since,
"%s__gte" % end_date_field: since,
"%s__lt" % end_date_field: until,
}
)
predicate4 = Q(
**{
"%s__gte" % date_field: since,
"%s__lt" % date_field: until,
"%s__gte" % end_date_field: until,
}
)
predicate5 = Q(
**{"%s__lt" % date_field: since, "%s__gte" % end_date_field: until}
)
return qs.filter(
predicate1 | predicate2 | predicate3 | predicate4 | predicate5
)
return qs.filter(**{"%s__gte" % date_field: since})
def get_context_data(self, **kwargs):
"""
Injects variables necessary for rendering the calendar into the context.
Variables added are: `calendar`, `weekdays`, `month`, `next_month` and
`previous_month`.
"""
data = super().get_context_data(**kwargs)
year = self.get_year()
month = self.get_month()
date = _date_from_string(
year, self.get_year_format(), month, self.get_month_format()
)
cal = Calendar(self.get_first_of_week())
month_calendar = []
now = datetime.datetime.utcnow()
date_lists = defaultdict(list)
multidate_objs = []
for obj in data["object_list"]:
obj_date = self.get_start_date(obj)
end_date_field = self.get_end_date_field()
if end_date_field:
end_date = self.get_end_date(obj)
if end_date and end_date != obj_date:
multidate_objs.append(
{
"obj": obj,
"range": [x for x in daterange(obj_date, end_date)],
}
)
continue # We don't put multi-day events in date_lists
date_lists[obj_date].append(obj)
for week in cal.monthdatescalendar(date.year, date.month):
week_range = set(daterange(week[0], week[6]))
week_events = []
for val in multidate_objs:
intersect_length = len(week_range.intersection(val["range"]))
if intersect_length:
# Event happens during this week
slot = 1
width = (
intersect_length
) # How many days is the event during this week?
nowrap_previous = (
True
) # Does the event continue from the previous week?
nowrap_next = True # Does the event continue to the next week?
if val["range"][0] >= week[0]:
slot = 1 + (val["range"][0] - week[0]).days
else:
nowrap_previous = False
if val["range"][-1] > week[6]:
nowrap_next = False
week_events.append(
{
"event": val["obj"],
"slot": slot,
"width": width,
"nowrap_previous": nowrap_previous,
"nowrap_next": nowrap_next,
}
)
week_calendar = {"events": week_events, "date_list": []}
for day in week:
week_calendar["date_list"].append(
{
"day": day,
"events": date_lists[day],
"today": day == now.date(),
"is_current_month": day.month == date.month,
}
)
month_calendar.append(week_calendar)
data["calendar"] = month_calendar
data["weekdays"] = [DAYS[x] for x in cal.iterweekdays()]
data["month"] = date
data["next_month"] = self.get_next_month(date)
data["previous_month"] = self.get_previous_month(date)
return data
class CalendarMonthView(MultipleObjectTemplateResponseMixin, BaseCalendarMonthView):
"""
A view for displaying a calendar month, and rendering a template response
"""
template_name_suffix = "_calendar_month"
|