Ticket #394: timesince.patch
File timesince.patch, 10.7 KB (added by , 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 125 125 comment=comment.id, 126 126 user=media.get_uploader.username, 127 127 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> 129 131 </a>: 130 132 </div> 131 133 <div class="comment_content"> … … 141 143 {% endif %} 142 144 </div> 143 145 <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) -%} 145 147 <h3>Added on</h3> 146 <p> {{ date }}</p>148 <p><span title="{{ date }}">{{ formatted_time }}</span></p> 147 149 {%- endtrans %} 148 150 {% if media.tags %} 149 151 {% 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 17 from datetime import datetime, timedelta 18 19 from mediagoblin.tools.timesince import is_aware, timesince 20 21 22 def 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 29 29 from mediagoblin.tools import common 30 30 from mediagoblin.tools.translate import get_gettext_translation 31 31 from mediagoblin.tools.pluginapi import get_hook_templates 32 from mediagoblin.tools.timesince import timesince 32 33 from mediagoblin.meddleware.csrf import render_csrf_form_token 33 34 34 35 36 35 37 SETUP_JINJA_ENVS = {} 36 38 37 39 … … def get_jinja_env(template_loader, locale): 73 75 74 76 template_env.filters['urlencode'] = url_quote_plus 75 77 78 # add human readable fuzzy date time 79 template_env.globals['timesince'] = timesince 80 76 81 # allow for hooking up plugin templates 77 82 template_env.globals['get_hook_templates'] = get_hook_templates 78 83 -
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 29 from __future__ import unicode_literals 30 31 import datetime 32 import pytz 33 34 from mediagoblin.tools.translate import pass_to_ugettext, lazy_pass_to_ungettext as _ 35 36 """UTC time zone as a tzinfo instance.""" 37 utc = pytz.utc if pytz else UTC() 38 39 def 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 48 def 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): 123 123 *args, **kwargs) 124 124 125 125 126 def 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 126 136 def lazy_pass_to_ugettext(*args, **kwargs): 127 137 """ 128 138 Lazily pass to ugettext. … … def lazy_pass_to_ngettext(*args, **kwargs): 158 168 """ 159 169 return LazyProxy(pass_to_ngettext, *args, **kwargs) 160 170 171 def 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 161 181 162 182 def fake_ugettext_passthrough(string): 163 183 """ -
setup.py
diff --git a/setup.py b/setup.py index a98cd01..ce1e410 100644
a b setup( 61 61 'sqlalchemy-migrate', 62 62 'mock', 63 63 'itsdangerous', 64 'pytz', 64 65 ## This is optional! 65 66 # 'translitcodec', 66 67 ## For now we're expecting that users will install this from