text: from __future__ import unicode_literals
  
  import datetime
  
  from django.conf import settings
  from django.core.exceptions import ValidationError
  from django.urls import reverse
  from django.db import models
  from django.utils import timezone
  from django.utils.encoding import python_2_unicode_compatible
  from django.utils.functional import cached_property
  from django.utils.translation import ugettext_lazy as _
  
  
  from .managers import EventManager
  
  auth_user_model = getattr(settings, "AUTH_USER_MODEL", "auth.User")
  
  
  @python_2_unicode_compatible
  class Event(models.Model):
  
      USER_COLORS = getattr(settings, "CALENDAR_COLORS", '')
  
      REPEAT_CHOICES = (
          ('NEVER', _('Never')),
          ('DAILY', _('Every Day')),
          ('WEEKDAY', _('Every Weekday')),
          ('WEEKLY', _('Every Week')),
          ('BIWEEKLY', _('Every 2 Weeks')),
          ('MONTHLY', _('Every Month')),
          ('YEARLY', _('Every Year')),
      )
      COLORS = [
          ('eeeeee', _('gray')),
          ('ff0000', _('red')),
          ('0000ff', _('blue')),
          ('00ff00', _('green')),
          ('000000', _('black')),
          ('ffffff', _('white')),
      ]
  
      try:
          COLORS += USER_COLORS
      except Exception:
          pass
  
      start_date = models.DateTimeField(verbose_name=_("start date"))
      end_date = models.DateTimeField(_("end date"))
      all_day = models.BooleanField(_("all day"), default=False)
      repeat = models.CharField(
          _("repeat"), max_length=15, choices=REPEAT_CHOICES, default='NEVER'
      )
      end_repeat = models.DateField(_("end repeat"), null=True, blank=True)
      title = models.CharField(_("title"), max_length=255)
      description = models.TextField(_("description"))
      location = models.ManyToManyField(
          'Location', verbose_name=_('locations'), blank=True
      )
      objects = EventManager()
      created_by = models.ForeignKey(
          auth_user_model, verbose_name=_("created by"), related_name='events', on_delete=models.CASCADE
      )
      categories = models.ManyToManyField(
          'Category', verbose_name=_('categories'), blank=True
      )
      tags = models.ManyToManyField('Tag', verbose_name=_('tags'), blank=True)
  
      # --------------------------------COLORS-------------------------------- #
      background_color = models.CharField(
          _("background color"), max_length=10, choices=COLORS, default='eeeeee'
      )
      background_color_custom = models.CharField(
          _("background color custom"), max_length=6, blank=True,
          help_text=_('Must be a valid hex triplet. Default is gray (eeeeee)')
      )
      font_color = models.CharField(
          _("font color"), max_length=10, choices=COLORS, default='000000'
      )
      font_color_custom = models.CharField(
          _("font color custom"), max_length=6, blank=True,
          help_text=_('Must be a valid hex triplet. Default is black (000000)')
      )
  
      def __init__(self, *args, **kwargs):
          super(Event, self).__init__(*args, **kwargs)
          self._last_popover_html = None
          self._last_check_if_cancelled = None
          self._check_if_cancelled_cache = {}
          self.title_extra = ''
  
      def get_l_start_date(self):
          """Localized start date."""
          return timezone.localtime(self.start_date)
  
      @cached_property
      def l_start_date(self):
          return self.get_l_start_date()
  
      def get_l_end_date(self):
          """Localized end date."""
          return timezone.localtime(self.end_date)
  
      @cached_property
      def l_end_date(self):
          return self.get_l_end_date()
  
      def is_happening(self, now):
          """Return True if the event is happening 'now', False if not."""
          start = self.l_start_date
          end = self.l_end_date
          happening = False
          # check that the event has started and 'now' is btwn start & end:
          if (now >= start) and (start.time() <= now.time() <= end.time()):
              if self.repeats('WEEKDAY'):
                  if not now.weekday() > 4:  # must be weekday
                      happening = True
              elif self.repeats('DAILY') or self.repeats('NEVER'):
                      happening = True
              elif self.repeats('MONTHLY'):
                  if start.day <= now.day <= end.day:
                      happening = True
              elif self.repeats('YEARLY'):
                  if (start.month <= now.month <= end.month) and \
                          (start.day <= now.day <= end.day):
                      happening = True
              else:
                  repeat = {'WEEKLY': 7, 'BIWEEKLY': 14}
                  while end <= now:
                      start += datetime.timedelta(days=repeat[self.repeat])
                      end += datetime.timedelta(days=repeat[self.repeat])
                  if start <= now <= end:
                      happening = True
          return happening
  
      def repeats(self, repeat):
          return self.repeat == repeat
  
      def is_chunk(self):
          return self.l_start_date.day != self.l_end_date.day
  
      def starts_same_month_as(self, month):
          return self.l_start_date.month == month
  
      def ends_same_month_as(self, month):
          return self.l_end_date.month == month
  
      def starts_same_year_month_as(self, year, month):
          return self.l_start_date.year == year and \
              self.l_start_date.month == month
  
      def starts_same_month_not_year_as(self, month, year):
          return self.l_start_date.year != year and \
              self.l_start_date.month == month
  
      def starts_ends_same_month(self):
          return self.l_start_date.month == self.l_end_date.month
  
      def starts_ends_yr_mo(self, year, month):
          yr = self.l_start_date.year == year or self.l_end_date.year == year
          mo = self.l_start_date.month == month or self.l_end_date.month == month
          return yr and mo
  
      def get_start_end_diff(self):
          """Return the difference between start and end dates."""
          s = self.l_start_date
          e = self.l_end_date
          start = datetime.date(s.year, s.month, s.day)
          end = datetime.date(e.year, e.month, e.day)
          diff = start - end
          return abs(diff.days)
  
      @cached_property
      def start_end_diff(self):
          return self.get_start_end_diff()
  
      def get_colors(self):
          bg = self.background_color_custom
          fnt = self.font_color_custom
          if not bg:
              bg = self.background_color
          if not fnt:
              fnt = self.font_color
          bg = '#' + bg
          fnt = '#' + fnt
          return bg, fnt
  
      @cached_property
      def colors(self):
          return self.get_colors()
  
      def will_occur(self, after_time):
          """Return True if the event will occur again after 'after_date'."""
          return (
              self.end_repeat is None
              or
              self.end_repeat >= after_time.date()
              or
              self.l_start_date >= after_time
              or
              self.l_end_date >= after_time
          )
  
      def __str__(self):
          return self.title
  
      def check_if_cancelled(self, date):
          """Return True if event was in cancelled state at 'date'. Also set self.title_extra to ' (CANCELLED)' if it was so.
  
          Warning! Results are memoized on instance level. If you need to reset "cache" of results then set ``instance._prefetched_objects_cache = {}``
          """
          result = self._check_if_cancelled_cache.get(date, None)
          if result is None:
              try:
                  # if cancellations are prefetched then use iteration
                  self._prefetched_objects_cache[Event.cancellations.related.field.related_query_name()]
                  result = any(
                      ((cancellation.date == date) for cancellation in self.cancellations.all())
                  )
              except (AttributeError, KeyError):
                  result = False
                  result = self.cancellations.filter(date=date).exists()
  
              self._check_if_cancelled_cache[date] = result
  
          if result:
              self.title_extra = _(" (CANCELLED)")
          self._last_check_if_cancelled = result
          return result
  
      @property
      def last_check_if_cancelled(self):
          if self._last_check_if_cancelled is None:
              raise AttributeError("``event.last_check_if_cancelled`` can't be used yet: call ``event.check_if_cancelled(date)`` first")
          return self._last_check_if_cancelled
  
      def clean(self):
          self.clean_start_end_dates()
          self.clean_repeat()
          self.clean_colors()
  
      def clean_start_end_dates(self):
          if self.start_date and self.end_date:
              if self.l_start_date > self.l_end_date:
                  raise ValidationError(
                      "The event's start date must be before the end date."
                  )
              elif (self.l_end_date - self.l_start_date) > datetime.timedelta(7):
                  raise ValidationError(
                      "Only events spanning 7 days or less are supported."
                  )
  
      def clean_repeat(self):
          if self.repeats('NEVER') and self.end_repeat:
              # events not scheduled to repeat should't have an end repeat date
              self.end_repeat = None
  
          if self.repeats('DAILY') or self.repeats('WEEKDAY'):
              if self.is_chunk():
                  raise ValidationError(
                      "Repeating every day or every weekday is not supported \
                      for events that start and end on different days."
                  )
  
      def clean_colors(self):
          """Makes sure that if a custom color is supplied, it's valid."""
          err = _("Color must be a valid hex triplet.")
          colors = ['background_color_custom', 'font_color_custom']
          colors2 = colors + ['background_color', 'font_color']
          # If there are custom colors specified in settings, length of
          # self.COLORS will be > 6, so check for validity
          if len(self.COLORS) > 6:
              colors = colors2
          for color in colors:
              c = getattr(self, color)
              l = len(c)
              if l:
                  if l != 6:
                      raise ValidationError(err)
                  else:
                      try:
                          int(c, 16)
                      except ValueError:
                          raise ValidationError(err)
  
      def get_absolute_url(self):
          return reverse('calendar:detail', args=[str(self.id)])
  
      class Meta:
          verbose_name = _('event')
          verbose_name_plural = _('events')
  
  
  @python_2_unicode_compatible
  class Location(models.Model):
      name = models.CharField(_('Name'), max_length=255)
      address_line_1 = models.CharField(
          _('Address Line 1'), max_length=255, blank=True)
      address_line_2 = models.CharField(
          _('Address Line 2'), max_length=255, blank=True)
      address_line_3 = models.CharField(
          _('Address Line 3'), max_length=255, blank=True)
      state = models.CharField(
          _('State / Province / Region'), max_length=63, blank=True)
      city = models.CharField(
          _('City / Town'), max_length=63, blank=True)
      zipcode = models.CharField(
          _('ZIP / Postal Code'), max_length=31, blank=True)
      country = models.CharField(_('Country'), max_length=127, blank=True)
  
      def __str__(self):
          return self.name
  
  
  @python_2_unicode_compatible
  class Category(models.Model):
      title = models.CharField(_('title'), max_length=255)
  
      def __str__(self):
          return self.title
  
      class Meta:
          verbose_name_plural = 'Categories'
  
  
  @python_2_unicode_compatible
  class Tag(models.Model):
      name = models.CharField(_('name'), max_length=255)
  
      def __str__(self):
          return self.name
  
  
  @python_2_unicode_compatible
  class Cancellation(models.Model):
      event = models.ForeignKey(
          Event, related_name="cancellations", related_query_name="cancellation", on_delete=models.CASCADE
      )
      reason = models.CharField(_("reason"), max_length=255)
      date = models.DateField(_("date"))
  
      def __str__(self):
          return self.event.title + ' - ' + str(self.date)
  