├── docs ├── _static │ └── css │ │ └── custom.css ├── api.rst ├── index.rst ├── Makefile ├── make.bat ├── quickstart.rst ├── conf.py └── advanced.rst ├── MANIFEST.in ├── example ├── requirements.txt ├── app.py └── templates │ └── index.html ├── .readthedocs.yaml ├── .gitignore ├── README.md ├── tox.ini ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ └── tests.yml ├── CHANGES.md ├── tests └── test_flask_moment.py └── src └── flask_moment └── __init__.py /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .py .class, .py .method, .py .property { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ------------- 3 | 4 | .. autoclass:: flask_moment.moment 5 | :members: 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE tox.ini 2 | recursive-include docs * 3 | recursive-exclude docs/_build * 4 | recursive-include tests * 5 | exclude **/*.pyc 6 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.0.1 2 | Flask==2.0.1 3 | Flask-Moment 4 | importlib-metadata==4.5.0 5 | itsdangerous==2.0.1 6 | Jinja2==3.0.1 7 | MarkupSafe==2.0.1 8 | pkg-resources==0.0.0 9 | typing-extensions==3.10.0.0 10 | Werkzeug==2.2.3 11 | zipp==3.4.1 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask-Moment documentation master file, created by 2 | sphinx-quickstart on Wed Jun 9 23:10:03 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Flask-Moment 7 | ============ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | quickstart 13 | advanced 14 | api 15 | 16 | * :ref:`genindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | .cache 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | venv 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask-Moment 2 | ============ 3 | 4 | [![Build status](https://github.com/miguelgrinberg/flask-moment/workflows/build/badge.svg)](https://github.com/miguelgrinberg/flask-moment/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/flask-moment/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/flask-moment) 5 | 6 | This extension enhances Jinja2 templates with formatting of dates and times 7 | using [moment.js](https://momentjs.com/). 8 | 9 | Resources 10 | --------- 11 | 12 | - [Documentation](http://flask-moment.readthedocs.io/en/latest/) 13 | - [PyPI](https://pypi.python.org/pypi/flask-moment) 14 | - [Change Log](https://github.com/miguelgrinberg/flask-moment/blob/main/CHANGES.md) 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=flake8,py38,py39,py310,py311,py312,pypy3,docs 3 | skip_missing_interpreters=true 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39 10 | 3.10: py310 11 | 3.11: py311 12 | pypy-3: pypy3 13 | 14 | [testenv] 15 | commands= 16 | pip install -e . 17 | pytest -p no:logging --cov=flask_moment --cov-branch --cov-report term-missing --cov-report=xml 18 | deps= 19 | pytest 20 | pytest-cov 21 | 22 | [testenv:flake8] 23 | commands= 24 | flake8 src/flask_moment tests example 25 | deps= 26 | flake8 27 | 28 | [testenv:docs] 29 | changedir=docs 30 | deps= 31 | sphinx 32 | allowlist_externals= 33 | make 34 | commands= 35 | make html 36 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from flask import Flask, render_template, jsonify 3 | from flask_moment import Moment 4 | 5 | app = Flask(__name__) 6 | moment = Moment(app) 7 | 8 | 9 | @app.route('/') 10 | def index(): 11 | now = datetime.utcnow() 12 | midnight = datetime(now.year, now.month, now.day, 0, 0, 0) 13 | epoch = datetime(1970, 1, 1, 0, 0, 0) 14 | next_saturday = now + timedelta(5 - now.weekday()) 15 | return render_template('index.html', now=now, midnight=midnight, 16 | epoch=epoch, next_saturday=next_saturday) 17 | 18 | 19 | @app.route('/ajax') 20 | def ajax(): 21 | return jsonify({'timestamp': moment.create(datetime.utcnow()).format( 22 | 'LLLL')}) 23 | 24 | 25 | if __name__ == '__main__': 26 | app.run(debug=True) 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Flask-Moment" 3 | version = "1.0.7.dev0" 4 | authors = [ 5 | { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, 6 | ] 7 | description = "Formatting of dates and times in Flask templates using moment.js." 8 | classifiers = [ 9 | "Intended Audience :: Developers", 10 | "Programming Language :: Python :: 3", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | ] 14 | requires-python = ">=3.6" 15 | dependencies = [ 16 | "Flask", 17 | "packaging >=14.1", 18 | ] 19 | 20 | [project.readme] 21 | file = "README.md" 22 | content-type = "text/markdown" 23 | 24 | [project.urls] 25 | Homepage = "https://github.com/miguelgrinberg/flask-moment" 26 | "Bug Tracker" = "https://github.com/miguelgrinberg/flask-moment/issues" 27 | 28 | [project.optional-dependencies] 29 | docs = [ 30 | "sphinx", 31 | ] 32 | 33 | [tool.setuptools] 34 | zip-safe = false 35 | include-package-data = true 36 | 37 | [tool.setuptools.package-dir] 38 | "" = "src" 39 | 40 | [tool.setuptools.packages.find] 41 | where = [ 42 | "src", 43 | ] 44 | namespaces = false 45 | 46 | [build-system] 47 | requires = [ 48 | "setuptools>=61.2", 49 | ] 50 | build-backend = "setuptools.build_meta" 51 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-python@v3 16 | - run: python -m pip install --upgrade pip wheel 17 | - run: pip install tox tox-gh-actions 18 | - run: tox -eflake8 19 | tests: 20 | name: tests 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.10'] 25 | fail-fast: false 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-python@v3 30 | with: 31 | python-version: ${{ matrix.python }} 32 | - run: python -m pip install --upgrade pip wheel 33 | - run: pip install tox tox-gh-actions 34 | - run: tox 35 | coverage: 36 | name: coverage 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: actions/setup-python@v3 41 | - run: python -m pip install --upgrade pip wheel 42 | - run: pip install tox tox-gh-actions 43 | - run: tox 44 | - uses: codecov/codecov-action@v3 45 | with: 46 | files: ./coverage.xml 47 | fail_ci_if_error: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flask-Moment example app 4 | {{ moment.include_moment() }} 5 | 20 | 21 | 22 |

The current date and time is: {{ moment().format('MMMM Do YYYY, h:mm:ss a') }}.

23 |

24 | {{ moment(midnight).fromTime(now) }} it was midnight in the UTC timezone. 25 | That was {{ moment(midnight).calendar() }} in your local time. 26 | It will be midnight again {{ moment(midnight).toNow() }}. 27 |

28 |

29 | Unix epoch is {{ moment(epoch, local=True).format('LLLL') }} in the UTC timezone. 30 | That was {{ moment(epoch).format('LLLL') }} in your local time. 31 |

32 |

33 | This page was rendered on {{ moment(now).format('LLL') }}, 34 | which was {{ moment(now).fromNow(refresh = True) }}. 35 |

36 |

37 | The next Saturday will be in {{ moment(now).toTime(next_saturday) }}, from the time this page was rendered. 38 |

39 |

40 | Time difference between now and next Saturday: {{ moment(next_saturday).diff(now, 'days') }} days 41 | or {{ moment(next_saturday).diff(now, 'hours') }} hours. 42 |

43 |
44 | Load Ajax timestamp 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | ----------- 3 | 4 | Installation and configuration 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Install the extension with ``pip``:: 8 | 9 | 10 | pip install flask-moment 11 | 12 | Once the extension is installed, create an instance and initialize it with the 13 | Flask application:: 14 | 15 | from flask_moment import Moment 16 | moment = Moment(app) 17 | 18 | If the application uses the `application factory pattern `_, 19 | the two-step initialization method can be used instead:: 20 | 21 | moment = Moment() 22 | 23 | def create_app(): 24 | app = Flask(__name__) 25 | moment.init_app(app) 26 | return app 27 | 28 | app = create_app(prod_config) 29 | 30 | To complete the initialization, add the following line inside the ```` 31 | section of the template(s) that will use the extension:: 32 | 33 | {{ moment.include_moment() }} 34 | 35 | If you use template inheritance, the best place to add this line is in your 36 | base template. 37 | 38 | Note that older versions of this extension required jQuery, but this isn't the 39 | case anymore. The ``include_jquery()`` function that existed in older releases 40 | has now been removed. 41 | 42 | The ``include_moment()`` function accepts a few arguments that control settings 43 | such as the version of ``moment.js`` to use, or whether the library should be 44 | imported from a CDN. 45 | 46 | Rendering timestamps with Flask-Moment 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | To render a date with Flask-Moment, use the ``moment()`` function with one of 50 | the available formatting options. The following example uses a custom format 51 | string to render the current time:: 52 | 53 |

Current date and time: {{ moment().format('MMMM Do YYYY, h:mm:ss a') }}.

54 | 55 | To render a timestamp other than the current time, pass a ``datetime`` object 56 | as an argument to the ``moment()`` function:: 57 | 58 |

Date: {{ moment(date).format('LL') }}

59 | 60 | Some of the rendering functions render the elapsed time between a datetime 61 | object and current time. The following example renders a timestamp in a 62 | "time ago" style:: 63 | 64 |

{{ author }} said {{ moment(comment_datetime).fromNow() }}

65 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../src')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Flask-Moment' 21 | copyright = '2021, Miguel Grinberg' 22 | author = 'Miguel Grinberg' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 41 | 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = 'alabaster' 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] 54 | 55 | html_css_files = [ 56 | 'css/custom.css', 57 | ] 58 | 59 | html_theme_options = { 60 | 'github_user': 'miguelgrinberg', 61 | 'github_repo': 'flask-moment', 62 | 'github_banner': True, 63 | 'github_button': True, 64 | 'github_type': 'star', 65 | 'fixed_sidebar': True, 66 | } 67 | 68 | autodoc_default_options = { 69 | 'member-order': 'bysource', 70 | } 71 | 72 | add_module_names = False 73 | -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced options 2 | ---------------- 3 | 4 | Auto-Refresh 5 | ~~~~~~~~~~~~ 6 | 7 | All the display functions take an optional ``refresh`` argument that when set 8 | to ``True`` will re-render timestamps every minute. If a different time 9 | interval is desired, pass the interval in minutes as the value of the 10 | ``refresh`` argument. 11 | 12 | This option is useful for relative time formats such as the one returned by 13 | the ``fromNow()`` or ``fromTime()`` functions. For example, the date of a 14 | comment would appear as "a few seconds ago" when the comment was just entered, 15 | but a minute later it would automatically refresh to "a minute ago" without 16 | the need to refresh the page. 17 | 18 | By default automatic refreshing is disabled. 19 | 20 | Default Format 21 | ~~~~~~~~~~~~~~ 22 | 23 | The ``format()`` function can be invoked without arguments, in which case a 24 | default format of ISO8601 defined by the moment.js library is used. If you 25 | want to set a different default, you can set the ``MOMENT_DEFAULT_FORMAT`` 26 | variable in the Flask configuration. Consult the 27 | `moment.js format documentation `_ 28 | for a list of accepted tokens. 29 | 30 | Internationalization 31 | ~~~~~~~~~~~~~~~~~~~~ 32 | 33 | By default dates and times are rendered in English. To change to a different 34 | language add the following line in the ```` section of your template(s), 35 | after the ``include_moment()`` line:: 36 | 37 | {{ moment.locale("es") }} 38 | 39 | The above example sets the language to Spanish. Moment.js supports a large 40 | number of languages. Consult the documentation for the list of languages and 41 | their corresponding two letter codes. 42 | 43 | This extension also supports auto-detection of the client's browser language:: 44 | 45 | {{ moment.locale(auto_detect=True) }} 46 | 47 | Custom locales can also be included as a dictionary:: 48 | 49 | {{ moment.locale(customizations={ ... }) }} 50 | 51 | See the `moment.js locale customizations `_ 52 | documentation for details on how to define a custom locale. 53 | 54 | Ajax Support 55 | ~~~~~~~~~~~~ 56 | 57 | It is also possible to create Flask-Moment timestamps in Python code that is 58 | invoked from the client page through Ajax, without the use of Jinja templates:: 59 | 60 | 61 | timestamp = moment.create(datetime.utcnow()).calendar() 62 | 63 | The ``moment`` variable is the ``Moment`` instance that was created at 64 | initialization time. 65 | 66 | A timestamp created in this way is an HTML string that can be returned as part 67 | of a response. For example, here is how a timestamp can be returned in a JSON 68 | object:: 69 | 70 | return { 'timestamp': moment.create(datetime.utcnow()).format('L') } 71 | 72 | The Ajax callback in the browser needs to call ``flask_moment_render_all()`` 73 | each time an element containing a timestamp is added to the DOM. The example 74 | application in the Flask-Moment GitHub repository demonstrates how this is 75 | done. 76 | 77 | Subresource Integrity (SRI) 78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | `SRI `_ 81 | is a security feature that enables browsers to verify that resources they 82 | fetch are not maliciously manipulated. To do so a cryptographic hash is 83 | provided that proves integrity. 84 | 85 | SRI for the moment.js library is enabled by default. If you wish to use 86 | a version different than the one bundled with this extension, or want to host 87 | your own javascript, a `separate hash `_ 88 | can be provided. 89 | 90 | Just add ``sri=`` when calling ``moment.include_moment()``. If no 91 | SRI hash is provided when a custom moment.js version is used, then SRI 92 | verification is not used. 93 | 94 | To disable SRI, pass ``sri=False`` in the ``include_moment()`` call. 95 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Flask-Moment change log 2 | 3 | **Release 1.0.6** - 2024-05-28 4 | 5 | - Remove use of deprecated `datetime.utcnow()` function ([commit](https://github.com/miguelgrinberg/flask-moment/commit/3216125c9bcf8efa4b6c33746af20a49a84ddcab)) 6 | - Support timestamps passed as ISO 8601 strings [#94](https://github.com/miguelgrinberg/flask-moment/issues/94) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e9663bbeb810f0b37da1d210fbfc91e7327a3f2e)) 7 | 8 | **Release 1.0.5** - 2022-10-05 9 | 10 | - Upgrade moment.js to version 2.29.4 [#89](https://github.com/miguelgrinberg/flask-moment/issues/89) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/4483b94442ce165b7b83afbed9ed9a9a513658bc)) (thanks **Ezequiel Parziale**!) 11 | - Fix documentation typos [#88](https://github.com/miguelgrinberg/flask-moment/issues/88) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/3fda3bc3640d57fc105c69e2ad6ef8af7aa430c0)) (thanks **Tim Gates**!) 12 | 13 | **Release 1.0.4** - 2022-07-17 14 | 15 | - Add packaging as a dependency [#86](https://github.com/miguelgrinberg/flask-moment/issues/86) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e7bc9332eff0e78c9312100ad8540e2cdda600a9)) (thanks **Chuxiao Feng**!) 16 | 17 | **Release 1.0.3** - 2022-07-16 18 | 19 | - Return the raw JavaScript code to enable unsupported use cases [#84](https://github.com/miguelgrinberg/flask-moment/issues/84) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/c79961d46f3b69c7ffc217c2bb35f8289e54b8c4)) 20 | - Remove deprecated `StrictVersion` usage [#85](https://github.com/miguelgrinberg/flask-moment/issues/85) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/02a8392876f84c439f548fe639a8890aef0ede97)) 21 | 22 | **Release 1.0.2** - 2021-07-16 23 | 24 | - High CPU usage when refresh is disabled [#81](https://github.com/miguelgrinberg/flask-moment/issues/81) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/7ffce2fea01c25d72b4417b408b6cca5119b8a3e)) 25 | 26 | **Release 1.0.1** - 2021-06-11 27 | 28 | - Fix package metadata for release [#80](https://github.com/miguelgrinberg/flask-moment/issues/80) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/19d601e87eae4c0bd7d34f4734de46b931e8b1b2)) 29 | 30 | **Release 1.0.0** - 2021-06-11 31 | 32 | - Remove dependency on jQuery ([commit](https://github.com/miguelgrinberg/flask-moment/commit/3ab78505303ca5ecf1de1324893918f3b541f2d6)) (thanks **yuxiaoy**!) 33 | - Add `diff()` function [#78](https://github.com/miguelgrinberg/flask-moment/issues/78) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/79d8f43a6f0d31ce9a81d39954bb3f2f08923028)) (thanks **valerii**!) 34 | - Import Markup from markupsafe [#71](https://github.com/miguelgrinberg/flask-moment/issues/71) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/b1c48c41d6783585096003dd22bfd0d121ea306e)) (thanks **jn**!) 35 | - Fix Markup in unit tests [#72](https://github.com/miguelgrinberg/flask-moment/issues/72) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/7566c6b6c2d09ca3b06e3116dd7b5f35ce37b83e)) (thanks **Valerii Tryhubov**!) 36 | - Sphinx documentation ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e9afd81a602cbf1962af307a6d5b5556ee827381)) 37 | - Update requirements for example application ([commit](https://github.com/miguelgrinberg/flask-moment/commit/1cdc1f16eed3ad945d2774b925a4f9e2f2a54644)) 38 | - Improved project structure ([commit](https://github.com/miguelgrinberg/flask-moment/commit/f79481c84eae62d7d0c8606bfa235f5b223111cf)) 39 | - Update links in documentation [#79](https://github.com/miguelgrinberg/flask-moment/issues/79) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/335d3b3cea648f0eb90137b1cdd3840b7cb737ee)) (thanks **Frank Yu**!) 40 | - Unit test reorganization ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e29403f78b326b70445ef4659b42aea29de67604)) 41 | - GitHub Actions builds ([commit](https://github.com/miguelgrinberg/flask-moment/commit/1886b76330fd9d1f07985f588ecc5ceaeec5c9fc)) 42 | - Fix documentation typos [#74](https://github.com/miguelgrinberg/flask-moment/issues/74) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/13a35e259292666210445018ea72e6d1b0579486)) (thanks **yuxiaoy**!) 43 | 44 | **Release 0.11.0** - 2020-12-17 45 | 46 | - Update default moment version and hash [#67](https://github.com/miguelgrinberg/flask-moment/issues/67) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/f4ceefbdab6c9dcf1449127e7cdf7a3aa0049da3)) (thanks **Irakliy Krasnov**!) 47 | - Always use `https://` to request the JavaScript files from the CDN [#65](https://github.com/miguelgrinberg/flask-moment/issues/65) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/a8f42fce6b1f50b73694830975a29bb30d95efdd)) (thanks **Vicki Jackson**!) 48 | - Update moment.js and add option to use Moment.js without locales [#63](https://github.com/miguelgrinberg/flask-moment/issues/63) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/5472239042d674809fc62dd3112d52f8154d3ec9)) (thanks **Steven van de Graaf**!) 49 | 50 | **Release 0.10.0** - 2020-06-10 51 | 52 | - Various code and test improvements, also add python 3.7 and 3.8 to build ([commit](https://github.com/miguelgrinberg/flask-moment/commit/842185179d2b89f895281b0b4077a8eabeba6f83)) 53 | - Remove use of JavaScript eval[#60](https://github.com/miguelgrinberg/flask-moment/issues/60) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/5e453fc0e9a0947662ceaa0fca50cab090ca3811)) (thanks **Emilien Klein**!) 54 | - Docs: Fix simple typo, acepted -> accepted [#58](https://github.com/miguelgrinberg/flask-moment/issues/58) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e506753ffe9d0b5200173f714c0294754b685699)) (thanks **Tim Gates**!) 55 | - Add `toTime` and `toNow` to display functions [#57](https://github.com/miguelgrinberg/flask-moment/issues/57) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/13ba7ee3b0ab8d0e7aa774781ef3b3ec33cad9ce)) (thanks **Mohamed Feddad**!) 56 | 57 | **Release 0.9.0** - 2019-08-04 58 | 59 | - Updated requirements in example application ([commit](https://github.com/miguelgrinberg/flask-moment/commit/28a5fdbcffcd8b6ed03d9e67bdbf31328b57e2d6)) 60 | - Add support of customization object for the method moment.locale [#50](https://github.com/miguelgrinberg/flask-moment/issues/50) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/5b274132604f6e2d8dd5b4c7adb221b17c42c817)) (thanks **Aleksandr**!) 61 | 62 | **Release 0.8.0** - 2019-06-16 63 | 64 | - Default format [#48](https://github.com/miguelgrinberg/flask-moment/issues/48) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/8786663286806668eab3683458aa3390d505484b)) (thanks **jacobthetechy**!) 65 | - Simplify tests and handling of js versions ([commit](https://github.com/miguelgrinberg/flask-moment/commit/1b273c445957ea507ee23926dfa17be111b74fd7)) 66 | - Add SRI v2.0 [#42](https://github.com/miguelgrinberg/flask-moment/issues/42) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/d87eaf8e485757a871928e5248d07a586abfd5fc)) (thanks **M0r13n**!) 67 | 68 | **Release 0.7.0** - 2019-02-16 69 | 70 | - Added `no_js` argument to `include_moment` ([commit](https://github.com/miguelgrinberg/flask-moment/commit/d99f9ba7c393fa120afff10a9bded11d09a303d5)) 71 | - Fix license declaration in setup.py ([commit](https://github.com/miguelgrinberg/flask-moment/commit/243d8e0d523e4f060a7c2cf1ce7b8135bdcefaf1)) 72 | 73 | **Release 0.6.0** - 2017-12-28 74 | 75 | - Fix travis build ([commit](https://github.com/miguelgrinberg/flask-moment/commit/d0468b770d2cbdedf92e051053793c186eb9f6ba)) 76 | - Add auto-detect locale support [#33](https://github.com/miguelgrinberg/flask-moment/issues/33) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/32e18b6fc6594ba34281cef04356a5d24bfdf5a2)) (thanks **Grey Li**!) 77 | - Fixed unit tests ([commit](https://github.com/miguelgrinberg/flask-moment/commit/6503a2f53cccba498df9675f3ebceaae01dc5c1c)) 78 | 79 | **Release 0.5.2** - 2017-09-29 80 | 81 | - Bump moment.js version to 2.18.1 ([commit](https://github.com/miguelgrinberg/flask-moment/commit/c412f13e1235ab4ec5f07fd01a8dcf56b9a7ad51)) 82 | - Travis build badge ([commit](https://github.com/miguelgrinberg/flask-moment/commit/bbd622383b63892702f337772e45f1443bf610c7)) 83 | - A few unit test fixes, plus tox/travis builds ([commit](https://github.com/miguelgrinberg/flask-moment/commit/71f46e56ff60d2ae66f189909de3544d7aca35ec)) 84 | - Add tests for the basic features [#28](https://github.com/miguelgrinberg/flask-moment/issues/28) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e908e2befa456f4fd886845e52060b643b3ecf04)) (thanks **Kyle Lawlor**!) 85 | - Added example of how to setup Flask-Moment using a Flask app factory ([commit](https://github.com/miguelgrinberg/flask-moment/commit/5a27fff48864d7233b05936b54edbc3212f0938d)) (thanks **Jeff Widman**!) 86 | - Switched imports from deprecated `flask.ext.extensionname` format to `flask_extensionname` ([commit](https://github.com/miguelgrinberg/flask-moment/commit/32f8efc9079a5e7eedf05abba72d42ffc806d4c3)) (thanks **Jeff Widman**!) 87 | 88 | **Release 0.5.1** - 2015-08-06 89 | 90 | - Hide timestamps until rendering is complete [#16](https://github.com/miguelgrinberg/flask-moment/issues/16) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/a5d312f789949e352845dd1f767e98a8e428b3eb)) (thanks **Cole Kettler**!) 91 | - Upgrading the default version to the latest moment.js release(2.10.3) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/56102d5b53c8d2b95b3684661517cc0cb2a6c713)) (thanks **small-yellow-rice**!) 92 | 93 | **Release 0.4.0** 94 | 95 | - Added `local_js` option to `include_moment()` and `include_jquery()` ([commit](https://github.com/miguelgrinberg/flask-moment/commit/8bd3035ec4d234ac7617e22e46efc06936cd0db2)) 96 | - pep8 fixes ([commit](https://github.com/miguelgrinberg/flask-moment/commit/ff63a40e97c91a1432101125c6b26cc40f2b62d2)) 97 | 98 | **Release 0.3.3** 99 | 100 | - Correct fix for [#8](https://github.com/miguelgrinberg/flask-moment/issues/8) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/0d3dcd8a550bb7961d3a1469ed80b24bcf277466)) 101 | 102 | **Release 0.3.2** 103 | 104 | - Correct `include_moment(version=None)` handling [#8](https://github.com/miguelgrinberg/flask-moment/issues/8) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/e59dae390e4fccb30ef761ff18e29803fe3d9e57)) 105 | 106 | **Release 0.3.1** 107 | 108 | - Fixed https support ([commit](https://github.com/miguelgrinberg/flask-moment/commit/1ccf8aec18f3325eb9d74468f904cd10b9c31f27)) (thanks **Tal Shiri**!) 109 | 110 | **Release 0.3** 111 | 112 | - Added "local" argument to `moment()` constructor to disable conversion from UTC to local time ([commit](https://github.com/miguelgrinberg/flask-moment/commit/3afcbc6290494402b420a0a98bae2be94a7565f1)) 113 | - Use secure URLs when request is secure [#2](https://github.com/miguelgrinberg/flask-moment/issues/2) ([commit](https://github.com/miguelgrinberg/flask-moment/commit/0ee69da4408171168c6c0fe84d6f437ab4e8031f)) 114 | 115 | **Release 0.2** 116 | 117 | - First public version 118 | -------------------------------------------------------------------------------- /tests/test_flask_moment.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import unittest 3 | from unittest import mock 4 | 5 | from flask import Flask, render_template_string 6 | from markupsafe import Markup 7 | 8 | from flask_moment import Moment, default_moment_version, default_moment_sri 9 | 10 | 11 | class TestMoment(unittest.TestCase): 12 | def setUp(self): 13 | self.app = Flask(__name__) 14 | self.moment_app = Moment(self.app) 15 | self.appctx = self.app.app_context() 16 | self.moment = self.app.extensions['moment'] 17 | self.appctx.push() 18 | 19 | def tearDown(self): 20 | self.appctx.pop() 21 | 22 | def test_init(self): 23 | assert self.app.extensions['moment'].__name__ == 'moment' 24 | assert self.app.template_context_processors[None][1].__globals__[ 25 | '__name__'] == 'flask_moment' 26 | 27 | def test_init_app(self): 28 | app = Flask(__name__) 29 | moment = Moment() 30 | moment.init_app(app) 31 | assert app.extensions['moment'].__name__ == 'moment' 32 | assert app.template_context_processors[None][1].__globals__[ 33 | '__name__'] == 'flask_moment' 34 | 35 | def test_include_moment_directly(self): 36 | include_moment = self.moment.include_moment() 37 | assert isinstance(include_moment, Markup) 38 | assert '' in str( 53 | include_moment) 54 | 55 | def test_include_moment_renders_properly(self): 56 | ts = str(render_template_string('{{ moment.include_moment() }}')) 57 | assert ' 0 136 | 137 | def test__render_refresh(self): 138 | m = self.moment() 139 | rts = m._render(func='format', refresh=True) 140 | assert isinstance(rts, Markup) 141 | assert rts.find('data-timestamp="') > 0 142 | assert rts.find('data-function="format" data-refresh="60000"') > 0 143 | 144 | def test_format_default(self): 145 | m = self.moment() 146 | rts = m.format('this-format-please') 147 | assert rts.find( 148 | 'data-format="this-format-please" data-refresh="0"') > 0 149 | 150 | def test_fromNow_default(self): 151 | m = self.moment() 152 | rts = m.fromNow() 153 | assert rts.find('data-function="fromNow" data-refresh="0"') > 0 154 | 155 | def test_fromNow_no_suffix(self): 156 | m = self.moment() 157 | rts = m.fromNow(no_suffix=True) 158 | assert rts.find('data-function="fromNow" data-nosuffix="1" ' 159 | 'data-refresh="0"') > 0 160 | 161 | def test_fromTime_default(self): 162 | m = self.moment() 163 | ts = datetime(2017, 1, 15, 22, 47, 6, 479898) 164 | rts = m.fromTime(timestamp=ts) 165 | assert rts.find('data-function="from" data-timestamp2="{}" ' 166 | 'data-refresh="0"'.format( 167 | m._timestamp_as_iso_8601(ts))) > 0 168 | assert rts.find(m._timestamp_as_iso_8601( 169 | timestamp=m.timestamp)) > 0 170 | 171 | def test_fromTime_no_suffix(self): 172 | m = self.moment() 173 | ts = datetime(2017, 1, 15, 22, 47, 6, 479898) 174 | rts = m.fromTime(timestamp=ts, no_suffix=True) 175 | assert rts.find('data-function="from" data-timestamp2="{}" ' 176 | 'data-nosuffix="1" data-refresh="0"'.format( 177 | m._timestamp_as_iso_8601(ts))) > 0 178 | assert rts.find(m._timestamp_as_iso_8601( 179 | timestamp=m.timestamp)) > 0 180 | 181 | def test_toNow_default(self): 182 | m = self.moment() 183 | rts = m.toNow() 184 | assert rts.find('data-function="toNow" data-refresh="0"') > 0 185 | 186 | def test_toNow_no_suffix(self): 187 | m = self.moment() 188 | no_suffix = True 189 | rts = m.toNow(no_suffix=no_suffix) 190 | assert rts.find('data-function="toNow" data-nosuffix="1" ' 191 | 'data-refresh="0"') > 0 192 | 193 | def test_toTime_default(self): 194 | m = self.moment() 195 | ts = datetime(2020, 1, 15, 22, 47, 6, 479898) 196 | rts = m.toTime(timestamp=ts) 197 | assert rts.find('data-function="to" data-timestamp2="{}" ' 198 | 'data-refresh="0"'.format( 199 | m._timestamp_as_iso_8601(ts))) > 0 200 | assert rts.find(m._timestamp_as_iso_8601( 201 | timestamp=m.timestamp)) > 0 202 | 203 | def test_toTime_no_suffix(self): 204 | m = self.moment() 205 | ts = datetime(2020, 1, 15, 22, 47, 6, 479898) 206 | no_suffix = True 207 | rts = m.toTime(timestamp=ts, no_suffix=no_suffix) 208 | assert rts.find('data-function="to" data-timestamp2="{}" ' 209 | 'data-nosuffix="1" data-refresh="0"'.format( 210 | m._timestamp_as_iso_8601(ts))) > 0 211 | assert rts.find(m._timestamp_as_iso_8601( 212 | timestamp=m.timestamp)) > 0 213 | 214 | def test_calendar_default(self): 215 | m = self.moment() 216 | rts = m.calendar() 217 | assert rts.find('data-function="calendar" data-refresh="0"') > 0 218 | 219 | def test_valueOf_default(self): 220 | m = self.moment() 221 | rts = m.valueOf() 222 | assert rts.find('data-function="valueOf" data-refresh="0"') > 0 223 | 224 | def test_unix_default(self): 225 | m = self.moment() 226 | rts = m.unix() 227 | assert rts.find('data-function="unix" data-refresh="0"') > 0 228 | 229 | def test_diff_days(self): 230 | m = self.moment() 231 | ts = datetime(2020, 1, 15, 22, 47, 6, 479898) 232 | rts = m.diff(ts, 'days') 233 | assert rts.find( 234 | 'data-function="diff" data-timestamp2="{}" data-units="days" ' 235 | 'data-refresh="0"'.format( 236 | m._timestamp_as_iso_8601(ts))) > 0 237 | assert rts.find(m._timestamp_as_iso_8601( 238 | timestamp=m.timestamp)) > 0 239 | 240 | def test_diff_hours(self): 241 | m = self.moment() 242 | ts = datetime(2020, 1, 15, 22, 47, 6, 479898) 243 | rts = m.diff(ts, 'hours') 244 | assert rts.find( 245 | 'data-function="diff" data-timestamp2="{}" data-units="hours" ' 246 | 'data-refresh="0"'.format( 247 | m._timestamp_as_iso_8601(ts))) > 0 248 | assert rts.find(m._timestamp_as_iso_8601( 249 | timestamp=m.timestamp)) > 0 250 | 251 | @mock.patch('flask_moment._naive_now') 252 | def test_create_default_no_timestamp(self, now): 253 | ts = datetime(2017, 1, 15, 22, 1, 21, 101361) 254 | now.return_value = ts 255 | moment = Moment() 256 | moment.init_app(self.app) 257 | assert moment.create().timestamp == ts 258 | 259 | def test_create_default_with_timestamp(self): 260 | moment = Moment() 261 | moment.init_app(self.app) 262 | ts = datetime(2017, 1, 15, 22, 47, 6, 479898) 263 | assert moment.create(timestamp=ts).timestamp == ts 264 | 265 | def test_moment_with_non_default_versions(self): 266 | include_moment = None 267 | 268 | def _check_assertions(): 269 | assert 'src=\"' in include_moment 270 | assert 'integrity=\"' not in include_moment 271 | assert 'crossorigin\"' not in include_moment 272 | 273 | include_moment = self.moment.include_moment(version='2.8.0') 274 | _check_assertions() 275 | include_moment = self.moment.include_moment(version='2.3.1') 276 | _check_assertions() 277 | include_moment = self.moment.include_moment(version='2.16.8') 278 | _check_assertions() 279 | include_moment = self.moment.include_moment(version='2.30.1') 280 | _check_assertions() 281 | 282 | def test_moment_with_default_version(self): 283 | include_moment = self.moment.include_moment() 284 | assert include_moment.startswith( 285 | ''.format( 288 | default_moment_version, default_moment_sri)) 289 | 290 | def test_moment_from_cdn_with_custom_sri_hash(self): 291 | include_moment = self.moment.include_moment(sri='sha384-12345678') 292 | assert include_moment.startswith( 293 | ''.format(default_moment_version)) 296 | 297 | include_moment = self.moment.include_moment(version='2.0.0', 298 | sri='sha384-12345678') 299 | assert include_moment.startswith( 300 | '') 303 | 304 | def test_moment_local(self): 305 | include_moment = self.moment.include_moment(local_js=True) 306 | assert 'src=\"' in include_moment 307 | assert 'integrity=\"' not in include_moment 308 | assert 'crossorigin\"' not in include_moment 309 | 310 | def test_moment_local_with_sri(self): 311 | include_moment = self.moment.include_moment(local_js=True, 312 | sri='sha384-87654321') 313 | assert 'src=\"' in include_moment 314 | assert 'integrity=\"sha384-87654321\"' in include_moment 315 | assert 'crossorigin=\"anonymous\"' in include_moment 316 | 317 | def test_disabling_moment_default(self): 318 | include_moment = self.moment.include_moment(sri=False) 319 | assert 'src=\"' in include_moment 320 | assert 'integrity=\"' not in include_moment 321 | assert 'crossorigin' not in include_moment 322 | 323 | def test_disabling_moment_custom(self): 324 | include_moment = self.moment.include_moment(local_js=True, sri=False) 325 | assert 'src=\"' in include_moment 326 | assert 'integrity=\"' not in include_moment 327 | assert 'crossorigin' not in include_moment 328 | 329 | def test_disabling_moment_custom_version(self): 330 | include_moment = self.moment.include_moment(version='2.17.9', 331 | sri=False) 332 | assert 'src=\"' in include_moment 333 | assert 'integrity=\"' not in include_moment 334 | assert 'crossorigin' not in include_moment 335 | -------------------------------------------------------------------------------- /src/flask_moment/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from packaging.version import parse as version_parse 3 | from markupsafe import Markup 4 | from flask import current_app 5 | 6 | # //cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment-with-locales.min.js 7 | default_moment_version = '2.29.4' 8 | default_moment_sri = ('sha512-42PE0rd+wZ2hNXftlM78BSehIGzezNeQuzihiBCvUEB3CVx' 9 | 'HvsShF86wBWwQORNxNINlBPuq7rG4WWhNiTVHFg==') 10 | 11 | js_code = '''function flask_moment_render(elem) {{ 12 | const timestamp = moment(elem.dataset.timestamp); 13 | const func = elem.dataset.function; 14 | const format = elem.dataset.format; 15 | const timestamp2 = elem.dataset.timestamp2; 16 | const no_suffix = elem.dataset.nosuffix; 17 | const units = elem.dataset.units; 18 | let args = []; 19 | if (format) 20 | args.push(format); 21 | if (timestamp2) 22 | args.push(moment(timestamp2)); 23 | if (no_suffix) 24 | args.push(no_suffix); 25 | if (units) 26 | args.push(units); 27 | elem.textContent = timestamp[func].apply(timestamp, args); 28 | elem.classList.remove('flask-moment'); 29 | elem.style.display = ""; 30 | }} 31 | function flask_moment_render_all() {{ 32 | const moments = document.querySelectorAll('.flask-moment'); 33 | moments.forEach(function(moment) {{ 34 | flask_moment_render(moment); 35 | const refresh = moment.dataset.refresh; 36 | if (refresh && refresh > 0) {{ 37 | (function(elem, interval) {{ 38 | setInterval(function() {{ 39 | flask_moment_render(elem); 40 | }}, interval); 41 | }})(moment, refresh); 42 | }} 43 | }}) 44 | }} 45 | document.addEventListener("DOMContentLoaded", flask_moment_render_all);''' 46 | 47 | 48 | def _naive_now(): 49 | """Return a naive datetime object set to current UTC time.""" 50 | return datetime.now(timezone.utc).replace(tzinfo=None) 51 | 52 | 53 | class moment(object): 54 | """Create a moment object. 55 | 56 | :param timestamp: The ``datetime`` object or ISO 8601 string representing 57 | the timestamp. 58 | :param local: If ``True``, the ``timestamp`` argument is given in the 59 | local client time. In most cases this argument will be set 60 | to ``False`` and all the timestamps managed by the server 61 | will be in the UTC timezone. 62 | """ 63 | @classmethod 64 | def include_moment(cls, version=default_moment_version, local_js=None, 65 | no_js=None, sri=None, with_locales=True): 66 | """Include the moment.js library and the supporting JavaScript code 67 | used by this extension. 68 | 69 | This function must be called in the ```` section of the Jinja 70 | template(s) that use this extension. 71 | 72 | :param version: The version of moment.js to include. 73 | :param local_js: The URL to import the moment.js library from. Use this 74 | option to import the library from a locally hosted 75 | file. 76 | :param no_js: Just add the supporting code for this extension, without 77 | importing the moment.js library. . Use this option if 78 | the library is imported elsewhere in the template. The 79 | supporting JavaScript code for this extension is still 80 | included. 81 | :param sri: The SRI hash to use when importing the moment.js library, 82 | or ``None`` if the SRI hash is unknown or disabled. 83 | :param with_locales: If ``True``, include the version of moment.js that 84 | has all the locales. 85 | """ 86 | mjs = '' 87 | if version == default_moment_version and local_js is None and \ 88 | with_locales is True and sri is None: 89 | sri = default_moment_sri 90 | if not no_js: 91 | if local_js is not None: 92 | if not sri: 93 | mjs = '\n'.format(local_js) 94 | else: 95 | mjs = ('\n').format( 97 | local_js, sri) 98 | elif version is not None: 99 | if with_locales: 100 | js_filename = 'moment-with-locales.min.js' \ 101 | if version_parse(version) >= version_parse('2.8.0') \ 102 | else 'moment-with-langs.min.js' 103 | else: 104 | js_filename = 'moment.min.js' 105 | 106 | if not sri: 107 | mjs = ('\n').format( 109 | version, js_filename) 110 | else: 111 | mjs = ('\n').format( 114 | version, js_filename, sri) 115 | return Markup('{}\n\n'''.format( 116 | mjs, cls.flask_moment_js())) 117 | 118 | @staticmethod 119 | def locale(language='en', auto_detect=False, customization=None): 120 | """Configure the moment.js locale. 121 | 122 | :param language: The language code. 123 | :param auto_detect: If ``True``, detect the locale from the browser. 124 | :param customization: A dictionary with custom options for the locale, 125 | as needed by the moment.js library. 126 | """ 127 | if auto_detect: 128 | return Markup('') 132 | if customization: 133 | return Markup( 134 | ''.format( 135 | language, customization)) 136 | return Markup( 137 | ''.format(language)) 138 | 139 | @staticmethod 140 | def flask_moment_js(): 141 | """Return the JavaScript supporting code for this extension. 142 | 143 | This method is provided to enable custom configurations that are not 144 | supported by ``include_moment``. The return value of this method is 145 | a string with raw JavaScript code. This code can be added to your own 146 | `` 151 | 152 | Alternatively, the code can be returned in a JavaScript endpoint that 153 | can be loaded from the HTML file as an external resource:: 154 | 155 | @app.route('/flask-moment.js') 156 | def flask_moment_js(): 157 | return (moment.flask_moment_js(), 200, 158 | {'Content-Type': 'application/javascript'}) 159 | 160 | Note: only the code specific to Flask-Moment is included. When using 161 | this method, you must include the moment.js library separately. 162 | """ 163 | default_format = '' 164 | if 'MOMENT_DEFAULT_FORMAT' in current_app.config: 165 | default_format = '\nmoment.defaultFormat = "{}";'.format( 166 | current_app.config['MOMENT_DEFAULT_FORMAT']) 167 | return '''moment.locale("en");{}\n{}'''.format(default_format, js_code) 168 | 169 | @staticmethod 170 | def lang(language): 171 | """Set the language. This is a simpler version of the :func:`locale` 172 | function. 173 | 174 | :param language: The language code to use. 175 | """ 176 | return moment.locale(language) 177 | 178 | def __init__(self, timestamp=None, local=False): 179 | if timestamp is None: 180 | timestamp = _naive_now() 181 | self.timestamp = timestamp 182 | self.local = local 183 | 184 | def _timestamp_as_iso_8601(self, timestamp): 185 | if isinstance(timestamp, str): 186 | return timestamp # assume it is already in ISO 8601 format 187 | tz = '' 188 | if not self.local: 189 | tz = 'Z' 190 | return timestamp.strftime('%Y-%m-%dT%H:%M:%S' + tz) 191 | 192 | def _render(self, func, format=None, timestamp2=None, no_suffix=None, 193 | units=None, refresh=False): 194 | t = self._timestamp_as_iso_8601(self.timestamp) 195 | data_values = 'data-function="{}"'.format(func) 196 | if format: 197 | data_values += ' data-format="{}"'.format(format) 198 | if timestamp2: 199 | data_values += ' data-timestamp2="{}"'.format(timestamp2) 200 | if no_suffix: 201 | data_values += ' data-nosuffix="1"' 202 | if units: 203 | data_values += ' data-units="{}"'.format(units) 204 | return Markup(('{}').format( 207 | t, data_values, int(refresh) * 60000, t)) 208 | 209 | def format(self, fmt=None, refresh=False): 210 | """Format a moment object with a custom formatting string. 211 | 212 | :param fmt: The formatting specification to use, as documented by the 213 | ``format()`` function frommoment.js. 214 | :param refresh: If set to ``True``, refresh the timestamp at one 215 | minute intervals. If set to ``False``, background 216 | refreshing is disabled. If set to an integer, the 217 | refresh occurs at the indicated interval, given in 218 | minutes. 219 | """ 220 | return self._render("format", format=(fmt or ''), refresh=refresh) 221 | 222 | def fromNow(self, no_suffix=False, refresh=False): 223 | """Render the moment object as a relative time. 224 | 225 | This formatting option is often called "time ago", since it renders 226 | the timestamp using friendly text strings such as "2 hours ago" or 227 | "in 3 weeks". 228 | 229 | :param no_suffix: if set to ``True``, the time difference does not 230 | include the suffix (the "ago" or similar). 231 | :param refresh: If set to ``True``, refresh the timestamp at one 232 | minute intervals. If set to ``False``, background 233 | refreshing is disabled. If set to an integer, the 234 | refresh occurs at the indicated interval, given in 235 | minutes. 236 | """ 237 | return self._render("fromNow", no_suffix=int(no_suffix), 238 | refresh=refresh) 239 | 240 | def fromTime(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 ``from()`` function from moment.js. 245 | 246 | :param timestamp: The reference ``datetime`` object or ISO 8601 string. 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("from", timestamp2=self._timestamp_as_iso_8601( 256 | timestamp), no_suffix=int(no_suffix), refresh=refresh) 257 | 258 | def toNow(self, no_suffix=False, refresh=False): 259 | """Render the moment object as a relative time. 260 | 261 | This function renders as the reverse time interval of ``fromNow()``. 262 | 263 | :param no_suffix: if set to ``True``, the time difference does not 264 | include the suffix (the "ago" or similar). 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("toNow", no_suffix=int(no_suffix), refresh=refresh) 272 | 273 | def toTime(self, timestamp, no_suffix=False, refresh=False): 274 | """Render the moment object as a relative time with respect to a 275 | given reference time. 276 | 277 | This function maps to the ``to()`` function from moment.js. 278 | 279 | :param timestamp: The reference ``datetime`` object or ISO 8601 string. 280 | :param no_suffix: if set to ``True``, the time difference does not 281 | include the suffix (the "ago" or similar). 282 | :param refresh: If set to ``True``, refresh the timestamp at one 283 | minute intervals. If set to ``False``, background 284 | refreshing is disabled. If set to an integer, the 285 | refresh occurs at the indicated interval, given in 286 | minutes. 287 | """ 288 | return self._render("to", timestamp2=self._timestamp_as_iso_8601( 289 | timestamp), no_suffix=int(no_suffix), refresh=refresh) 290 | 291 | def calendar(self, refresh=False): 292 | """Render the moment object as a relative time, either to current time 293 | or a given reference timestamp. 294 | 295 | This function renders relative time using day references such as 296 | tomorrow, next Sunday, etc. 297 | 298 | :param refresh: If set to ``True``, refresh the timestamp at one 299 | minute intervals. If set to ``False``, background 300 | refreshing is disabled. If set to an integer, the 301 | refresh occurs at the indicated interval, given in 302 | minutes. 303 | """ 304 | return self._render("calendar", refresh=refresh) 305 | 306 | def valueOf(self, refresh=False): 307 | """Render the moment object as milliseconds from Unix Epoch. 308 | 309 | :param refresh: If set to ``True``, refresh the timestamp at one 310 | minute intervals. If set to ``False``, background 311 | refreshing is disabled. If set to an integer, the 312 | refresh occurs at the indicated interval, given in 313 | minutes. 314 | """ 315 | return self._render("valueOf", refresh=refresh) 316 | 317 | def unix(self, refresh=False): 318 | """Render the moment object as seconds from Unix Epoch. 319 | 320 | :param refresh: If set to ``True``, refresh the timestamp at one 321 | minute intervals. If set to ``False``, background 322 | refreshing is disabled. If set to an integer, the 323 | refresh occurs at the indicated interval, given in 324 | minutes. 325 | """ 326 | return self._render("unix", refresh=refresh) 327 | 328 | def diff(self, timestamp, units, refresh=False): 329 | """Render the difference between the moment object and the given 330 | timestamp using the provided units. 331 | 332 | :param timestamp: The reference ``datetime`` object or ISO 8601 string. 333 | :param units: A time unit such as `years`, `months`, `weeks`, `days`, 334 | `hours`, `minutes` or `seconds`. 335 | :param refresh: If set to ``True``, refresh the timestamp at one 336 | minute intervals. If set to ``False``, background 337 | refreshing is disabled. If set to an integer, the 338 | refresh occurs at the indicated interval, given in 339 | minutes. 340 | """ 341 | return self._render("diff", timestamp2=self._timestamp_as_iso_8601( 342 | timestamp), units=units, refresh=refresh) 343 | 344 | 345 | class Moment(object): 346 | def __init__(self, app=None): 347 | if app is not None: 348 | self.init_app(app) 349 | 350 | def init_app(self, app): 351 | if not hasattr(app, 'extensions'): # pragma: no cover 352 | app.extensions = {} 353 | app.extensions['moment'] = moment 354 | app.context_processor(self.context_processor) 355 | 356 | @staticmethod 357 | def context_processor(): 358 | return { 359 | 'moment': current_app.extensions['moment'] 360 | } 361 | 362 | def flask_moment_js(self): 363 | return current_app.extensions['moment'].flask_moment_js() 364 | 365 | def create(self, timestamp=None): 366 | return current_app.extensions['moment'](timestamp) 367 | --------------------------------------------------------------------------------