Ticket #394: timesince.patch

File timesince.patch, 10.7 KB (added by Jessica Tallon, 12 years ago)
  • mediagoblin/templates/mediagoblin/user_pages/media.html

    diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
    index b77c12b..58b9cdc 100644
    a b  
    125125                            comment=comment.id,
    126126                            user=media.get_uploader.username,
    127127                            media=media.slug_or_id) }}#comment">
    128               {{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}
     128              <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'>
     129                {{ timesince(comment.created) }}
     130              </span>
    129131            </a>:
    130132          </div>
    131133          <div class="comment_content">
     
    141143    {% endif %}
    142144  </div>
    143145  <div class="media_sidebar">
    144     {% trans date=media.created.strftime("%Y-%m-%d") -%}
     146    {% trans date=media.created.strftime("%Y-%m-%d"), formatted_time=timesince(media.created) -%}
    145147      <h3>Added on</h3>
    146       <p>{{ date }}</p>
     148      <p><span title="{{ date }}">{{ formatted_time }}</span></p>
    147149    {%- endtrans %}
    148150    {% if media.tags %}
    149151      {% include "mediagoblin/utils/tags.html" %}
  • new file mediagoblin/tests/test_timesince.py

    diff --git a/mediagoblin/tests/test_timesince.py b/mediagoblin/tests/test_timesince.py
    new file mode 100644
    index 0000000..1f8a082
    - +  
     1# GNU MediaGoblin -- federated, autonomous media hosting
     2# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
     3#
     4# This program is free software: you can redistribute it and/or modify
     5# it under the terms of the GNU Affero General Public License as published by
     6# the Free Software Foundation, either version 3 of the License, or
     7# (at your option) any later version.
     8#
     9# This program is distributed in the hope that it will be useful,
     10# but WITHOUT ANY WARRANTY; without even the implied warranty of
     11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12# GNU Affero General Public License for more details.
     13#
     14# You should have received a copy of the GNU Affero General Public License
     15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16
     17from datetime import datetime, timedelta
     18
     19from mediagoblin.tools.timesince import is_aware, timesince
     20
     21
     22def test_timesince(test_app):
     23        test_time = datetime.now()
     24
     25        # it should ignore second and microseconds
     26        assert timesince(test_time, test_time + timedelta(microseconds=1)) == "0 minutes"
     27        assert timesince(test_time, test_time + timedelta(seconds=1)) == "0 minutes"
     28
     29        # test minutes, hours, days, weeks, months and years (singular and plural)
     30        assert timesince(test_time, test_time + timedelta(minutes=1)) == "1 minute"
     31        assert timesince(test_time, test_time + timedelta(minutes=2)) == "2 minutes"
     32
     33        assert timesince(test_time, test_time + timedelta(hours=1)) == "1 hour"
     34        assert timesince(test_time, test_time + timedelta(hours=2)) == "2 hours"
     35
     36        assert timesince(test_time, test_time + timedelta(days=1)) == "1 day"
     37        assert timesince(test_time, test_time + timedelta(days=2)) == "2 days"
     38
     39        assert timesince(test_time, test_time + timedelta(days=7)) == "1 week"
     40        assert timesince(test_time, test_time + timedelta(days=14)) == "2 weeks"
     41
     42        assert timesince(test_time, test_time + timedelta(days=30)) == "1 month"
     43        assert timesince(test_time, test_time + timedelta(days=60)) == "2 months"
     44
     45        assert timesince(test_time, test_time + timedelta(days=365)) == "1 year"
     46        assert timesince(test_time, test_time + timedelta(days=730)) == "2 years"
     47
     48        # okay now we want to test combinations
     49        #       e.g. 1 hour, 5 days
     50        assert timesince(test_time, test_time + timedelta(days=5, hours=1)) == "5 days, 1 hour"
     51
     52        assert timesince(test_time, test_time + timedelta(days=15)) == "2 weeks, 1 day"
     53
     54        assert timesince(test_time, test_time + timedelta(days=97)) == "3 months, 1 week"
     55
     56        assert timesince(test_time, test_time + timedelta(days=2250)) == "6 years, 2 months"
     57
  • mediagoblin/tools/template.py

    diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
    index 74d811e..78d6565 100644
    a b from mediagoblin import _version  
    2929from mediagoblin.tools import common
    3030from mediagoblin.tools.translate import get_gettext_translation
    3131from mediagoblin.tools.pluginapi import get_hook_templates
     32from mediagoblin.tools.timesince import timesince
    3233from mediagoblin.meddleware.csrf import render_csrf_form_token
    3334
    3435
     36
    3537SETUP_JINJA_ENVS = {}
    3638
    3739
    def get_jinja_env(template_loader, locale):  
    7375
    7476    template_env.filters['urlencode'] = url_quote_plus
    7577
     78    # add human readable fuzzy date time
     79    template_env.globals['timesince'] = timesince
     80
    7681    # allow for hooking up plugin templates
    7782    template_env.globals['get_hook_templates'] = get_hook_templates
    7883
  • new file mediagoblin/tools/timesince.py

    diff --git a/mediagoblin/tools/timesince.py b/mediagoblin/tools/timesince.py
    new file mode 100644
    index 0000000..b761c1b
    - +  
     1# Copyright (c) Django Software Foundation and individual contributors.
     2# All rights reserved.
     3#
     4# Redistribution and use in source and binary forms, with or without modification,
     5# are permitted provided that the following conditions are met:
     6#
     7#    1. Redistributions of source code must retain the above copyright notice,
     8#       this list of conditions and the following disclaimer.
     9#   
     10#    2. Redistributions in binary form must reproduce the above copyright
     11#       notice, this list of conditions and the following disclaimer in the
     12#       documentation and/or other materials provided with the distribution.
     13#
     14#    3. Neither the name of Django nor the names of its contributors may be used
     15#       to endorse or promote products derived from this software without
     16#       specific prior written permission.
     17#
     18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
     19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
     22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
     25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28
     29from __future__ import unicode_literals
     30
     31import datetime
     32import pytz
     33
     34from mediagoblin.tools.translate import pass_to_ugettext, lazy_pass_to_ungettext as _
     35
     36"""UTC time zone as a tzinfo instance."""
     37utc = pytz.utc if pytz else UTC()
     38
     39def is_aware(value):
     40    """
     41    Determines if a given datetime.datetime is aware.
     42
     43    The logic is described in Python's docs:
     44    http://docs.python.org/library/datetime.html#datetime.tzinfo
     45    """
     46    return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None
     47
     48def timesince(d, now=None, reversed=False):
     49    """
     50    Takes two datetime objects and returns the time between d and now
     51    as a nicely formatted string, e.g. "10 minutes".  If d occurs after now,
     52    then "0 minutes" is returned.
     53
     54    Units used are years, months, weeks, days, hours, and minutes.
     55    Seconds and microseconds are ignored.  Up to two adjacent units will be
     56    displayed.  For example, "2 weeks, 3 days" and "1 year, 3 months" are
     57    possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not.
     58
     59    Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
     60    """
     61    chunks = (
     62      (60 * 60 * 24 * 365, lambda n: _('year', 'years', n)),
     63      (60 * 60 * 24 * 30, lambda n: _('month', 'months', n)),
     64      (60 * 60 * 24 * 7, lambda n : _('week', 'weeks', n)),
     65      (60 * 60 * 24, lambda n : _('day', 'days', n)),
     66      (60 * 60, lambda n: _('hour', 'hours', n)),
     67      (60, lambda n: _('minute', 'minutes', n))
     68    )
     69    # Convert datetime.date to datetime.datetime for comparison.
     70    if not isinstance(d, datetime.datetime):
     71        d = datetime.datetime(d.year, d.month, d.day)
     72    if now and not isinstance(now, datetime.datetime):
     73        now = datetime.datetime(now.year, now.month, now.day)
     74
     75    if not now:
     76        now = datetime.datetime.now(utc if is_aware(d) else None)
     77
     78    delta = (d - now) if reversed else (now - d)
     79    # ignore microseconds
     80    since = delta.days * 24 * 60 * 60 + delta.seconds
     81    if since <= 0:
     82        # d is in the future compared to now, stop processing.
     83        return '0 ' + pass_to_ugettext('minutes')
     84    for i, (seconds, name) in enumerate(chunks):
     85        count = since // seconds
     86        if count != 0:
     87            break
     88    s = pass_to_ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)}
     89    if i + 1 < len(chunks):
     90        # Now get the second item
     91        seconds2, name2 = chunks[i + 1]
     92        count2 = (since - (seconds * count)) // seconds2
     93        if count2 != 0:
     94            s += pass_to_ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)}
     95    return s
  • mediagoblin/tools/translate.py

    diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py
    index 1d37c4d..4acafac 100644
    a b def pass_to_ugettext(*args, **kwargs):  
    123123        *args, **kwargs)
    124124
    125125
     126def pass_to_ungettext(*args, **kwargs):
     127    """
     128    Pass a translation on to the appropriate ungettext method.
     129
     130    The reason we can't have a global ugettext method is because
     131    mg_globals gets swapped out by the application per-request.
     132    """
     133    return mg_globals.thread_scope.translations.ungettext(
     134        *args, **kwargs)
     135
    126136def lazy_pass_to_ugettext(*args, **kwargs):
    127137    """
    128138    Lazily pass to ugettext.
    def lazy_pass_to_ngettext(*args, **kwargs):  
    158168    """
    159169    return LazyProxy(pass_to_ngettext, *args, **kwargs)
    160170
     171def lazy_pass_to_ungettext(*args, **kwargs):
     172    """
     173    Lazily pass to ungettext.
     174
     175    This is useful if you have to define a translation on a module
     176    level but you need it to not translate until the time that it's
     177    used as a string.
     178    """
     179    return LazyProxy(pass_to_ungettext, *args, **kwargs)
     180
    161181
    162182def fake_ugettext_passthrough(string):
    163183    """
  • setup.py

    diff --git a/setup.py b/setup.py
    index a98cd01..ce1e410 100644
    a b setup(  
    6161        'sqlalchemy-migrate',
    6262        'mock',
    6363        'itsdangerous',
     64        'pytz',
    6465        ## This is optional!
    6566        # 'translitcodec',
    6667        ## For now we're expecting that users will install this from