text: # -*- coding: utf-8 -*-
  #
  # Copyright (C) 2003-2020 Edgewall Software
  # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
  # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
  # Copyright (C) 2005-2006 Christian Boos <cboos@edgewall.org>
  # All rights reserved.
  #
  # This software is licensed as described in the file COPYING, which
  # you should have received as part of this distribution. The terms
  # are also available at https://trac.edgewall.org/wiki/TracLicense.
  #
  # This software consists of voluntary contributions made by many
  # individuals. For the exact contribution history, see the revision
  # history and logs, available at https://trac.edgewall.org/log/.
  #
  # Author: Jonas Borgström <jonas@edgewall.com>
  #         Christopher Lenz <cmlenz@gmx.de>
  
  import pkg_resources
  import re
  from datetime import datetime, timedelta
  
  from trac.config import IntOption, BoolOption
  from trac.core import *
  from trac.perm import IPermissionRequestor
  from trac.timeline.api import ITimelineEventProvider
  from trac.util.datefmt import (datetime_now, format_date, format_datetime,
                                 format_time, localtz, parse_date,
                                 pretty_timedelta, to_datetime, to_utimestamp,
                                 truncate_datetime, user_time, utc)
  from trac.util.html import tag
  from trac.util.text import to_unicode
  from trac.util.translation import _
  from trac.web import IRequestHandler, IRequestFilter
  from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider,
                               accesskey, add_link, add_stylesheet, add_warning,
                               auth_link, component_guard, prevnext_nav,
                               web_context)
  from trac.wiki.api import IWikiSyntaxProvider
  from trac.wiki.formatter import concat_path_query_fragment, \
                                  split_url_into_path_query_fragment
  
  
  class TimelineModule(Component):
  
      implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
                 IRequestFilter, ITemplateProvider, IWikiSyntaxProvider)
  
      event_providers = ExtensionPoint(ITimelineEventProvider)
  
      default_daysback = IntOption('timeline', 'default_daysback', 30,
          """Default number of days displayed in the Timeline, in days.
          """)
  
      max_daysback = IntOption('timeline', 'max_daysback', 90,
          """Maximum number of days (-1 for unlimited) displayable in the
          Timeline.
          """)
  
      abbreviated_messages = BoolOption('timeline', 'abbreviated_messages',
                                        True,
          """Whether wiki-formatted event messages should be truncated or not.
  
          This only affects the default rendering, and can be overriden by
          specific event providers, see their own documentation.
          """)
  
      _authors_pattern = re.compile(r'(-)?(?:"([^"]*)"|\'([^\']*)\'|([^\s]+))')
  
      # INavigationContributor methods
  
      def get_active_navigation_item(self, req):
          return 'timeline'
  
      def get_navigation_items(self, req):
          if 'TIMELINE_VIEW' in req.perm('timeline'):
              yield ('mainnav', 'timeline',
                     tag.a(_("Timeline"), href=req.href.timeline(),
                           accesskey=accesskey(req, 2)))
  
      # IPermissionRequestor methods
  
      def get_permission_actions(self):
          return ['TIMELINE_VIEW']
  
      # IRequestHandler methods
  
      def match_request(self, req):
          return req.path_info == '/timeline'
  
      def process_request(self, req):
          req.perm('timeline').require('TIMELINE_VIEW')
  
          format = req.args.get('format')
          maxrows = req.args.getint('max', 50 if format == 'rss' else 0)
          lastvisit = req.session.as_int('timeline.lastvisit', 0)
  
          # indication of new events is unchanged when form is updated by user
          revisit = any(a in req.args
                        for a in ['update', 'from', 'daysback', 'author'])
          if revisit:
              lastvisit = req.session.as_int('timeline.nextlastvisit',
                                             lastvisit)
  
          # Parse the from date and adjust the timestamp to the last second of
          # the day
          fromdate = datetime_now(req.tz)
          today = truncate_datetime(fromdate)
          yesterday = to_datetime(today.replace(tzinfo=None) - timedelta(days=1),
                                  req.tz)
          precisedate = precision = None
          if 'from' in req.args:
              # Acquire from date only from non-blank input
              reqfromdate = req.args.get('from').strip()
              if reqfromdate:
                  try:
                      precisedate = user_time(req, parse_date, reqfromdate)
                  except TracError as e:
                      add_warning(req, e)
                  else:
                      fromdate = precisedate.astimezone(req.tz)
              precision = req.args.get('precision', '')
              if precision.startswith('second'):
                  precision = timedelta(seconds=1)
              elif precision.startswith('minute'):
                  precision = timedelta(minutes=1)
              elif precision.startswith('hour'):
                  precision = timedelta(hours=1)
              else:
                  precision = None
          fromdate = to_datetime(datetime(fromdate.year, fromdate.month,
                                          fromdate.day, 23, 59, 59, 999999),
                                 req.tz)
  
          pref = req.session.as_int('timeline.daysback', self.default_daysback)
          default = 90 if format == 'rss' else pref
          daysback = req.args.as_int('daysback', default,
                                     min=1, max=self.max_daysback)
  
          authors = req.args.get('authors')
          if authors is None and format != 'rss':
              authors = req.session.get('timeline.authors')
          authors = (authors or '').strip()
  
          data = {'fromdate': fromdate, 'daysback': daysback,
                  'authors': authors, 'today': today, 'yesterday': yesterday,
                  'precisedate': precisedate, 'precision': precision,
                  'events': [], 'filters': [],
                  'abbreviated_messages': self.abbreviated_messages}
  
          available_filters = []
          for event_provider in self.event_providers:
              with component_guard(self.env, req, event_provider):
                  available_filters += (event_provider.get_timeline_filters(req)
                                        or [])
  
          # check the request or session for enabled filters, or use default
          filters = [f[0] for f in available_filters if f[0] in req.args]
          if not filters and format != 'rss':
              filters = [f[0] for f in available_filters
                              if req.session.as_int('timeline.filter.' + f[0])]
          if not filters:
              filters = [f[0] for f in available_filters if len(f) == 2 or f[2]]
  
          # save the results of submitting the timeline form to the session
          if 'update' in req.args:
              for filter_ in available_filters:
                  key = 'timeline.filter.%s' % filter_[0]
                  if filter_[0] in req.args:
                      req.session[key] = '1'
                  elif key in req.session:
                      del req.session[key]
  
          stop = fromdate
          start = to_datetime(stop.replace(tzinfo=None) -
                              timedelta(days=daysback + 1), req.tz)
  
          # create author include and exclude sets
          include = set()
          exclude = set()
          for match in self._authors_pattern.finditer(authors):
              name = (match.group(2) or match.group(3) or match.group(4)).lower()
              if match.group(1):
                  exclude.add(name)
              else:
                  include.add(name)
  
          # gather all events for the given period of time
          events = []
          for provider in self.event_providers:
              with component_guard(self.env, req, provider):
                  for event in provider.get_timeline_events(req, start, stop,
                                                            filters) or []:
                      author = (event[2] or '').lower()
                      if ((not include or author in include) and
                          author not in exclude):
                          events.append(
                              self._event_data(req, provider, event, lastvisit))
  
          # prepare sorted global list
          events = sorted(events, key=lambda e: e['datetime'], reverse=True)
          if maxrows:
              events = events[:maxrows]
  
          data['events'] = events
  
          if format == 'rss':
              rss_context = web_context(req, absurls=True)
              rss_context.set_hints(wiki_flavor='html', shorten_lines=False)
              data['context'] = rss_context
              return 'timeline.rss', data, {'content_type': 'application/rss+xml'}
          else:
              req.session.set('timeline.daysback', daysback,
                              self.default_daysback)
              req.session.set('timeline.authors', authors, '')
              # store lastvisit
              if events and not revisit:
                  lastviewed = to_utimestamp(events[0]['datetime'])
                  req.session['timeline.lastvisit'] = max(lastvisit, lastviewed)
                  req.session['timeline.nextlastvisit'] = lastvisit
              html_context = web_context(req)
              html_context.set_hints(wiki_flavor='oneliner',
                                     shorten_lines=self.abbreviated_messages)
              data['context'] = html_context
  
          add_stylesheet(req, 'common/css/timeline.css')
          rss_href = req.href.timeline([(f, 'on') for f in filters],
                                       daysback=90, max=50, authors=authors,
                                       format='rss')
          add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'),
                   'application/rss+xml', 'rss')
          Chrome(self.env).add_jquery_ui(req)
  
          for filter_ in available_filters:
              data['filters'].append({'name': filter_[0], 'label': filter_[1],
                                      'enabled': filter_[0] in filters})
  
          # Navigation to the previous/next period of 'daysback' days
          previous_start = fromdate.replace(tzinfo=None) - \
                           timedelta(days=daysback + 1)
          previous_start = format_date(previous_start, format='iso8601',
                                       tzinfo=req.tz)
          add_link(req, 'prev', req.href.timeline(from_=previous_start,
                                                  authors=authors,
                                                  daysback=daysback),
                   _("Previous Period"))
          if today - fromdate > timedelta(days=0):
              next_start = fromdate.replace(tzinfo=None) + \
                           timedelta(days=daysback + 1)
              next_start = format_date(to_datetime(next_start, req.tz),
                                       format='iso8601', tzinfo=req.tz)
              add_link(req, 'next', req.href.timeline(from_=next_start,
                                                      authors=authors,
                                                      daysback=daysback),
                       _("Next Period"))
          prevnext_nav(req, _("Previous Period"), _("Next Period"))
  
          return 'timeline.html', data
  
      # ITemplateProvider methods
  
      def get_htdocs_dirs(self):
          return []
  
      def get_templates_dirs(self):
          return [pkg_resources.resource_filename('trac.timeline', 'templates')]
  
      # IRequestFilter methods
  
      def pre_process_request(self, req, handler):
          return handler
  
      def post_process_request(self, req, template, data, metadata):
          if data:
              def pretty_dateinfo(date, format=None, dateonly=False):
                  if not date:
                      return ''
                  if format == 'date':
                      absolute = user_time(req, format_date, date)
                  else:
                      absolute = user_time(req, format_datetime, date)
                  now = datetime_now(localtz)
                  relative = pretty_timedelta(date, now)
                  if not format:
                      format = req.session.get('dateinfo',
                                   Chrome(self.env).default_dateinfo_format)
                  if format == 'relative':
                      if date > now:
                          label = _("in %(relative)s", relative=relative) \
                                  if not dateonly else relative
                          title = _("on %(date)s at %(time)s",
                                    date=user_time(req, format_date, date),
                                    time=user_time(req, format_time, date))
                          return tag.span(label, title=title)
                      else:
                          label = _("%(relative)s ago", relative=relative) \
                                  if not dateonly else relative
                          title = _("See timeline at %(absolutetime)s",
                                    absolutetime=absolute)
                  else:
                      if dateonly:
                          label = absolute
                      elif req.lc_time == 'iso8601':
                          label = _("at %(iso8601)s", iso8601=absolute)
                      elif format == 'date':
                          label = _("on %(date)s", date=absolute)
                      else:
                          label = _("on %(date)s at %(time)s",
                                    date=user_time(req, format_date, date),
                                    time=user_time(req, format_time, date))
                      if date > now:
                          title = _("in %(relative)s", relative=relative)
                          return tag.span(label, title=title)
                      title = _("See timeline %(relativetime)s ago",
                                relativetime=relative)
                  return self.get_timeline_link(req, date, label,
                                                precision='second', title=title)
              def dateinfo(date):
                  return pretty_dateinfo(date, format='relative', dateonly=True)
              data['pretty_dateinfo'] = pretty_dateinfo
              data['dateinfo'] = dateinfo
          return template, data, metadata
  
      # IWikiSyntaxProvider methods
  
      def get_wiki_syntax(self):
          return []
  
      def get_link_resolvers(self):
          def link_resolver(formatter, ns, target, label):
              path, query, fragment = split_url_into_path_query_fragment(target)
              precision = None
              time = path.split("T", 1)
              if len(time) > 1:
                  time = time[1].split("Z")[0]
                  if len(time) >= 6:
                      precision = 'seconds'
                  elif len(time) >= 4:
                      precision = 'minutes'
                  elif len(time) >= 2:
                      precision = 'hours'
              try:
                  dt = parse_date(path, utc, locale='iso8601', hint='iso8601')
                  return self.get_timeline_link(formatter.req, dt, label,
                                                precision, query, fragment)
              except TracError as e:
                  return tag.a(label, title=to_unicode(e),
                               class_='timeline missing')
          yield 'timeline', link_resolver
  
      # Public methods
  
      def get_timeline_link(self, req, date, label=None, precision='hours',
                            query=None, fragment=None, title=None):
          iso_date = format_datetime(date, 'iso8601', req.tz)
          href = req.href.timeline(from_=iso_date, precision=precision)
          return tag.a(label or iso_date, class_='timeline',
                       title=title or _("See timeline at %(absolutetime)s",
                                        absolutetime=iso_date),
                       href=concat_path_query_fragment(href, query, fragment))
  
      # Internal methods
  
      def _event_data(self, req, provider, event, lastvisit):
          """Compose the timeline event date from the event tuple and prepared
          provider methods"""
          if len(event) == 5:  # with special provider
              kind, datetime, author, data, provider = event
          else:
              kind, datetime, author, data = event
          render = lambda field, context: \
                   provider.render_timeline_event(context, field, event)
          localized_datetime = to_datetime(datetime, tzinfo=req.tz)
          localized_date = truncate_datetime(localized_datetime)
          datetime_uid = to_utimestamp(localized_datetime)
          return {'kind': kind, 'author': author, 'date': localized_date,
                  'datetime': localized_datetime, 'datetime_uid': datetime_uid,
                  'render': render,
                  'unread': lastvisit and lastvisit < datetime_uid,
                  'event': event, 'data': data, 'provider': provider}
  