MoinMoin/stats/hitcounts.py
author Thomas Waldmann <tw AT waldmann-edv DOT de>
Wed, 11 Feb 2009 02:34:33 +0100
changeset 4569 3caaa8c74c41
parent 4426 8982d8226218
permissions -rw-r--r--
wikiutil: replace moin's cgi/urllib wrappers by calls to werkzeug.utils code
     1 # -*- coding: iso-8859-1 -*-
     2 """
     3     MoinMoin - Hitcount Statistics
     4 
     5     This macro creates a hitcount chart from the data in "event.log".
     6 
     7     TODO: refactor to use a class, this code is ugly.
     8           A lot of code here is duplicated in stats.useragents.
     9           Maybe both can use same base class, maybe some parts are useful to other code.
    10 
    11     @copyright: 2002-2004 Juergen Hermann <jh@web.de>,
    12                 2007 MoinMoin:ThomasWaldmann
    13     @license: GNU GPL, see COPYING for details.
    14 """
    15 
    16 _debug = 0
    17 
    18 import time
    19 
    20 from MoinMoin import caching, wikiutil, logfile
    21 from MoinMoin.Page import Page
    22 from MoinMoin.logfile import eventlog
    23 
    24 # this is a CONSTANT used for on-disk caching, it must NOT be configurable and
    25 # not depend on request.user!
    26 DATE_FMT = '%04d-%02d-%02d' # % (y, m, d)
    27 
    28 def linkto(pagename, request, params=''):
    29     _ = request.getText
    30 
    31     if not request.cfg.chart_options:
    32         return text(pagename, request, params)
    33 
    34     if _debug:
    35         return draw(pagename, request)
    36 
    37     page = Page(request, pagename)
    38 
    39     # Create escaped query string from dict and params
    40     querystr = {'action': 'chart', 'type': 'hitcounts'}
    41     querystr = wikiutil.makeQueryString(querystr)
    42     querystr = wikiutil.escape(querystr)
    43     if params:
    44         querystr += '&amp;' + params
    45 
    46     data = {'url': page.url(request, querystr)}
    47     data.update(request.cfg.chart_options)
    48     result = ('<img src="%(url)s" width="%(width)d" height="%(height)d"'
    49               ' alt="hitcounts chart">') % data
    50 
    51     return result
    52 
    53 
    54 def get_data(pagename, request, filterpage=None):
    55     cache_days, cache_views, cache_edits = [], [], []
    56     cache_date = 0
    57 
    58     # Get results from cache
    59     if filterpage:
    60         arena = Page(request, pagename)
    61         cache = caching.CacheEntry(request, arena, 'hitcounts', scope='item', use_pickle=True)
    62     else:
    63         arena = 'charts'
    64         cache = caching.CacheEntry(request, arena, 'hitcounts', scope='wiki', use_pickle=True)
    65 
    66     if cache.exists():
    67         try:
    68             cache_date, cache_days, cache_views, cache_edits = cache.content()
    69         except:
    70             cache.remove() # cache gone bad
    71 
    72     # Get new results from the log
    73     log = eventlog.EventLog(request)
    74     try:
    75         new_date = log.date()
    76     except logfile.LogMissing:
    77         new_date = None
    78 
    79     # prepare data
    80     days = []
    81     views = []
    82     edits = []
    83     ratchet_day = None
    84     ratchet_time = None
    85     if new_date is not None:
    86         log.set_filter(['VIEWPAGE', 'SAVEPAGE'])
    87         latest = None
    88         for event in log.reverse():
    89             # don't use event_log.date()
    90             if latest is None:
    91                 latest = event[0]
    92             event_usecs = event[0]
    93             if event_usecs <= cache_date:
    94                 break
    95             eventpage = event[2].get('pagename', '')
    96             if filterpage and eventpage != filterpage:
    97                 continue
    98             event_secs = wikiutil.version2timestamp(event_usecs)
    99             time_tuple = time.gmtime(event_secs) # must be UTC
   100             day = tuple(time_tuple[0:3])
   101             if day != ratchet_day:
   102                 # new day
   103                 while ratchet_time:
   104                     ratchet_time -= 86400 # seconds per day
   105                     rday = tuple(time.gmtime(ratchet_time)[0:3]) # must be UTC
   106                     if rday <= day:
   107                         break
   108                     days.append(DATE_FMT % rday)
   109                     views.append(0)
   110                     edits.append(0)
   111                 days.append(DATE_FMT % day)
   112                 views.append(0)
   113                 edits.append(0)
   114                 ratchet_day = day
   115                 ratchet_time = event_secs
   116             if event[1] == 'VIEWPAGE':
   117                 views[-1] += 1
   118             elif event[1] == 'SAVEPAGE':
   119                 edits[-1] += 1
   120 
   121         days.reverse()
   122         views.reverse()
   123         edits.reverse()
   124 
   125     # merge the day on the end of the cache
   126     if cache_days and days and days[0] == cache_days[-1]:
   127         cache_edits[-1] += edits[0]
   128         cache_views[-1] += views[0]
   129         days, views, edits = days[1:], views[1:], edits[1:]
   130 
   131     # Update and save the cache
   132     cache_days.extend(days)
   133     cache_views.extend(views)
   134     cache_edits.extend(edits)
   135     if new_date is not None:
   136         cache.update((latest, cache_days, cache_views, cache_edits))
   137 
   138     return cache_days, cache_views, cache_edits
   139 
   140 
   141 def text(pagename, request, params=''):
   142     from MoinMoin.util.dataset import TupleDataset, Column
   143     from MoinMoin.widget.browser import DataBrowserWidget
   144     _ = request.getText
   145 
   146     # check params
   147     filterpage = None
   148     if params.startswith('page='):
   149         filterpage = wikiutil.url_unquote(params[len('page='):])
   150 
   151     if request and request.values and 'page' in request.values:
   152         filterpage = request.values['page']
   153 
   154     days, views, edits = get_data(pagename, request, filterpage)
   155 
   156     hits = TupleDataset()
   157     hits.columns = [Column('day', label=_("Date"), align='left'),
   158                     Column('views', label=_("Views/day"), align='right'),
   159                     Column('edits', label=_("Edits/day"), align='right'),
   160                     ]
   161 
   162     maxentries = 30
   163 
   164     if maxentries < len(days):
   165         step = float(len(days))/ maxentries
   166     else:
   167         step = 1
   168 
   169     sv = 0.0
   170     se = 0.0
   171     sd = 0.0
   172     cnt = 0
   173 
   174     for i in xrange(len(days)-1, -1, -1):
   175         d, v, e = days[i], views[i], edits[i]
   176         # sum up views and edits to step days
   177         sd += 1
   178         cnt += 1
   179         sv += v
   180         se += e
   181         if cnt >= step:
   182             cnt -= step
   183             hits.addRow((d, "%.1f" % (sv/sd), "%.1f" % (se/sd)))
   184             sv = 0.0
   185             se = 0.0
   186             sd = 0.0
   187 
   188     table = DataBrowserWidget(request)
   189     table.setData(hits)
   190     return table.render(method="GET")
   191 
   192 
   193 def draw(pagename, request):
   194     import shutil, cStringIO
   195     from MoinMoin.stats.chart import Chart, ChartData, Color
   196 
   197     _ = request.getText
   198 
   199     # check params
   200     filterpage = None
   201     if request and request.values and 'page' in request.values:
   202         filterpage = request.values['page']
   203 
   204     days, views, edits = get_data(pagename, request, filterpage)
   205 
   206     import math
   207 
   208     try:
   209         scalefactor = float(max(views))/max(edits)
   210     except (ZeroDivisionError, ValueError):
   211         scalefactor = 1.0
   212     else:
   213         scalefactor = int(10 ** math.floor(math.log10(scalefactor)))
   214 
   215     # scale edits up
   216     edits = [x * scalefactor for x in edits]
   217 
   218     # create image
   219     image = cStringIO.StringIO()
   220     c = Chart()
   221     c.addData(ChartData(views, color='green'))
   222     c.addData(ChartData(edits, color='red'))
   223     chart_title = ''
   224     if request.cfg.sitename:
   225         chart_title = "%s: " % request.cfg.sitename
   226     chart_title = chart_title + _('Page hits and edits')
   227     if filterpage:
   228         chart_title = _("%(chart_title)s for %(filterpage)s") % {
   229             'chart_title': chart_title,
   230             'filterpage': filterpage,
   231         }
   232     chart_title = "%s\n%sx%d" % (chart_title, _("green=view\nred=edit"), scalefactor)
   233     c.option(
   234         title=chart_title.encode('iso-8859-1', 'replace'), # gdchart can't do utf-8
   235         xtitle=(_('date') + ' (Server)').encode('iso-8859-1', 'replace'),
   236         ytitle=_('# of hits').encode('iso-8859-1', 'replace'),
   237         title_font=c.GDC_GIANT,
   238         #thumblabel = 'THUMB', thumbnail = 1, thumbval = 10,
   239         #ytitle_color = Color('green'),
   240         #yaxis2 = 1,
   241         #ytitle2 = '# of edits',
   242         #ytitle2_color = Color('red'),
   243         #ylabel2_color = Color('black'),
   244         #interpolations = 0,
   245         threed_depth=1.0,
   246         requested_yinterval=1.0,
   247         stack_type=c.GDC_STACK_BESIDE
   248     )
   249     c.draw(c.GDC_LINE,
   250         (request.cfg.chart_options['width'], request.cfg.chart_options['height']),
   251         image, days)
   252 
   253     request.content_type = 'image/gif'
   254     request.content_length = len(image.getvalue())
   255 
   256     # copy the image
   257     image.reset()
   258     shutil.copyfileobj(image, request, 8192)
   259