|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard packages |
|
17 from datetime import datetime |
|
18 |
|
19 # import interfaces |
|
20 from pyams_utils.interfaces.tales import ITALESExtension |
|
21 from zope.dublincore.interfaces import IZopeDublinCore |
|
22 |
|
23 # import packages |
|
24 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter |
|
25 from pyams_utils.request import check_request |
|
26 from pyams_utils.timezone import gmtime, tztime |
|
27 from zope.datetime import parseDatetimetz |
|
28 from zope.interface import Interface |
|
29 |
|
30 from pyams_utils import _ |
|
31 |
|
32 |
|
33 def unidate(value): |
|
34 """Get specified date converted to unicode ISO format |
|
35 |
|
36 Dates are always assumed to be stored in GMT timezone |
|
37 |
|
38 :param date value: input date to convert to unicode |
|
39 :return: unicode; input date converted to unicode |
|
40 |
|
41 >>> from datetime import datetime |
|
42 >>> from pyams_utils.date import unidate |
|
43 >>> value = datetime(2016, 11, 15, 10, 13, 12) |
|
44 >>> unidate(value) |
|
45 '2016-11-15T10:13:12+00:00' |
|
46 """ |
|
47 if value is not None: |
|
48 value = gmtime(value) |
|
49 return value.isoformat('T') |
|
50 return None |
|
51 |
|
52 |
|
53 def parse_date(value): |
|
54 """Get date specified in unicode ISO format to Python datetime object |
|
55 |
|
56 Dates are always assumed to be stored in GMT timezone |
|
57 |
|
58 :param str value: unicode date to be parsed |
|
59 :return: datetime; the specified value, converted to datetime |
|
60 |
|
61 >>> from pyams_utils.date import parse_date |
|
62 >>> parse_date('2016-11-15T10:13:12+00:00') |
|
63 datetime.datetime(2016, 11, 15, 10, 13, 12, tzinfo=<StaticTzInfo 'GMT'>) |
|
64 """ |
|
65 if value is not None: |
|
66 return gmtime(parseDatetimetz(value)) |
|
67 return None |
|
68 |
|
69 |
|
70 def date_to_datetime(value): |
|
71 """Get datetime value converted from a date or datetime object |
|
72 |
|
73 :param date/datetime value: a date or datetime value to convert |
|
74 :return: datetime; input value converted to datetime |
|
75 |
|
76 >>> from datetime import date, datetime |
|
77 >>> from pyams_utils.date import date_to_datetime |
|
78 >>> value = date(2016, 11, 15) |
|
79 >>> date_to_datetime(value) |
|
80 datetime.datetime(2016, 11, 15, 0, 0) |
|
81 >>> value = datetime(2016, 11, 15, 10, 13, 12) |
|
82 >>> value |
|
83 datetime.datetime(2016, 11, 15, 10, 13, 12) |
|
84 >>> date_to_datetime(value) is value |
|
85 True |
|
86 """ |
|
87 if not value: |
|
88 return None |
|
89 if type(value) is datetime: |
|
90 return value |
|
91 return datetime(value.year, value.month, value.day) |
|
92 |
|
93 |
|
94 SH_DATE_FORMAT = _("%d/%m/%Y") |
|
95 SH_DATETIME_FORMAT = _("%d/%m/%Y - %H:%M") |
|
96 |
|
97 EXT_DATE_FORMAT = _("on %d/%m/%Y") |
|
98 EXT_DATETIME_FORMAT = _("on %d/%m/%Y at %H:%M") |
|
99 |
|
100 |
|
101 def format_date(value, format=EXT_DATE_FORMAT, request=None): |
|
102 """Format given date with the given format |
|
103 |
|
104 :param datetime value: the value to format |
|
105 :param str format: a format string to use by `strftime` function |
|
106 :param request: the request from which to extract localization info for translation |
|
107 :return: str; input datetime converted to given format |
|
108 |
|
109 >>> from datetime import datetime |
|
110 >>> from pyams_utils.date import format_date, SH_DATE_FORMAT |
|
111 >>> value = datetime(2016, 11, 15, 10, 13, 12) |
|
112 >>> format_date(value) |
|
113 'on 15/11/2016' |
|
114 >>> format_date(value, SH_DATE_FORMAT) |
|
115 '15/11/2016' |
|
116 """ |
|
117 if not value: |
|
118 return '--' |
|
119 if request is None: |
|
120 request = check_request() |
|
121 localizer = request.localizer |
|
122 return datetime.strftime(tztime(value), localizer.translate(format)) |
|
123 |
|
124 |
|
125 def format_datetime(value, format=EXT_DATETIME_FORMAT, request=None): |
|
126 """Format given datetime with the given format including time |
|
127 |
|
128 :param datetime value: the value to format |
|
129 :param str format: a format string to use by `strftime` function |
|
130 :param request: request; the request from which to extract localization info for translation |
|
131 :return: str; input datetime converted to given format |
|
132 |
|
133 >>> from datetime import datetime |
|
134 >>> from pyams_utils.date import format_datetime, SH_DATETIME_FORMAT |
|
135 >>> value = datetime(2016, 11, 15, 10, 13, 12) |
|
136 >>> format_datetime(value) |
|
137 'on 15/11/2016 at 10:13' |
|
138 >>> format_datetime(value, SH_DATETIME_FORMAT) |
|
139 '15/11/2016 - 10:13' |
|
140 """ |
|
141 return format_date(value, format, request) |
|
142 |
|
143 |
|
144 def get_age(value, request=None): |
|
145 """Get 'human' age of a given datetime (including timezone) compared to current datetime (in UTC) |
|
146 |
|
147 :param datetime value: input datetime to be compared with current datetime |
|
148 :return: str; the delta value, converted to months, weeks, days, hours or minutes |
|
149 """ |
|
150 if request is None: |
|
151 request = check_request() |
|
152 translate = request.localizer.translate |
|
153 now = gmtime(datetime.utcnow()) |
|
154 delta = now - gmtime(value) |
|
155 if delta.days > 60: |
|
156 return translate(_("%d months ago")) % int(round(delta.days * 1.0 / 30)) |
|
157 elif delta.days > 10: |
|
158 return translate(_("%d weeks ago")) % int(round(delta.days * 1.0 / 7)) |
|
159 elif delta.days > 2: |
|
160 return translate(_("%d days ago")) % delta.days |
|
161 elif delta.days == 2: |
|
162 return translate(_("the day before yesterday")) |
|
163 elif delta.days == 1: |
|
164 return translate(_("yesterday")) |
|
165 else: |
|
166 hours = int(round(delta.seconds * 1.0 / 3600)) |
|
167 if hours > 1: |
|
168 return translate(_("%d hours ago")) % hours |
|
169 elif delta.seconds > 300: |
|
170 return translate(_("%d minutes ago")) % int(round(delta.seconds * 1.0 / 60)) |
|
171 else: |
|
172 return translate(_("less than 5 minutes ago")) |
|
173 |
|
174 |
|
175 def get_duration(v1, v2=None, request=None): |
|
176 """Get 'human' delta as string between two dates |
|
177 |
|
178 :param datetime v1: start date |
|
179 :param datetime v2: end date, or current date (in UTC) if None |
|
180 :param request: the request from which to extract localization infos |
|
181 :return: str; approximate delta between the two input dates |
|
182 |
|
183 >>> from datetime import datetime |
|
184 >>> from pyams_utils.date import get_duration |
|
185 >>> from pyramid.testing import DummyRequest |
|
186 >>> request = DummyRequest() |
|
187 >>> date1 = datetime(2015, 1, 1) |
|
188 >>> date2 = datetime(2014, 3, 1) |
|
189 >>> get_duration(date1, date2, request) |
|
190 '10 months' |
|
191 |
|
192 Dates order is not important: |
|
193 |
|
194 >>> get_duration(date2, date1, request) |
|
195 '10 months' |
|
196 >>> date2 = datetime(2014, 11, 10) |
|
197 >>> get_duration(date1, date2, request) |
|
198 '7 weeks' |
|
199 >>> date2 = datetime(2014, 12, 26) |
|
200 >>> get_duration(date1, date2, request) |
|
201 '6 days' |
|
202 |
|
203 For durations lower than 2 days, duration also display hours: |
|
204 |
|
205 >>> date1 = datetime(2015, 1, 1) |
|
206 >>> date2 = datetime(2015, 1, 2, 15, 10, 0) |
|
207 >>> get_duration(date1, date2, request) |
|
208 '1 day and 15 hours' |
|
209 >>> date2 = datetime(2015, 1, 2) |
|
210 >>> get_duration(date1, date2, request) |
|
211 '24 hours' |
|
212 >>> date2 = datetime(2015, 1, 1, 13, 12) |
|
213 >>> get_duration(date1, date2, request) |
|
214 '13 hours' |
|
215 >>> date2 = datetime(2015, 1, 1, 1, 15) |
|
216 >>> get_duration(date1, date2, request) |
|
217 '75 minutes' |
|
218 >>> date2 = datetime(2015, 1, 1, 0, 0, 15) |
|
219 >>> get_duration(date1, date2, request) |
|
220 '15 seconds' |
|
221 """ |
|
222 if v2 is None: |
|
223 v2 = datetime.utcnow() |
|
224 assert isinstance(v1, datetime) and isinstance(v2, datetime) |
|
225 if request is None: |
|
226 request = check_request() |
|
227 translate = request.localizer.translate |
|
228 v1, v2 = min(v1, v2), max(v1, v2) |
|
229 delta = v2 - v1 |
|
230 if delta.days > 60: |
|
231 return translate(_("%d months")) % int(round(delta.days * 1.0 / 30)) |
|
232 elif delta.days > 10: |
|
233 return translate(_("%d weeks")) % int(round(delta.days * 1.0 / 7)) |
|
234 elif delta.days >= 2: |
|
235 return translate(_("%d days")) % delta.days |
|
236 else: |
|
237 hours = int(round(delta.seconds * 1.0 / 3600)) |
|
238 if delta.days == 1: |
|
239 if hours == 0: |
|
240 return translate(_("24 hours")) |
|
241 else: |
|
242 return translate(_("%d day and %d hours")) % (delta.days, hours) |
|
243 else: |
|
244 if hours > 2: |
|
245 return translate(_("%d hours")) % hours |
|
246 else: |
|
247 minutes = int(round(delta.seconds * 1.0 / 60)) |
|
248 if minutes > 2: |
|
249 return translate(_("%d minutes")) % minutes |
|
250 else: |
|
251 return translate(_("%d seconds")) % delta.seconds |
|
252 |
|
253 |
|
254 @adapter_config(name='timestamp', context=(Interface, Interface, Interface), provides=ITALESExtension) |
|
255 class TimestampTalesAdapter(ContextRequestViewAdapter): |
|
256 """extension:timestamp(context) TALES adapter |
|
257 |
|
258 A PyAMS TALES extension to get timestamp based on last context modification date. |
|
259 """ |
|
260 |
|
261 def render(self, context=None, formatting=None): |
|
262 if context is None: |
|
263 context = self.request.context |
|
264 if formatting == 'iso': |
|
265 format_func = datetime.isoformat |
|
266 else: |
|
267 format_func = datetime.timestamp |
|
268 dc = IZopeDublinCore(context, None) |
|
269 if dc is None: |
|
270 return format_func(datetime.utcnow()) |
|
271 else: |
|
272 return format_func(dc.modified) |