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