1from distutils.version import StrictVersion 2from datetime import datetime 3from markupsafe import Markup 4from flask import current_app 5 6# //cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment-with-locales.min.js 7default_moment_version = '2.29.1' 8default_moment_sri = ('sha512-LGXaggshOkD/at6PFNcp2V2unf9LzFq6LE+sChH7ceMTDP0' 9 'g2kn6Vxwgg7wkPP7AAtX+lmPqPdxB47A0Nz0cMQ==') 10 11 12class moment(object): 13 """Create a moment object. 14 15 :param timestamp: The ``datetime`` object representing the timestamp. 16 :param local: If ``True``, the ``timestamp`` argument is given in the 17 local client time. In most cases this argument will be set 18 to ``False`` and all the timestamps managed by the server 19 will be in the UTC timezone. 20 """ 21 @staticmethod 22 def include_moment(version=default_moment_version, local_js=None, 23 no_js=None, sri=None, with_locales=True): 24 """Include the moment.js library and the supporting JavaScript code 25 used by this extension. 26 27 This function must be called in the ``<head>`` section of the Jinja 28 template(s) that use this extension. 29 30 :param version: The version of moment.js to include. 31 :param local_js: The URL to import the moment.js library from. Use this 32 option to import the library from a locally hosted 33 file. 34 :param no_js: Just add the supporting code for this extension, without 35 importing the moment.js library. . Use this option if 36 the library is imported elsewhere in the template. The 37 supporting JavaScript code for this extension is still 38 included. 39 :param sri: The SRI hash to use when importing the moment.js library, 40 or ``None`` if the SRI hash is unknown or disabled. 41 :param with_locales: If ``True``, include the version of moment.js that 42 has all the locales. 43 """ 44 js = '' 45 if version == default_moment_version and local_js is None and \ 46 sri is None: 47 sri = default_moment_sri 48 if not no_js: 49 if local_js is not None: 50 if not sri: 51 js = '<script src="{}"></script>\n'.format(local_js) 52 else: 53 js = ('<script src="{}" integrity="{}" ' 54 'crossorigin="anonymous"></script>\n').format( 55 local_js, sri) 56 elif version is not None: 57 if with_locales: 58 js_filename = 'moment-with-locales.min.js' \ 59 if StrictVersion(version) >= StrictVersion('2.8.0') \ 60 else 'moment-with-langs.min.js' 61 else: 62 js_filename = 'moment.min.js' 63 64 if not sri: 65 js = ('<script src="https://cdnjs.cloudflare.com/ajax/' 66 'libs/moment.js/{}/{}"></script>\n').format( 67 version, js_filename) 68 else: 69 js = ('<script src="https://cdnjs.cloudflare.com/ajax/' 70 'libs/moment.js/{}/{}" integrity="{}" ' 71 'crossorigin="anonymous"></script>\n').format( 72 version, js_filename, sri) 73 74 default_format = '' 75 if 'MOMENT_DEFAULT_FORMAT' in current_app.config: 76 default_format = '\nmoment.defaultFormat = "{}";'.format( 77 current_app.config['MOMENT_DEFAULT_FORMAT']) 78 return Markup('''{}<script> 79moment.locale("en");{} 80function flask_moment_render(elem) {{ 81 const timestamp = moment(elem.dataset.timestamp); 82 const func = elem.dataset.function; 83 const format = elem.dataset.format; 84 const timestamp2 = elem.dataset.timestamp2; 85 const no_suffix = elem.dataset.nosuffix; 86 const units = elem.dataset.units; 87 let args = []; 88 if (format) 89 args.push(format); 90 if (timestamp2) 91 args.push(moment(timestamp2)); 92 if (no_suffix) 93 args.push(no_suffix); 94 if (units) 95 args.push(units); 96 elem.textContent = timestamp[func].apply(timestamp, args); 97 elem.classList.remove('flask-moment'); 98 elem.style.display = ""; 99}} 100function flask_moment_render_all() {{ 101 const moments = document.querySelectorAll('.flask-moment'); 102 moments.forEach(function(moment) {{ 103 flask_moment_render(moment); 104 const refresh = moment.dataset.refresh; 105 if (refresh && refresh > 0) {{ 106 (function(elem, interval) {{ 107 setInterval(function() {{ 108 flask_moment_render(elem); 109 }}, interval); 110 }})(moment, refresh); 111 }} 112 }}) 113}} 114document.addEventListener("DOMContentLoaded", flask_moment_render_all); 115</script>'''.format(js, default_format)) # noqa: E501 116 117 @staticmethod 118 def locale(language='en', auto_detect=False, customization=None): 119 """Configure the moment.js locale. 120 121 :param language: The language code. 122 :param auto_detect: If ``True``, detect the locale from the browser. 123 :param customization: A dictionary with custom options for the locale, 124 as needed by the moment.js library. 125 """ 126 if auto_detect: 127 return Markup('<script>\nvar locale = ' 128 'window.navigator.userLanguage || ' 129 'window.navigator.language;\n' 130 'moment.locale(locale);\n</script>') 131 if customization: 132 return Markup( 133 '<script>\nmoment.locale("{}", {});\n</script>'.format( 134 language, customization)) 135 return Markup( 136 '<script>\nmoment.locale("{}");\n</script>'.format(language)) 137 138 @staticmethod 139 def lang(language): 140 """Set the language. This is a simpler version of the :func:`locale` 141 function. 142 143 :param language: The language code to use. 144 """ 145 return moment.locale(language) 146 147 def __init__(self, timestamp=None, local=False): 148 if timestamp is None: 149 timestamp = datetime.utcnow() 150 self.timestamp = timestamp 151 self.local = local 152 153 def _timestamp_as_iso_8601(self, timestamp): 154 tz = '' 155 if not self.local: 156 tz = 'Z' 157 return timestamp.strftime('%Y-%m-%dT%H:%M:%S' + tz) 158 159 def _render(self, func, format=None, timestamp2=None, no_suffix=None, 160 units=None, refresh=False): 161 t = self._timestamp_as_iso_8601(self.timestamp) 162 data_values = 'data-function="{}"'.format(func) 163 if format: 164 data_values += ' data-format="{}"'.format(format) 165 if timestamp2: 166 data_values += ' data-timestamp2="{}"'.format(timestamp2) 167 if no_suffix: 168 data_values += ' data-nosuffix="1"' 169 if units: 170 data_values += ' data-units="{}"'.format(units) 171 return Markup(('<span class="flask-moment" data-timestamp="{}" ' + 172 '{} data-refresh="{}" ' + 173 'style="display: none">{}</span>').format( 174 t, data_values, int(refresh) * 60000, t)) 175 176 def format(self, fmt=None, refresh=False): 177 """Format a moment object with a custom formatting string. 178 179 :param fmt: The formatting specification to use, as documented by the 180 ``format()`` function frommoment.js. 181 :param refresh: If set to ``True``, refresh the timestamp at one 182 minute intervals. If set to ``False``, background 183 refreshing is disabled. If set to an integer, the 184 refresh occurs at the indicated interval, given in 185 minutes. 186 """ 187 return self._render("format", format=(fmt or ''), refresh=refresh) 188 189 def fromNow(self, no_suffix=False, refresh=False): 190 """Render the moment object as a relative time. 191 192 This formatting option is often called "time ago", since it renders 193 the timestamp using friendly text strings such as "2 hours ago" or 194 "in 3 weeks". 195 196 :param no_suffix: if set to ``True``, the time difference does not 197 include the suffix (the "ago" or similar). 198 :param refresh: If set to ``True``, refresh the timestamp at one 199 minute intervals. If set to ``False``, background 200 refreshing is disabled. If set to an integer, the 201 refresh occurs at the indicated interval, given in 202 minutes. 203 """ 204 return self._render("fromNow", no_suffix=int(no_suffix), 205 refresh=refresh) 206 207 def fromTime(self, timestamp, no_suffix=False, refresh=False): 208 """Render the moment object as a relative time with respect to a 209 given reference time. 210 211 This function maps to the ``from()`` function from moment.js. 212 213 :param timestamp: The reference ``datetime`` object. 214 :param no_suffix: if set to ``True``, the time difference does not 215 include the suffix (the "ago" or similar). 216 :param refresh: If set to ``True``, refresh the timestamp at one 217 minute intervals. If set to ``False``, background 218 refreshing is disabled. If set to an integer, the 219 refresh occurs at the indicated interval, given in 220 minutes. 221 """ 222 return self._render("from", timestamp2=self._timestamp_as_iso_8601( 223 timestamp), no_suffix=int(no_suffix), refresh=refresh) 224 225 def toNow(self, no_suffix=False, refresh=False): 226 """Render the moment object as a relative time. 227 228 This function renders as the reverse time interval of ``fromNow()``. 229 230 :param no_suffix: if set to ``True``, the time difference does not 231 include the suffix (the "ago" or similar). 232 :param refresh: If set to ``True``, refresh the timestamp at one 233 minute intervals. If set to ``False``, background 234 refreshing is disabled. If set to an integer, the 235 refresh occurs at the indicated interval, given in 236 minutes. 237 """ 238 return self._render("toNow", no_suffix=int(no_suffix), refresh=refresh) 239 240 def toTime(self, timestamp, no_suffix=False, refresh=False): 241 """Render the moment object as a relative time with respect to a 242 given reference time. 243 244 This function maps to the ``to()`` function from moment.js. 245 246 :param timestamp: The reference ``datetime`` object. 247 :param no_suffix: if set to ``True``, the time difference does not 248 include the suffix (the "ago" or similar). 249 :param refresh: If set to ``True``, refresh the timestamp at one 250 minute intervals. If set to ``False``, background 251 refreshing is disabled. If set to an integer, the 252 refresh occurs at the indicated interval, given in 253 minutes. 254 """ 255 return self._render("to", timestamp2=self._timestamp_as_iso_8601( 256 timestamp), no_suffix=int(no_suffix), refresh=refresh) 257 258 def calendar(self, refresh=False): 259 """Render the moment object as a relative time, either to current time 260 or a given reference timestamp. 261 262 This function renders relative time using day references such as 263 tomorrow, next Sunday, etc. 264 265 :param refresh: If set to ``True``, refresh the timestamp at one 266 minute intervals. If set to ``False``, background 267 refreshing is disabled. If set to an integer, the 268 refresh occurs at the indicated interval, given in 269 minutes. 270 """ 271 return self._render("calendar", refresh=refresh) 272 273 def valueOf(self, refresh=False): 274 """Render the moment object as milliseconds from Unix Epoch. 275 276 :param refresh: If set to ``True``, refresh the timestamp at one 277 minute intervals. If set to ``False``, background 278 refreshing is disabled. If set to an integer, the 279 refresh occurs at the indicated interval, given in 280 minutes. 281 """ 282 return self._render("valueOf", refresh=refresh) 283 284 def unix(self, refresh=False): 285 """Render the moment object as seconds from Unix Epoch. 286 287 :param refresh: If set to ``True``, refresh the timestamp at one 288 minute intervals. If set to ``False``, background 289 refreshing is disabled. If set to an integer, the 290 refresh occurs at the indicated interval, given in 291 minutes. 292 """ 293 return self._render("unix", refresh=refresh) 294 295 def diff(self, timestamp, units, refresh=False): 296 """Render the difference between the moment object and the given 297 timestamp using the provided units. 298 299 :param timestamp: The reference ``datetime`` object. 300 :param units: A time unit such as `years`, `months`, `weeks`, `days`, 301 `hours`, `minutes` or `seconds`. 302 :param refresh: If set to ``True``, refresh the timestamp at one 303 minute intervals. If set to ``False``, background 304 refreshing is disabled. If set to an integer, the 305 refresh occurs at the indicated interval, given in 306 minutes. 307 """ 308 return self._render("diff", timestamp2=self._timestamp_as_iso_8601( 309 timestamp), units=units, refresh=refresh) 310 311 312class Moment(object): 313 def __init__(self, app=None): 314 if app is not None: 315 self.init_app(app) 316 317 def init_app(self, app): 318 if not hasattr(app, 'extensions'): # pragma: no cover 319 app.extensions = {} 320 app.extensions['moment'] = moment 321 app.context_processor(self.context_processor) 322 323 @staticmethod 324 def context_processor(): 325 return { 326 'moment': current_app.extensions['moment'] 327 } 328 329 def create(self, timestamp=None): 330 return current_app.extensions['moment'](timestamp) 331