"Fossies" - the Fresh Open Source Software Archive

Member "PURELIB/trac/timeline/web_ui.py" (27 Aug 2019, 17326 Bytes) of package /windows/misc/Trac-1.4.win-amd64.exe:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "web_ui.py": 1.3.5_vs_1.3.6.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # Copyright (C) 2003-2019 Edgewall Software
    4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
    5 # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
    6 # Copyright (C) 2005-2006 Christian Boos <cboos@edgewall.org>
    7 # All rights reserved.
    8 #
    9 # This software is licensed as described in the file COPYING, which
   10 # you should have received as part of this distribution. The terms
   11 # are also available at https://trac.edgewall.org/wiki/TracLicense.
   12 #
   13 # This software consists of voluntary contributions made by many
   14 # individuals. For the exact contribution history, see the revision
   15 # history and logs, available at https://trac.edgewall.org/log/.
   16 #
   17 # Author: Jonas Borgström <jonas@edgewall.com>
   18 #         Christopher Lenz <cmlenz@gmx.de>
   19 
   20 import pkg_resources
   21 import re
   22 from datetime import datetime, timedelta
   23 
   24 from trac.config import IntOption, BoolOption
   25 from trac.core import *
   26 from trac.perm import IPermissionRequestor
   27 from trac.timeline.api import ITimelineEventProvider
   28 from trac.util.datefmt import (datetime_now, format_date, format_datetime,
   29                                format_time, localtz, parse_date,
   30                                pretty_timedelta, to_datetime, to_utimestamp,
   31                                truncate_datetime, user_time, utc)
   32 from trac.util.html import tag
   33 from trac.util.text import to_unicode
   34 from trac.util.translation import _
   35 from trac.web import IRequestHandler, IRequestFilter
   36 from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider,
   37                              accesskey, add_link, add_stylesheet, add_warning,
   38                              auth_link, component_guard, prevnext_nav,
   39                              web_context)
   40 from trac.wiki.api import IWikiSyntaxProvider
   41 from trac.wiki.formatter import concat_path_query_fragment, \
   42                                 split_url_into_path_query_fragment
   43 
   44 
   45 class TimelineModule(Component):
   46 
   47     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
   48                IRequestFilter, ITemplateProvider, IWikiSyntaxProvider)
   49 
   50     event_providers = ExtensionPoint(ITimelineEventProvider)
   51 
   52     default_daysback = IntOption('timeline', 'default_daysback', 30,
   53         """Default number of days displayed in the Timeline, in days.
   54         """)
   55 
   56     max_daysback = IntOption('timeline', 'max_daysback', 90,
   57         """Maximum number of days (-1 for unlimited) displayable in the
   58         Timeline.
   59         """)
   60 
   61     abbreviated_messages = BoolOption('timeline', 'abbreviated_messages',
   62                                       True,
   63         """Whether wiki-formatted event messages should be truncated or not.
   64 
   65         This only affects the default rendering, and can be overriden by
   66         specific event providers, see their own documentation.
   67         """)
   68 
   69     _authors_pattern = re.compile(r'(-)?(?:"([^"]*)"|\'([^\']*)\'|([^\s]+))')
   70 
   71     # INavigationContributor methods
   72 
   73     def get_active_navigation_item(self, req):
   74         return 'timeline'
   75 
   76     def get_navigation_items(self, req):
   77         if 'TIMELINE_VIEW' in req.perm('timeline'):
   78             yield ('mainnav', 'timeline',
   79                    tag.a(_("Timeline"), href=req.href.timeline(),
   80                          accesskey=accesskey(req, 2)))
   81 
   82     # IPermissionRequestor methods
   83 
   84     def get_permission_actions(self):
   85         return ['TIMELINE_VIEW']
   86 
   87     # IRequestHandler methods
   88 
   89     def match_request(self, req):
   90         return req.path_info == '/timeline'
   91 
   92     def process_request(self, req):
   93         req.perm('timeline').require('TIMELINE_VIEW')
   94 
   95         format = req.args.get('format')
   96         maxrows = req.args.getint('max', 50 if format == 'rss' else 0)
   97         lastvisit = req.session.as_int('timeline.lastvisit', 0)
   98 
   99         # indication of new events is unchanged when form is updated by user
  100         revisit = any(a in req.args
  101                       for a in ['update', 'from', 'daysback', 'author'])
  102         if revisit:
  103             lastvisit = req.session.as_int('timeline.nextlastvisit',
  104                                            lastvisit)
  105 
  106         # Parse the from date and adjust the timestamp to the last second of
  107         # the day
  108         fromdate = datetime_now(req.tz)
  109         today = truncate_datetime(fromdate)
  110         yesterday = to_datetime(today.replace(tzinfo=None) - timedelta(days=1),
  111                                 req.tz)
  112         precisedate = precision = None
  113         if 'from' in req.args:
  114             # Acquire from date only from non-blank input
  115             reqfromdate = req.args.get('from').strip()
  116             if reqfromdate:
  117                 try:
  118                     precisedate = user_time(req, parse_date, reqfromdate)
  119                 except TracError as e:
  120                     add_warning(req, e)
  121                 else:
  122                     fromdate = precisedate.astimezone(req.tz)
  123             precision = req.args.get('precision', '')
  124             if precision.startswith('second'):
  125                 precision = timedelta(seconds=1)
  126             elif precision.startswith('minute'):
  127                 precision = timedelta(minutes=1)
  128             elif precision.startswith('hour'):
  129                 precision = timedelta(hours=1)
  130             else:
  131                 precision = None
  132         fromdate = to_datetime(datetime(fromdate.year, fromdate.month,
  133                                         fromdate.day, 23, 59, 59, 999999),
  134                                req.tz)
  135 
  136         pref = req.session.as_int('timeline.daysback', self.default_daysback)
  137         default = 90 if format == 'rss' else pref
  138         daysback = req.args.as_int('daysback', default,
  139                                    min=1, max=self.max_daysback)
  140 
  141         authors = req.args.get('authors')
  142         if authors is None and format != 'rss':
  143             authors = req.session.get('timeline.authors')
  144         authors = (authors or '').strip()
  145 
  146         data = {'fromdate': fromdate, 'daysback': daysback,
  147                 'authors': authors, 'today': today, 'yesterday': yesterday,
  148                 'precisedate': precisedate, 'precision': precision,
  149                 'events': [], 'filters': [],
  150                 'abbreviated_messages': self.abbreviated_messages}
  151 
  152         available_filters = []
  153         for event_provider in self.event_providers:
  154             with component_guard(self.env, req, event_provider):
  155                 available_filters += (event_provider.get_timeline_filters(req)
  156                                       or [])
  157 
  158         # check the request or session for enabled filters, or use default
  159         filters = [f[0] for f in available_filters if f[0] in req.args]
  160         if not filters and format != 'rss':
  161             filters = [f[0] for f in available_filters
  162                             if req.session.as_int('timeline.filter.' + f[0])]
  163         if not filters:
  164             filters = [f[0] for f in available_filters if len(f) == 2 or f[2]]
  165 
  166         # save the results of submitting the timeline form to the session
  167         if 'update' in req.args:
  168             for filter_ in available_filters:
  169                 key = 'timeline.filter.%s' % filter_[0]
  170                 if filter_[0] in req.args:
  171                     req.session[key] = '1'
  172                 elif key in req.session:
  173                     del req.session[key]
  174 
  175         stop = fromdate
  176         start = to_datetime(stop.replace(tzinfo=None) -
  177                             timedelta(days=daysback + 1), req.tz)
  178 
  179         # create author include and exclude sets
  180         include = set()
  181         exclude = set()
  182         for match in self._authors_pattern.finditer(authors):
  183             name = (match.group(2) or match.group(3) or match.group(4)).lower()
  184             if match.group(1):
  185                 exclude.add(name)
  186             else:
  187                 include.add(name)
  188 
  189         # gather all events for the given period of time
  190         events = []
  191         for provider in self.event_providers:
  192             with component_guard(self.env, req, provider):
  193                 for event in provider.get_timeline_events(req, start, stop,
  194                                                           filters) or []:
  195                     author = (event[2] or '').lower()
  196                     if ((not include or author in include) and
  197                         author not in exclude):
  198                         events.append(
  199                             self._event_data(req, provider, event, lastvisit))
  200 
  201         # prepare sorted global list
  202         events = sorted(events, key=lambda e: e['datetime'], reverse=True)
  203         if maxrows:
  204             events = events[:maxrows]
  205 
  206         data['events'] = events
  207 
  208         if format == 'rss':
  209             rss_context = web_context(req, absurls=True)
  210             rss_context.set_hints(wiki_flavor='html', shorten_lines=False)
  211             data['context'] = rss_context
  212             return 'timeline.rss', data, {'content_type': 'application/rss+xml'}
  213         else:
  214             req.session.set('timeline.daysback', daysback,
  215                             self.default_daysback)
  216             req.session.set('timeline.authors', authors, '')
  217             # store lastvisit
  218             if events and not revisit:
  219                 lastviewed = to_utimestamp(events[0]['datetime'])
  220                 req.session['timeline.lastvisit'] = max(lastvisit, lastviewed)
  221                 req.session['timeline.nextlastvisit'] = lastvisit
  222             html_context = web_context(req)
  223             html_context.set_hints(wiki_flavor='oneliner',
  224                                    shorten_lines=self.abbreviated_messages)
  225             data['context'] = html_context
  226 
  227         add_stylesheet(req, 'common/css/timeline.css')
  228         rss_href = req.href.timeline([(f, 'on') for f in filters],
  229                                      daysback=90, max=50, authors=authors,
  230                                      format='rss')
  231         add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'),
  232                  'application/rss+xml', 'rss')
  233         Chrome(self.env).add_jquery_ui(req)
  234 
  235         for filter_ in available_filters:
  236             data['filters'].append({'name': filter_[0], 'label': filter_[1],
  237                                     'enabled': filter_[0] in filters})
  238 
  239         # Navigation to the previous/next period of 'daysback' days
  240         previous_start = fromdate.replace(tzinfo=None) - \
  241                          timedelta(days=daysback + 1)
  242         previous_start = format_date(previous_start, format='iso8601',
  243                                      tzinfo=req.tz)
  244         add_link(req, 'prev', req.href.timeline(from_=previous_start,
  245                                                 authors=authors,
  246                                                 daysback=daysback),
  247                  _("Previous Period"))
  248         if today - fromdate > timedelta(days=0):
  249             next_start = fromdate.replace(tzinfo=None) + \
  250                          timedelta(days=daysback + 1)
  251             next_start = format_date(to_datetime(next_start, req.tz),
  252                                      format='iso8601', tzinfo=req.tz)
  253             add_link(req, 'next', req.href.timeline(from_=next_start,
  254                                                     authors=authors,
  255                                                     daysback=daysback),
  256                      _("Next Period"))
  257         prevnext_nav(req, _("Previous Period"), _("Next Period"))
  258 
  259         return 'timeline.html', data
  260 
  261     # ITemplateProvider methods
  262 
  263     def get_htdocs_dirs(self):
  264         return []
  265 
  266     def get_templates_dirs(self):
  267         return [pkg_resources.resource_filename('trac.timeline', 'templates')]
  268 
  269     # IRequestFilter methods
  270 
  271     def pre_process_request(self, req, handler):
  272         return handler
  273 
  274     def post_process_request(self, req, template, data, metadata):
  275         if data:
  276             def pretty_dateinfo(date, format=None, dateonly=False):
  277                 if not date:
  278                     return ''
  279                 if format == 'date':
  280                     absolute = user_time(req, format_date, date)
  281                 else:
  282                     absolute = user_time(req, format_datetime, date)
  283                 now = datetime_now(localtz)
  284                 relative = pretty_timedelta(date, now)
  285                 if not format:
  286                     format = req.session.get('dateinfo',
  287                                  Chrome(self.env).default_dateinfo_format)
  288                 if format == 'relative':
  289                     if date > now:
  290                         label = _("in %(relative)s", relative=relative) \
  291                                 if not dateonly else relative
  292                         title = _("on %(date)s at %(time)s",
  293                                   date=user_time(req, format_date, date),
  294                                   time=user_time(req, format_time, date))
  295                         return tag.span(label, title=title)
  296                     else:
  297                         label = _("%(relative)s ago", relative=relative) \
  298                                 if not dateonly else relative
  299                         title = _("See timeline at %(absolutetime)s",
  300                                   absolutetime=absolute)
  301                 else:
  302                     if dateonly:
  303                         label = absolute
  304                     elif req.lc_time == 'iso8601':
  305                         label = _("at %(iso8601)s", iso8601=absolute)
  306                     elif format == 'date':
  307                         label = _("on %(date)s", date=absolute)
  308                     else:
  309                         label = _("on %(date)s at %(time)s",
  310                                   date=user_time(req, format_date, date),
  311                                   time=user_time(req, format_time, date))
  312                     if date > now:
  313                         title = _("in %(relative)s", relative=relative)
  314                         return tag.span(label, title=title)
  315                     title = _("See timeline %(relativetime)s ago",
  316                               relativetime=relative)
  317                 return self.get_timeline_link(req, date, label,
  318                                               precision='second', title=title)
  319             def dateinfo(date):
  320                 return pretty_dateinfo(date, format='relative', dateonly=True)
  321             data['pretty_dateinfo'] = pretty_dateinfo
  322             data['dateinfo'] = dateinfo
  323         return template, data, metadata
  324 
  325     # IWikiSyntaxProvider methods
  326 
  327     def get_wiki_syntax(self):
  328         return []
  329 
  330     def get_link_resolvers(self):
  331         def link_resolver(formatter, ns, target, label):
  332             path, query, fragment = split_url_into_path_query_fragment(target)
  333             precision = None
  334             time = path.split("T", 1)
  335             if len(time) > 1:
  336                 time = time[1].split("Z")[0]
  337                 if len(time) >= 6:
  338                     precision = 'seconds'
  339                 elif len(time) >= 4:
  340                     precision = 'minutes'
  341                 elif len(time) >= 2:
  342                     precision = 'hours'
  343             try:
  344                 dt = parse_date(path, utc, locale='iso8601', hint='iso8601')
  345                 return self.get_timeline_link(formatter.req, dt, label,
  346                                               precision, query, fragment)
  347             except TracError as e:
  348                 return tag.a(label, title=to_unicode(e),
  349                              class_='timeline missing')
  350         yield 'timeline', link_resolver
  351 
  352     # Public methods
  353 
  354     def get_timeline_link(self, req, date, label=None, precision='hours',
  355                           query=None, fragment=None, title=None):
  356         iso_date = format_datetime(date, 'iso8601', req.tz)
  357         href = req.href.timeline(from_=iso_date, precision=precision)
  358         return tag.a(label or iso_date, class_='timeline',
  359                      title=title or _("See timeline at %(absolutetime)s",
  360                                       absolutetime=iso_date),
  361                      href=concat_path_query_fragment(href, query, fragment))
  362 
  363     # Internal methods
  364 
  365     def _event_data(self, req, provider, event, lastvisit):
  366         """Compose the timeline event date from the event tuple and prepared
  367         provider methods"""
  368         if len(event) == 5:  # with special provider
  369             kind, datetime, author, data, provider = event
  370         else:
  371             kind, datetime, author, data = event
  372         render = lambda field, context: \
  373                  provider.render_timeline_event(context, field, event)
  374         localized_datetime = to_datetime(datetime, tzinfo=req.tz)
  375         localized_date = truncate_datetime(localized_datetime)
  376         datetime_uid = to_utimestamp(localized_datetime)
  377         return {'kind': kind, 'author': author, 'date': localized_date,
  378                 'datetime': localized_datetime, 'datetime_uid': datetime_uid,
  379                 'render': render,
  380                 'unread': lastvisit and lastvisit < datetime_uid,
  381                 'event': event, 'data': data, 'provider': provider}