text: # ------------------------------------------------------------------------------
  # Joyous Widgets
  # ------------------------------------------------------------------------------
  import sys
  import json
  import datetime as dt
  from django.contrib.staticfiles.templatetags.staticfiles import static
  from django.forms import Media
  from django.utils.formats import get_format
  from django.utils import timezone
  from django.utils.translation import gettext as _
  from django.forms.widgets import MultiWidget, NumberInput, Select, \
          CheckboxSelectMultiple, FileInput
  from django.template.loader import render_to_string
  from wagtail.admin.widgets import AdminDateInput, AdminTimeInput
  from dateutil.parser import parse as dt_parse
  from .utils.recurrence import Weekday, Recurrence, DAILY, WEEKLY, MONTHLY, YEARLY
  from .utils.manythings import toTheOrdinal
  from .utils.names import WEEKDAY_NAMES, WEEKDAY_ABBRS, MONTH_ABBRS
  
  # ------------------------------------------------------------------------------
  class Time12hrInput(AdminTimeInput):
      """
      Display and Edit time fields in a 12hr format
      """
      template_name = 'joyous/widgets/time12hr_input.html'
  
      def __init__(self, attrs=None):
          super().__init__(attrs=attrs, format=None)
  
      def format_value(self, value):
          if isinstance(value, (dt.datetime, dt.time)):
              return value.strftime("%I:%M%P") # %P for lower case am/pm
          else:
              return value
  
      @property
      def media(self):
          return Media(js=[static("joyous/js/time12hr_admin.js")])
  
  # ------------------------------------------------------------------------------
  (EVERY_DAY, SAME_DAY, DAY_OF_MONTH) = (100, 101, 200)
  
  class RecurrenceWidget(MultiWidget):
      """
      Widget for entering the rule of a recurrence
      """
      template_name = 'joyous/widgets/recurrence_widget.html'
  
      def __init__(self, attrs=None):
          freqOptions = [(DAILY,   _("Daily")),
                         (WEEKLY,  _("Weekly")),
                         (MONTHLY, _("Monthly")),
                         (YEARLY,  _("Yearly"))]
          ordOptions1 = [(1,  toTheOrdinal(1)),
                         (2,  toTheOrdinal(2)),
                         (3,  toTheOrdinal(3)),
                         (4,  toTheOrdinal(4)),
                         (5,  toTheOrdinal(5)),
                         (-1, toTheOrdinal(-1)),
                         (EVERY_DAY, _("Every")),
                         (SAME_DAY, _("The Same"))]
          ordOptions2 = [(None, ""),
                         (1,    toTheOrdinal(1)),
                         (2,    toTheOrdinal(2)),
                         (3,    toTheOrdinal(3)),
                         (4,    toTheOrdinal(4)),
                         (5,    toTheOrdinal(5)),
                         (-1,   toTheOrdinal(-1))]
          dayOptions1  = enumerate(WEEKDAY_ABBRS)
          dayOptions2  = [(None, "")] + list(enumerate(WEEKDAY_NAMES))
          dayOptions3  = list(enumerate(WEEKDAY_NAMES)) +\
                         [(DAY_OF_MONTH, _("Day of the month"))]
          monthOptions = enumerate(MONTH_ABBRS[1:], 1)
  
          numAttrs = {'min': 1, 'max': 366}
          disableAttrs = {'disabled': True}
          if attrs:
              numAttrs.update(attrs)
              disableAttrs.update(attrs)
          widgets = [AdminDateInput(attrs=attrs),
                     Select(attrs=attrs, choices=freqOptions),         #1
                     NumberInput(attrs=numAttrs),
                     CheckboxSelectMultiple(attrs=attrs, choices=dayOptions1),
                     NumberInput(attrs=numAttrs),
                     AdminDateInput(attrs=attrs),                      #5
                     Select(attrs=attrs, choices=ordOptions1),
                     Select(attrs=attrs, choices=dayOptions3),
                     Select(attrs=disableAttrs, choices=ordOptions2),
                     Select(attrs=disableAttrs, choices=dayOptions2),
                     Select(attrs=disableAttrs, choices=ordOptions2),  #10
                     Select(attrs=disableAttrs, choices=dayOptions2),
                     CheckboxSelectMultiple(attrs=attrs, choices=monthOptions) ]
          super().__init__(widgets, attrs)
  
      def decompress(self, value):
          wdayChoices = []
          ordChoices  = [SAME_DAY,    None, None]
          dayChoices  = [DAY_OF_MONTH, None, None]
          monChoices  = []
          if isinstance(value, Recurrence):
              if value.freq == WEEKLY:
                  if value.byweekday:
                      wdayChoices = [day.weekday for day in value.byweekday]
              elif value.freq in (MONTHLY, YEARLY):
                  if value.byweekday:
                      if len(value.byweekday) == 7 and all(not day.n for day in value.byweekday):
                          ordChoices[0] = EVERY_DAY
                          dayChoices[0] = DAY_OF_MONTH
                      else:
                          for (i, day) in enumerate(value.byweekday[:3]):
                              dayChoices[i] = day.weekday
                              ordChoices[i] = day.n or EVERY_DAY
                  elif value.bymonthday:
                      ordChoices[0] = value.bymonthday[0]
                      if value.dtstart.day == ordChoices[0]:
                          ordChoices[0] = SAME_DAY
                      dayChoices[0] = DAY_OF_MONTH
                  if value.bymonth:
                      monChoices = value.bymonth
                  else:
                      value.dtstart.month
              return [value.dtstart,
                      value.freq,      #1
                      value.interval,
                      wdayChoices,
                      value.count,
                      value.until,     #5
                      ordChoices[0],
                      dayChoices[0],
                      ordChoices[1],
                      dayChoices[1],
                      ordChoices[2],   #10
                      dayChoices[2],
                      monChoices]
          else:
              return [None,
                      None,            #1
                      1,
                      wdayChoices,
                      None,
                      None,            #5
                      ordChoices[0],
                      dayChoices[0],
                      ordChoices[1],
                      dayChoices[1],
                      ordChoices[2],   #10
                      dayChoices[2],
                      monChoices]
  
      def get_context(self, name, value, attrs):
          """
          Return the context to use with the widget's template, e.g.
          {'widget': {
              'attrs': {'id': "id_repeat", 'required': True},
              'is_hidden': False,
              'name': "repeat",
              'required': True,
              'subwidgets': [... the context of all the component subwigets...],
              'template_name': "joyous/widgets/recurrence_widget.html",
              'value': "Tuesdays",
              'value_s': "Tuesdays",
              'value_r': "DTSTART:20181201\nRRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TU",
              }}}
          """
          context = super().get_context(name, value, attrs)
          context['widget']['value_s'] = str(value)
          context['widget']['value_r'] = repr(value)
          return context
  
      def value_from_datadict(self, data, files, name):
          values = [widget.value_from_datadict(data, files, "{}_{}".format(name, i))
                    for i, widget in enumerate(self.widgets)]
          try:
              def toIntOrNone(value):
                  return int(value) if value else None
              dtstart     = dt_parse(values[0]) if values[0] else None
              freq        = toIntOrNone(values[1])
              interval    = toIntOrNone(values[2]) or None
              #count      = toIntOrNone(values[4]) or None
              dtuntil     = dt_parse(values[5]) if values[5] else None
              ordChoices  = [toIntOrNone(values[6]),
                             toIntOrNone(values[8]),
                             toIntOrNone(values[10])]
              dayChoices  = [toIntOrNone(values[7]),
                             toIntOrNone(values[9]),
                             toIntOrNone(values[11])]
              wdayChoices = []
              mdayChoices = None
              monChoices  = []
              if freq == WEEKLY:
                  if values[3]:
                      wdayChoices = [int(day) for day in values[3]]
              elif freq in (MONTHLY, YEARLY):
                  if dayChoices[0] == DAY_OF_MONTH:    # day of the month
                      if ordChoices[0] == EVERY_DAY:      # every day, == daily
                          wdayChoices = range(7)
                      elif ordChoices[0] == SAME_DAY:     # the same day of the month
                          mdayChoices = None
                      else:
                          mdayChoices = [ordChoices[0]]
                  else:                         # a day of the week
                      if ordChoices[0] == EVERY_DAY:      # every of this weekday
                          wdayChoices = [Weekday(dayChoices[0])]
                      elif ordChoices[0] == SAME_DAY:     # the same weekday of the month
                          wdayNum = (dtstart.day - 1) // 7 + 1
                          wdayChoices = [Weekday(dayChoices[0], wdayNum)]
                      else:
                          wdayChoices = [Weekday(dayChoices[0], ordChoices[0])]
                  if dayChoices[1] != None and ordChoices[1] != None:
                      wdayChoices.append(Weekday(dayChoices[1], ordChoices[1]))
                  if dayChoices[2] != None and ordChoices[2] != None:
                      wdayChoices.append(Weekday(dayChoices[2], ordChoices[2]))
                  if freq == YEARLY:
                      if values[12]:
                          monChoices = [int(month) for month in values[12]]
  
              retval = Recurrence(dtstart    = dtstart,
                                  freq       = freq,
                                  interval   = interval,
                                  byweekday  = wdayChoices,
                                  #count      = count,
                                  until      = dtuntil,
                                  bymonthday = mdayChoices,
                                  bymonth    = monChoices)
          except (TypeError, ValueError):
              retval = None
          return retval
  
      @property
      def media(self):
          media = super()._get_media()
          media += Media(css={'all': [static("joyous/css/recurrence_admin.css")]},
                         js=[static("joyous/js/recurrence_admin.js")])
          return media
  
  # ------------------------------------------------------------------------------
  class ExceptionDateInput(AdminDateInput):
      """
      Display and Edit the dates which are the exceptions to a recurrence rule
      """
      template_name = 'joyous/widgets/exception_date_input.html'
  
      def __init__(self, attrs=None, format=None):
          super().__init__(attrs=attrs, format=format)
          self.overrides_repeat = None
  
      def get_context(self, name, value, attrs):
          context = super().get_context(name, value, attrs)
          config = {
              'dayOfWeekStart': get_format('FIRST_DAY_OF_WEEK'),
              'format':         self.js_format,
          }
          context['widget']['valid_dates'] = json.dumps(self.valid_dates())
          context['widget']['config_json'] = json.dumps(config)
          return context
  
      def valid_dates(self):
          valid_dates = None # null in JS
          if self.overrides_repeat:
              today = timezone.localdate()
              past = (today - dt.timedelta(days=200)).replace(day=1)
              future = (today + dt.timedelta(days=600)).replace(day=1)
              valid_dates = ["{:%Y%m%d}".format(occurence) for occurence in
                             self.overrides_repeat.between(past, future, inc=True)]
          return valid_dates
  
      @property
      def media(self):
          return Media(css={'all': [static("joyous/css/recurrence_admin.css")]},
                       js=[static("joyous/js/recurrence_admin.js")])
  
  # TODO Should probably also do validation on the returned date?
  # that would require ExceptionDateField and ExceptionDateFormField :(
  # or else use custom form for page validation?
  # https://github.com/torchbox/wagtail/pull/1867
  
  # ------------------------------------------------------------------------------
  