From 07caa26e50a86c0d2e09da5504ba25ec5b580dc6 Mon Sep 17 00:00:00 2001 From: Yann Savary Date: Fri, 24 Jun 2022 21:34:42 +0200 Subject: [PATCH] Migrate profile and favorites data from mongodb to postgresql (#18) Migrate profile and favorites data from mongodb to postgresql --- .../build-deploy-production-docker.yml | 45 ++ .github/workflows/test-python.yml | 35 ++ Dockerfile | 2 +- docker-cmd.sh | 1 + docker-compose.yml | 10 +- manage.py | 4 +- poetry.lock | 462 ++++++++++++++---- pyproject.toml | 16 +- setup.cfg | 2 + tests/__init__.py | 0 tests/test_fake.py | 2 + tools/import_from_json.py | 37 -- winds_mobi_admin/authentication.py | 13 +- winds_mobi_admin/settings.py | 116 ++--- winds_mobi_admin/urls.py | 4 +- winds_mobi_admin/wsgi.py | 2 +- winds_mobi_user/admin.py | 44 +- winds_mobi_user/apps.py | 2 +- winds_mobi_user/facebook_views.py | 26 +- winds_mobi_user/google_views.py | 32 +- winds_mobi_user/migrations/0001_initial.py | 51 ++ .../migrations/0002_auto_20220624_1736.py | 39 ++ winds_mobi_user/migrations/__init__.py | 0 winds_mobi_user/models.py | 20 + .../winds_mobi_user/oauth2_callback.html | 52 +- winds_mobi_user/urls.py | 20 +- winds_mobi_user/views.py | 169 ++++--- winds_mobi_zermatt/admin.py | 4 +- winds_mobi_zermatt/apps.py | 4 +- winds_mobi_zermatt/migrations/0001_initial.py | 17 +- winds_mobi_zermatt/models.py | 16 +- 31 files changed, 865 insertions(+), 382 deletions(-) create mode 100644 .github/workflows/build-deploy-production-docker.yml create mode 100644 .github/workflows/test-python.yml create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/test_fake.py delete mode 100644 tools/import_from_json.py create mode 100644 winds_mobi_user/migrations/0001_initial.py create mode 100644 winds_mobi_user/migrations/0002_auto_20220624_1736.py create mode 100644 winds_mobi_user/migrations/__init__.py create mode 100644 winds_mobi_user/models.py diff --git a/.github/workflows/build-deploy-production-docker.yml b/.github/workflows/build-deploy-production-docker.yml new file mode 100644 index 0000000..66b7d69 --- /dev/null +++ b/.github/workflows/build-deploy-production-docker.yml @@ -0,0 +1,45 @@ +name: Build and deploy a production docker image based on main branch + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build-deploy-production-docker: + name: Build and deploy + environment: production + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: | + ghcr.io/winds-mobi/winds-mobi-admin + tags: | + type=semver,pattern={{version}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + + - name: Update Watchtower containers + run: | + curl -H "Authorization: Bearer ${{ secrets.WATCHTOWER_HTTP_API_TOKEN }}" https://watchtower.winds.mobi/v1/update diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml new file mode 100644 index 0000000..567e264 --- /dev/null +++ b/.github/workflows/test-python.yml @@ -0,0 +1,35 @@ +name: Test project + +on: + push: + branches: + - main + pull_request: + +jobs: + test-python: + name: Lint and test + runs-on: ubuntu-latest + env: + PYTHON_VERSION: "3.9" + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install python dependencies with poetry + run: | + pip install poetry + poetry install + + - name: Run python linters + run: | + poetry run black --check . + poetry run flake8 . + + - name: Run python tests + run: poetry run pytest diff --git a/Dockerfile b/Dockerfile index f8248cb..763bbb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.9-slim-bullseye AS base +FROM python:3.9.13-slim-bullseye AS base ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 diff --git a/docker-cmd.sh b/docker-cmd.sh index a1c3ded..ace365b 100755 --- a/docker-cmd.sh +++ b/docker-cmd.sh @@ -1,3 +1,4 @@ #!/usr/bin/env sh +python manage.py migrate gunicorn --workers=3 --bind="0.0.0.0:$PORT" winds_mobi_admin.wsgi diff --git a/docker-compose.yml b/docker-compose.yml index 320f82a..1251608 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,9 @@ version: '3.9' services: postgres: - image: postgres:14.1 + image: postgres:14.4 ports: - - "5432:5432" + - "8016:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres @@ -12,6 +12,11 @@ services: volumes: - ./volumes/postgres:/var/lib/postgresql/data + redis: + image: redis:7.0.2 + ports: + - "8017:6379" + admin: build: context: . @@ -27,6 +32,7 @@ services: ALLOWED_HOSTS: localhost STATIC_URL: ${STATIC_URL} DB_URL: ${DB_URL} + REDIS_URL: ${REDIS_URL} MONGODB_URL: ${MONGODB_URL} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} FACEBOOK_REDIRECT_URI: ${FACEBOOK_REDIRECT_URI} diff --git a/manage.py b/manage.py index 9352670..dcf02a4 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'winds_mobi_admin.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "winds_mobi_admin.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/poetry.lock b/poetry.lock index e8d4446..9e91314 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,40 +1,69 @@ [[package]] name = "asgiref" -version = "3.4.1" +version = "3.5.2" description = "ASGI specs, helper code, and adapters" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "black" -version = "21.12b0" +version = "22.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = ">=3.10.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "5.0.0" +version = "5.2.0" description = "Extensible memoizing collections and decorators" category = "main" optional = false @@ -42,15 +71,15 @@ python-versions = "~=3.7" [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -61,23 +90,37 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + [[package]] name = "dj-database-url" version = "0.5.0" @@ -88,7 +131,7 @@ python-versions = "*" [[package]] name = "django" -version = "3.2.10" +version = "3.2.13" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -105,14 +148,14 @@ bcrypt = ["bcrypt"] [[package]] name = "django-cors-headers" -version = "3.10.1" +version = "3.13.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -Django = ">=2.2" +Django = ">=3.2" [[package]] name = "djangorestframework" @@ -190,6 +233,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "mccabe" version = "0.6.1" @@ -208,16 +259,27 @@ python-versions = "*" [[package]] name = "oauthlib" -version = "3.1.1" +version = "3.2.0" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" category = "main" optional = false python-versions = ">=3.6" [package.extras] -rsa = ["cryptography (>=3.0.0,<4)"] +rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" @@ -229,24 +291,44 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.4.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "psycopg2" -version = "2.9.2" +version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pyasn1" version = "0.4.8" @@ -284,7 +366,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyjwt" -version = "2.3.0" +version = "2.4.0" description = "JSON Web Token implementation in Python" category = "main" optional = false @@ -314,9 +396,41 @@ srv = ["dnspython (>=1.16.0,<1.17.0)"] tls = ["ipaddress"] zstd = ["zstandard"] +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + [[package]] name = "python-dotenv" -version = "0.19.2" +version = "0.20.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false @@ -327,33 +441,50 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2021.3" +version = "2022.1" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" +[[package]] +name = "redis" +version = "4.3.3" +description = "Python client for Redis database and key-value store" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = ">=4.0.2" +deprecated = ">=1.2.3" +packaging = ">=20.4" + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "requests" -version = "2.26.0" +version = "2.28.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2.0.0,<2.1.0" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "requests-oauthlib" -version = "1.3.0" +version = "1.3.1" description = "OAuthlib authentication support for Requests." category = "main" optional = false @@ -379,7 +510,7 @@ pyasn1 = ">=0.1.3" [[package]] name = "sentry-sdk" -version = "1.5.1" +version = "1.6.0" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -401,6 +532,7 @@ flask = ["flask (>=0.11)", "blinker (>=1.1)"] httpx = ["httpx (>=0.16.0)"] pure_eval = ["pure-eval", "executing", "asttokens"] pyspark = ["pyspark (>=2.4.4)"] +quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] @@ -424,30 +556,30 @@ python-versions = ">=3.5" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.9" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -462,51 +594,96 @@ python-versions = ">=3.5, <4" [package.extras] brotli = ["brotli"] +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "49d3674404a51c21eca8b686aed54f4947340555b4efcaff9dfb42e7952864f6" +content-hash = "65c2f3556cba61d8c6f20ac3571beb8397bc2df1fc8b96943f2d3de663365e2d" [metadata.files] asgiref = [ - {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, - {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, +] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] cachetools = [ - {file = "cachetools-5.0.0-py3-none-any.whl", hash = "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4"}, - {file = "cachetools-5.0.0.tar.gz", hash = "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6"}, + {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, + {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] dj-database-url = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, ] django = [ - {file = "Django-3.2.10-py3-none-any.whl", hash = "sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3"}, - {file = "Django-3.2.10.tar.gz", hash = "sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4"}, + {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, + {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, ] django-cors-headers = [ - {file = "django-cors-headers-3.10.1.tar.gz", hash = "sha256:b5a874b492bcad99f544bb76ef679472259eb41ee5644ca62d1a94ddb26b7f6e"}, - {file = "django_cors_headers-3.10.1-py3-none-any.whl", hash = "sha256:1390b5846e9835b0911e2574409788af87cd9154246aafbdc8ec546c93698fe6"}, + {file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"}, + {file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"}, ] djangorestframework = [ {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, @@ -532,6 +709,10 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -541,29 +722,41 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] oauthlib = [ - {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, - {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, + {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"}, + {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, - {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] psycopg2 = [ - {file = "psycopg2-2.9.2-cp310-cp310-win32.whl", hash = "sha256:6796ac614412ce374587147150e56d03b7845c9e031b88aacdcadc880e81bb38"}, - {file = "psycopg2-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:dfc32db6ce9ecc35a131320888b547199f79822b028934bb5b332f4169393e15"}, - {file = "psycopg2-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:77d09a79f9739b97099d2952bbbf18eaa4eaf825362387acbb9552ec1b3fa228"}, - {file = "psycopg2-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f65cba7924363e0d2f416041b48ff69d559548f2cb168ff972c54e09e1e64db8"}, - {file = "psycopg2-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:b8816c6410fa08d2a022e4e38d128bae97c1855e176a00493d6ec62ccd606d57"}, - {file = "psycopg2-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:26322c3f114de1f60c1b0febf8fdd595c221b4f624524178f515d07350a71bd1"}, - {file = "psycopg2-2.9.2-cp38-cp38-win32.whl", hash = "sha256:77b9105ef37bc005b8ffbcb1ed6d8685bb0e8ce84773738aa56421a007ec5a7a"}, - {file = "psycopg2-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:91c7fd0fe9e6c118e8ff5b665bc3445781d3615fa78e131d0b4f8c85e8ca9ec8"}, - {file = "psycopg2-2.9.2-cp39-cp39-win32.whl", hash = "sha256:a761b60da0ecaf6a9866985bcde26327883ac3cdb90535ab68b8d784f02b05ef"}, - {file = "psycopg2-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:fd7ddab7d6afee4e21c03c648c8b667b197104713e57ec404d5b74097af21e31"}, - {file = "psycopg2-2.9.2.tar.gz", hash = "sha256:a84da9fa891848e0270e8e04dcca073bc9046441eeb47069f5c0e36783debbea"}, + {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"}, + {file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"}, + {file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"}, + {file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"}, + {file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"}, + {file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"}, + {file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"}, + {file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"}, + {file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"}, + {file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"}, + {file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -604,8 +797,8 @@ pyflakes = [ {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pyjwt = [ - {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, - {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, + {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, + {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, ] pymongo = [ {file = "pymongo-3.12.2-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:2aae55a1eb7ede1a409e7f5705db03d2c8ff15a11da7fc32db0e6de4c395cfba"}, @@ -716,30 +909,41 @@ pymongo = [ {file = "pymongo-3.12.2-py2.7-macosx-10.14-intel.egg", hash = "sha256:409edd79362f93e39a7c8ee5ac174a0054012f7eb84cdd31185f22d25377f624"}, {file = "pymongo-3.12.2.tar.gz", hash = "sha256:64ea5e97fca1a37f83df9f3460bf63640bc0d725e12f3471e6acbf3a6040dd37"}, ] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] python-dotenv = [ - {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, - {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, ] pytz = [ - {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, - {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, +] +redis = [ + {file = "redis-4.3.3-py3-none-any.whl", hash = "sha256:f57f8df5d238a8ecf92f499b6b21467bfee6c13d89953c27edf1e2bc673622e7"}, + {file = "redis-4.3.3.tar.gz", hash = "sha256:2f7a57cf4af15cd543c4394bcbe2b9148db2606a37edba755368836e3a1d053e"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, + {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, ] requests-oauthlib = [ - {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, - {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, - {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, ] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, ] sentry-sdk = [ - {file = "sentry-sdk-1.5.1.tar.gz", hash = "sha256:2a1757d6611e4bec7d672c2b7ef45afef79fed201d064f53994753303944f5a8"}, - {file = "sentry_sdk-1.5.1-py2.py3-none-any.whl", hash = "sha256:e4cb107e305b2c1b919414775fa73a9997f996447417d22b98e7610ded1e9eb5"}, + {file = "sentry-sdk-1.6.0.tar.gz", hash = "sha256:b82ad57306d5546713f15d5d70daea0408cf7f998c7566db16e0e6257e51e561"}, + {file = "sentry_sdk-1.6.0-py2.py3-none-any.whl", hash = "sha256:ddbd191b6f4e696b7845b4d87389898ae1207981faf114f968a57363aa6be03c"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -750,18 +954,84 @@ sqlparse = [ {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] whitenoise = [ {file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"}, {file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"}, ] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] diff --git a/pyproject.toml b/pyproject.toml index 488ad83..5b85968 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,23 +13,25 @@ packages = [ [tool.poetry.dependencies] python = "3.9.*" -django = "3.2.10" +django = "3.2.13" dj-database-url = "0.5.0" djangorestframework = "3.13.1" -django-cors-headers = "3.10.1" +django-cors-headers = "3.13.0" google-auth = "1.6.3" google-auth-oauthlib = "0.3.0" gunicorn = "20.1.0" -psycopg2 = "2.9.2" +psycopg2 = "2.9.3" pymongo = "3.12.2" -pyjwt = "2.3.0" -sentry-sdk = "1.5.1" +pyjwt = "2.4.0" +redis = "4.3.3" +sentry-sdk = "1.6.0" whitenoise = "5.3.0" [tool.poetry.dev-dependencies] -black = "21.12b0" +black = "22.3.0" flake8 = "4.0.1" -python-dotenv = "^0.19.2" +pytest = "7.1.2" +python-dotenv = "^0.20.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fake.py b/tests/test_fake.py new file mode 100644 index 0000000..625c413 --- /dev/null +++ b/tests/test_fake.py @@ -0,0 +1,2 @@ +def test_fake(): + assert True diff --git a/tools/import_from_json.py b/tools/import_from_json.py deleted file mode 100644 index 17af119..0000000 --- a/tools/import_from_json.py +++ /dev/null @@ -1,37 +0,0 @@ -import json - -from winds_mobi_jdc.models import Station, StationStatus - - -def get_status(status): - if status == 0: - return StationStatus.unactive - elif status == 1: - return StationStatus.active - elif status == 2: - return StationStatus.maintenance - elif status == 3: - return StationStatus.test - elif status == 4: - return StationStatus.waiting - elif status == 5: - return StationStatus.wintering - elif status == 6: - return StationStatus.moved - - -with open('jdc.json') as f: - stations = json.load(f) - -for station in stations: - Station.objects.create( - id=station['id'], - short_name=station['shortname'], - name=station['name'], - description=station['raw_description'], - status=get_status(station['status_id']).name, - latitude=station['lat'], - longitude=station['long'], - altitude=station['altitude'], - phone_number=station['phone_nr'] if station['phone_nr'] else '' - ) diff --git a/winds_mobi_admin/authentication.py b/winds_mobi_admin/authentication.py index cb0df78..bfbbc92 100644 --- a/winds_mobi_admin/authentication.py +++ b/winds_mobi_admin/authentication.py @@ -6,16 +6,15 @@ class JWTAuthentication(BaseAuthentication): - def get_jwt_value(self, request): - auth = request.META.get('HTTP_AUTHORIZATION', '').split() + auth = request.META.get("HTTP_AUTHORIZATION", "").split() if not auth: return None if len(auth) == 1: - raise AuthenticationFailed('Invalid Authorization header: no credentials provided') + raise AuthenticationFailed("Invalid Authorization header: no credentials provided") elif len(auth) > 2: - raise AuthenticationFailed('Invalid Authorization header: credentials string should not contain spaces') + raise AuthenticationFailed("Invalid Authorization header: credentials string should not contain spaces") return auth[1] @@ -27,13 +26,13 @@ def authenticate(self, request): try: payload = jwt.decode(jwt_value, key=settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]) except jwt.ExpiredSignatureError: - raise AuthenticationFailed('Signature has expired') + raise AuthenticationFailed("Signature has expired") except jwt.DecodeError: - raise AuthenticationFailed('Error decoding signature') + raise AuthenticationFailed("Error decoding signature") except jwt.InvalidTokenError: raise AuthenticationFailed() - return payload['username'], jwt_value + return payload["username"], jwt_value class IsJWTAuthenticated(BasePermission): diff --git a/winds_mobi_admin/settings.py b/winds_mobi_admin/settings.py index 298ea1f..7b4ed24 100644 --- a/winds_mobi_admin/settings.py +++ b/winds_mobi_admin/settings.py @@ -24,92 +24,91 @@ # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('SECRET_KEY', 'dev') -JWT_ALGORITHM = 'HS256' +SECRET_KEY = os.environ.get("SECRET_KEY", "dev") +JWT_ALGORITHM = "HS256" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get('DEBUG', 'True').lower() not in ('false', 'no', '0') +DEBUG = os.environ.get("DEBUG", "True").lower() not in ("false", "no", "0") -ALLOWED_HOSTS = [s for s in os.environ.get('ALLOWED_HOSTS', '').split(',') if s] +ALLOWED_HOSTS = [s for s in os.environ.get("ALLOWED_HOSTS", "").split(",") if s] # https://docs.djangoproject.com/en/2.2/ref/settings/#secure-proxy-ssl-header -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'corsheaders', - 'rest_framework', - - 'winds_mobi_user.apps.UserConfig', - 'winds_mobi_zermatt.apps.ZermattConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "rest_framework", + "winds_mobi_user.apps.UserConfig", + "winds_mobi_zermatt.apps.ZermattConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'winds_mobi_admin.urls' +ROOT_URLCONF = "winds_mobi_admin.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'winds_mobi_admin.wsgi.application' +WSGI_APPLICATION = "winds_mobi_admin.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { - 'default': dj_database_url.parse( - os.environ.get('DB_URL') or 'postgres://postgres:postgres@localhost:5432/winds') + "default": dj_database_url.parse(os.environ.get("DB_URL", "postgres://postgres:postgres@localhost:5432/winds")) } -MONGODB_URL = os.environ.get('MONGODB_URL') or 'mongodb://localhost:27017/winds' +REDIS_URL = os.environ.get("REDIS_URL", None) +MONGODB_URL = os.environ.get("MONGODB_URL", None) +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -117,38 +116,31 @@ # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'Europe/Zurich' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "Europe/Zurich" USE_I18N = True - USE_L10N = True - USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_ROOT = os.environ.get('STATIC_ROOT') or os.path.join(BASE_DIR, 'static/') -STATIC_URL = os.environ.get('STATIC_URL') or '/static/' +STATIC_ROOT = os.environ.get("STATIC_ROOT") or os.path.join(BASE_DIR, "static/") +STATIC_URL = os.environ.get("STATIC_URL") or "/static/" # winds.mobi settings CORS_ORIGIN_ALLOW_ALL = True -SENTRY_DSN = os.environ.get('SENTRY_DSN') -ENVIRONMENT = os.environ.get('ENVIRONMENT') or 'development' +SENTRY_DSN = os.environ.get("SENTRY_DSN") +ENVIRONMENT = os.environ.get("ENVIRONMENT") or "development" -GOOGLE_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET') -FACEBOOK_REDIRECT_URI = os.environ.get('FACEBOOK_REDIRECT_URI') -FACEBOOK_CLIENT_ID = os.environ.get('FACEBOOK_CLIENT_ID') -FACEBOOK_CLIENT_SECRET = os.environ.get('FACEBOOK_CLIENT_SECRET') +GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET") +FACEBOOK_REDIRECT_URI = os.environ.get("FACEBOOK_REDIRECT_URI") +FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID") +FACEBOOK_CLIENT_SECRET = os.environ.get("FACEBOOK_CLIENT_SECRET") -sentry_sdk.init( - dsn=SENTRY_DSN, - environment=ENVIRONMENT, - integrations=[DjangoIntegration()] -) +sentry_sdk.init(dsn=SENTRY_DSN, environment=ENVIRONMENT, integrations=[DjangoIntegration()]) diff --git a/winds_mobi_admin/urls.py b/winds_mobi_admin/urls.py index 609307f..aa258e0 100644 --- a/winds_mobi_admin/urls.py +++ b/winds_mobi_admin/urls.py @@ -17,6 +17,6 @@ from django.urls import path, include urlpatterns = [ - path('admin/', admin.site.urls), - path('user/', include('winds_mobi_user.urls', namespace='user')), + path("admin/", admin.site.urls), + path("user/", include("winds_mobi_user.urls", namespace="user")), ] diff --git a/winds_mobi_admin/wsgi.py b/winds_mobi_admin/wsgi.py index 648017a..916d06a 100644 --- a/winds_mobi_admin/wsgi.py +++ b/winds_mobi_admin/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'winds_mobi_admin.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "winds_mobi_admin.settings") application = get_wsgi_application() diff --git a/winds_mobi_user/admin.py b/winds_mobi_user/admin.py index 5c77199..b11fa6d 100644 --- a/winds_mobi_user/admin.py +++ b/winds_mobi_user/admin.py @@ -1,11 +1,51 @@ from django.contrib import admin +from django.contrib.admin import display from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User +from django.db.models import F +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +from winds_mobi_user.models import Profile, SocialAuth + + +@admin.register(SocialAuth) +class SocialAuthAdmin(admin.ModelAdmin): + list_display = ("id", "provider", "provider_id", "user_url") + readonly_fields = ("provider", "provider_id", "data") + + @display(description=_("User url")) + def user_url(self, obj): + url = reverse("admin:auth_user_change", args=[obj.user.id]) + return format_html(f"{obj.user.username}", url=url) + + +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ("username",) + search_fields = ("data__favorites",) + + @display(description=_("Username")) + def username(self, obj): + return obj.user.username + + +class ProfileInline(admin.StackedInline): + model = Profile + + +class SocialAuthInline(admin.StackedInline): + model = SocialAuth + extra = 0 class CustomUserAdmin(UserAdmin): - list_display = list(UserAdmin.list_display) + ['last_login', 'date_joined'] - ordering = ('-date_joined',) + list_display = list(UserAdmin.list_display) + ["last_login", "date_joined"] + inlines = (ProfileInline, SocialAuthInline) + + def get_ordering(self, request): + return (F("last_login").desc(nulls_last=True),) admin.site.unregister(User) diff --git a/winds_mobi_user/apps.py b/winds_mobi_user/apps.py index 3be45e4..0e0ae31 100644 --- a/winds_mobi_user/apps.py +++ b/winds_mobi_user/apps.py @@ -2,4 +2,4 @@ class UserConfig(AppConfig): - name = 'winds_mobi_user' + name = "winds_mobi_user" diff --git a/winds_mobi_user/facebook_views.py b/winds_mobi_user/facebook_views.py index b26a488..319d789 100644 --- a/winds_mobi_user/facebook_views.py +++ b/winds_mobi_user/facebook_views.py @@ -5,34 +5,28 @@ from requests_oauthlib import OAuth2Session from requests_oauthlib.compliance_fixes import facebook_compliance_fix -from .views import Oauth2Callback +from winds_mobi_user.views import Oauth2Callback class FacebookOauth2Callback(Oauth2Callback): - graph_api_version = 'v12.0' + graph_api_version = "v12.0" - authorization_base_url = 'https://www.facebook.com/dialog/oauth?scope=public_profile&scope=email' - token_url = f'https://graph.facebook.com/{graph_api_version}/oauth/access_token' - me_url = f'https://graph.facebook.com/{graph_api_version}/me?fields=id,name,first_name,last_name,email,birthday,\ - timezone,website,location,locale,devices' + authorization_base_url = "https://www.facebook.com/dialog/oauth?scope=public_profile&scope=email" + token_url = f"https://graph.facebook.com/{graph_api_version}/oauth/access_token" + me_url = f"https://graph.facebook.com/{graph_api_version}/me?fields=id,name,first_name,last_name,email,birthday,\ + timezone,website,location,locale,devices" def get(self, request, *args, **kwargs): facebook = OAuth2Session(settings.FACEBOOK_CLIENT_ID, redirect_uri=settings.FACEBOOK_REDIRECT_URI) facebook = facebook_compliance_fix(facebook) - if 'code' not in self.request.GET: + if "code" not in self.request.GET: authorization_url, state = facebook.authorization_url(self.authorization_base_url) return HttpResponseRedirect(authorization_url) else: - auth_code = self.request.GET['code'] + auth_code = self.request.GET["code"] facebook.fetch_token(self.token_url, client_secret=settings.FACEBOOK_CLIENT_SECRET, code=auth_code) user_info = json.loads(facebook.get(self.me_url).text) - username = f"facebook-{user_info['id']}" - email = user_info['email'] or '' - - ott = self.save_user(username, email, user_info) - context = { - 'ott': ott, - 'redirect_url': '/stations/list' - } + ott = self.save_user_auth("facebook", user_info["id"], user_info["email"], user_info) + context = {"ott": ott, "redirect_url": "/stations/list"} return self.render_to_response(context) diff --git a/winds_mobi_user/google_views.py b/winds_mobi_user/google_views.py index b219b6b..2bc58e9 100644 --- a/winds_mobi_user/google_views.py +++ b/winds_mobi_user/google_views.py @@ -6,7 +6,7 @@ from google_auth_oauthlib.flow import Flow from rest_framework.reverse import reverse -from .views import Oauth2Callback +from winds_mobi_user.views import Oauth2Callback class GoogleOauth2Callback(Oauth2Callback): @@ -15,25 +15,23 @@ def get(self, request, *args, **kwargs): client_secret = json.load(config_file) flow = Flow.from_client_config( client_secret, - scopes=['openid', - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email'], - redirect_uri=reverse('user:google_oauth2callback', request=self.request)) + scopes=[ + "openid", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + ], + redirect_uri=reverse("user:google_oauth2callback", request=self.request), + ) - if 'code' not in self.request.GET: + if "code" not in self.request.GET: url, state = flow.authorization_url() return HttpResponseRedirect(url) else: - auth_code = self.request.GET['code'] + auth_code = self.request.GET["code"] token = flow.fetch_token(code=auth_code) - user_info = requests.get('https://www.googleapis.com/oauth2/v3/userinfo', - params={'access_token': token['access_token']}).json() - username = f"google-{user_info['sub']}" - email = user_info['email'] or '' - - ott = self.save_user(username, email, user_info) - context = { - 'ott': ott, - 'redirect_url': '/stations/list' - } + user_info = requests.get( + "https://www.googleapis.com/oauth2/v3/userinfo", params={"access_token": token["access_token"]} + ).json() + ott = self.save_user_auth("google", user_info["sub"], user_info["email"], user_info) + context = {"ott": ott, "redirect_url": "/stations/list"} return self.render_to_response(context) diff --git a/winds_mobi_user/migrations/0001_initial.py b/winds_mobi_user/migrations/0001_initial.py new file mode 100644 index 0000000..d40e1a5 --- /dev/null +++ b/winds_mobi_user/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 3.2.13 on 2022-06-24 15:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="SocialAuth", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("provider", models.CharField(max_length=50, verbose_name="Provider")), + ("provider_id", models.CharField(max_length=50, verbose_name="Provider id")), + ("data", models.JSONField(verbose_name="Data")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="social_auths", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Profile", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("data", models.JSONField(default=dict, verbose_name="Data")), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, related_name="profile", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.AddConstraint( + model_name="socialauth", + constraint=models.UniqueConstraint(fields=("provider", "provider_id"), name="unique_provider_id"), + ), + ] diff --git a/winds_mobi_user/migrations/0002_auto_20220624_1736.py b/winds_mobi_user/migrations/0002_auto_20220624_1736.py new file mode 100644 index 0000000..5724c92 --- /dev/null +++ b/winds_mobi_user/migrations/0002_auto_20220624_1736.py @@ -0,0 +1,39 @@ +import logging + +from django.conf import settings +from django.db import migrations +from pymongo import MongoClient, uri_parser + +log = logging.getLogger(__name__) + + +def migrate_user_profiles(apps, schema_editor): + uri = uri_parser.parse_uri(settings.MONGODB_URL) + mongo_client = MongoClient(uri["nodelist"][0][0], uri["nodelist"][0][1]) + mongo_db = mongo_client[uri["database"]] + + User = apps.get_model("auth", "User") + SocialAuth = apps.get_model("winds_mobi_user", "SocialAuth") + Profile = apps.get_model("winds_mobi_user", "Profile") + + for user in User.objects.all(): + profile = mongo_db.users.find_one(user.username) + if profile: + provider, provider_id = user.username.split("-") + SocialAuth.objects.get_or_create( + user=user, provider=provider, provider_id=provider_id, defaults={"data": profile["user-info"]} + ) + if "favorites" in profile and profile["favorites"]: + Profile.objects.get_or_create(user=user, defaults={"data": {"favorites": profile["favorites"]}}) + else: + log.warning(f"No profile found for user '{user.username}' in mongodb") + + +class Migration(migrations.Migration): + dependencies = [ + ("winds_mobi_user", "0001_initial"), + ] + + operations = [ + migrations.RunPython(migrate_user_profiles), + ] diff --git a/winds_mobi_user/migrations/__init__.py b/winds_mobi_user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/winds_mobi_user/models.py b/winds_mobi_user/models.py new file mode 100644 index 0000000..2e46b05 --- /dev/null +++ b/winds_mobi_user/models.py @@ -0,0 +1,20 @@ +from django.contrib.auth import get_user_model +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class SocialAuth(models.Model): + provider = models.CharField(_("Provider"), max_length=50) + provider_id = models.CharField(_("Provider id"), max_length=50) + user = models.ForeignKey(get_user_model(), related_name="social_auths", on_delete=models.CASCADE) + data = models.JSONField(_("Data")) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["provider", "provider_id"], name="unique_provider_id"), + ] + + +class Profile(models.Model): + user = models.OneToOneField(get_user_model(), related_name="profile", on_delete=models.CASCADE) + data = models.JSONField(_("Data"), default=dict) diff --git a/winds_mobi_user/templates/winds_mobi_user/oauth2_callback.html b/winds_mobi_user/templates/winds_mobi_user/oauth2_callback.html index 7324284..e9d4106 100644 --- a/winds_mobi_user/templates/winds_mobi_user/oauth2_callback.html +++ b/winds_mobi_user/templates/winds_mobi_user/oauth2_callback.html @@ -1,33 +1,33 @@ - - + winds.mobi - -
- - +
+ diff --git a/winds_mobi_user/urls.py b/winds_mobi_user/urls.py index a751002..98fdce3 100644 --- a/winds_mobi_user/urls.py +++ b/winds_mobi_user/urls.py @@ -1,17 +1,15 @@ from django.urls import path -from winds_mobi_user.views import Login, Profile, ProfileFavorite -from .facebook_views import FacebookOauth2Callback -from .google_views import GoogleOauth2Callback +from winds_mobi_user.facebook_views import FacebookOauth2Callback +from winds_mobi_user.google_views import GoogleOauth2Callback +from winds_mobi_user.views import LoginView, ProfileFavoriteView, ProfileView -app_name = 'user' +app_name = "user" urlpatterns = [ - path('login/', Login.as_view(), name='login'), - - path('profile/', Profile.as_view(), name='profile'), - path('profile/favorites//', ProfileFavorite.as_view(), name='profile_favorites'), - - path('google/oauth2callback/', GoogleOauth2Callback.as_view(), name='google_oauth2callback'), - path('facebook/oauth2callback/', FacebookOauth2Callback.as_view(), name='facebook_oauth2callback') + path("login/", LoginView.as_view(), name="login"), + path("profile/", ProfileView.as_view(), name="profile"), + path("profile/favorites//", ProfileFavoriteView.as_view(), name="profile_favorites"), + path("google/oauth2callback/", GoogleOauth2Callback.as_view(), name="google_oauth2callback"), + path("facebook/oauth2callback/", FacebookOauth2Callback.as_view(), name="facebook_oauth2callback"), ] diff --git a/winds_mobi_user/views.py b/winds_mobi_user/views.py index e0d2d01..a6dbc4e 100644 --- a/winds_mobi_user/views.py +++ b/winds_mobi_user/views.py @@ -1,23 +1,28 @@ import binascii import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import jwt from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth.models import User -from django.utils import timezone +from django.contrib.auth import authenticate, get_user_model +from django.db import transaction from django.views.generic import TemplateView -from pymongo import ASCENDING -from pymongo import MongoClient, uri_parser +from redis.client import Redis from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -from winds_mobi_admin.authentication import JWTAuthentication, IsJWTAuthenticated +from winds_mobi_admin.authentication import IsJWTAuthenticated, JWTAuthentication +from winds_mobi_user.models import Profile, SocialAuth +User = get_user_model() -class Login(APIView): + +def get_ott_key(ott): + return f"login-ott/{ott}" + + +class LoginView(APIView): """ Login into API with a One Time Token or a Django username/password Return a JWT token @@ -27,121 +32,143 @@ class Login(APIView): authentication_classes = () def post(self, request): - ott = request.data.get('ott') - username = request.data.get('username') - password = request.data.get('password') + ott = request.data.get("ott") + username = request.data.get("username") + password = request.data.get("password") if ott: - ott_doc = mongo_db.login_ott.find_one_and_delete({'_id': ott}) - if not ott_doc: - return Response({ - 'code': -11, - 'detail': 'Unable to find One Time Token'}, - status=status.HTTP_401_UNAUTHORIZED) - username = ott_doc['username'] + username = redis.getdel(get_ott_key(ott)) + if not username: + return Response( + {"code": -11, "detail": "Unable to find One Time Token"}, status=status.HTTP_401_UNAUTHORIZED + ) try: User.objects.get(username=username) except User.DoesNotExist: - return Response({ - 'code': -12, - 'detail': 'Unable to get user'}, - status=status.HTTP_401_UNAUTHORIZED) - token = jwt.encode({'username': username, 'exp': datetime.utcnow() + timedelta(days=30)}, - key=settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) - return Response({'token': token}) + return Response({"code": -12, "detail": "Unable to get user"}, status=status.HTTP_401_UNAUTHORIZED) + token = jwt.encode( + {"username": username, "exp": datetime.utcnow() + timedelta(days=30)}, + key=settings.SECRET_KEY, + algorithm=settings.JWT_ALGORITHM, + ) + return Response({"token": token}) elif username and password: user = authenticate(username=username, password=password) if user is not None: if user.is_active: - token = jwt.encode({'username': username, 'exp': datetime.utcnow() + timedelta(days=30)}, - key=settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) - return Response({'token': token}) + token = jwt.encode( + {"username": username, "exp": datetime.utcnow() + timedelta(days=30)}, + key=settings.SECRET_KEY, + algorithm=settings.JWT_ALGORITHM, + ) + return Response({"token": token}) else: - return Response({ - 'code': -22, - 'detail': 'The password is valid, but the account has been disabled'}, - status=status.HTTP_401_UNAUTHORIZED) + return Response( + {"code": -22, "detail": "The password is valid, but the account has been disabled"}, + status=status.HTTP_401_UNAUTHORIZED, + ) else: - return Response({ - 'code': -21, - 'detail': 'Invalid username or password'}, - status=status.HTTP_401_UNAUTHORIZED) + return Response( + {"code": -21, "detail": "Invalid username or password"}, status=status.HTTP_401_UNAUTHORIZED + ) else: - return Response({ - 'code': -1, - 'detail': 'Bad parameters'}, - status=status.HTTP_400_BAD_REQUEST) + return Response({"code": -1, "detail": "Bad parameters"}, status=status.HTTP_400_BAD_REQUEST) -class Profile(APIView): +class ProfileView(APIView): """ Get the profile of authenticated user """ + authentication_classes = (JWTAuthentication,) permission_classes = (IsJWTAuthenticated,) def get(self, request): - profile = mongo_db.users.find_one(request.user) + user = User.objects.get(username=request.user) + + profile = Profile.objects.filter(user=user).first() + profile_data = {} if profile: - return Response(profile) - else: - return Response(status=status.HTTP_404_NOT_FOUND) + profile_data.update(profile.data) + + social_auth = SocialAuth.objects.filter(user=user).first() + if social_auth: + if social_auth.provider == "facebook": + profile_data["picture"] = f"https://graph.facebook.com/{social_auth.provider_id}/picture" + profile_data["display-name"] = social_auth.data.get("first_name") + elif social_auth.provider == "google": + profile_data["picture"] = social_auth.data.get("picture") + profile_data["display-name"] = social_auth.data.get("given_name") + else: + profile_data["display-name"] = user.username + + # Compatibility with winds-mobi-js-client + profile_data["_id"] = user.username + if social_auth: + profile_data["user-info"] = social_auth.data + + return Response(profile_data) def delete(self, request): - mongo_db.users.delete_one({'_id': request.user}) user = User.objects.get(username=request.user) user.delete() return Response(status=status.HTTP_204_NO_CONTENT) -class ProfileFavorite(APIView): +class ProfileFavoriteView(APIView): """ Manage favorites stations list """ + authentication_classes = (JWTAuthentication,) permission_classes = (IsJWTAuthenticated,) def post(self, request, station_id): - mongo_db.users.update_one({'_id': request.user}, - {'$addToSet': {'favorites': station_id}}, - upsert=True) + user = User.objects.get(username=request.user) + with transaction.atomic(): + profile, created = Profile.objects.select_for_update().get_or_create(user=user) + favorites = profile.data.setdefault("favorites", []) + if station_id not in favorites: + favorites.append(station_id) + profile.save() return Response(status=status.HTTP_204_NO_CONTENT) def delete(self, request, station_id): - mongo_db.users.update_one({'_id': request.user}, - {'$pull': {'favorites': station_id}}, - upsert=True) + user = User.objects.get(username=request.user) + with transaction.atomic(): + profile = Profile.objects.select_for_update().get(user=user) + if profile and "favorites" in profile.data: + favorites = profile.data["favorites"] + if station_id in favorites: + favorites.remove(station_id) + profile.save() return Response(status=status.HTTP_204_NO_CONTENT) class Oauth2Callback(TemplateView): - template_name = 'winds_mobi_user/oauth2_callback.html' + template_name = "winds_mobi_user/oauth2_callback.html" def authenticate(self): pass - def save_user(self, username, email, user_info): - # Save user in Django + def save_user_auth(self, provider, provider_id, email, user_info): + username = f"{provider}-{provider_id}" # For now, we create a django account for each social auth try: - user = User.objects.get(username=username) + social_auth = SocialAuth.objects.get(provider=provider, provider_id=provider_id) + user = social_auth.user user.email = email + user.data = user_info # Update last_login field when a user does a social login (jwt token expired, ...) - user.last_login = timezone.now() - except User.DoesNotExist: - user = User(username=username, email=email) - user.save() - - # Save user_info - mongo_db.users.update_one({'_id': username}, {'$set': {'user-info': user_info}}, upsert=True) + user.last_login = datetime.now(timezone.utc) + user.save() + except SocialAuth.DoesNotExist: + user, created = User.objects.get_or_create(username=username, defaults={"email": email}) + SocialAuth.objects.create(provider=provider, provider_id=provider_id, user=user, data=user_info) # Generate One Time Token for API authentication - mongo_db.login_ott.create_index([('createdAt', ASCENDING)], expireAfterSeconds=30) - ott = binascii.hexlify(os.urandom(20)).decode('ascii') - mongo_db.login_ott.insert_one({'_id': ott, 'username': username, 'createdAt': datetime.utcnow()}) - + ott = binascii.hexlify(os.urandom(20)).decode("ascii") + redis.set(get_ott_key(ott), username, ex=30) return ott -uri = uri_parser.parse_uri(settings.MONGODB_URL) -mongo_client = MongoClient(uri['nodelist'][0][0], uri['nodelist'][0][1]) -mongo_db = mongo_client[uri['database']] +redis = Redis.from_url(url=settings.REDIS_URL, decode_responses=True) diff --git a/winds_mobi_zermatt/admin.py b/winds_mobi_zermatt/admin.py index 32a5003..bfdadc1 100644 --- a/winds_mobi_zermatt/admin.py +++ b/winds_mobi_zermatt/admin.py @@ -5,5 +5,5 @@ @admin.register(Station) class StationAdmin(admin.ModelAdmin): - ordering = ('id',) - list_display = ('id', 'short_name', 'name', 'latitude', 'longitude', 'altitude') + ordering = ("id",) + list_display = ("id", "short_name", "name", "latitude", "longitude", "altitude") diff --git a/winds_mobi_zermatt/apps.py b/winds_mobi_zermatt/apps.py index ea41c7d..8dfad0e 100644 --- a/winds_mobi_zermatt/apps.py +++ b/winds_mobi_zermatt/apps.py @@ -2,5 +2,5 @@ class ZermattConfig(AppConfig): - name = 'winds_mobi_zermatt' - verbose_name = 'Zermatt' + name = "winds_mobi_zermatt" + verbose_name = "Zermatt" diff --git a/winds_mobi_zermatt/migrations/0001_initial.py b/winds_mobi_zermatt/migrations/0001_initial.py index 9e4cd44..7c7c398 100644 --- a/winds_mobi_zermatt/migrations/0001_initial.py +++ b/winds_mobi_zermatt/migrations/0001_initial.py @@ -7,19 +7,18 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Station', + name="Station", fields=[ - ('id', models.CharField(max_length=50, primary_key=True, serialize=False, verbose_name='Id')), - ('short_name', models.CharField(max_length=20, verbose_name='Short name')), - ('name', models.CharField(max_length=40, verbose_name='Name')), - ('latitude', models.FloatField(verbose_name='Latitude')), - ('longitude', models.FloatField(verbose_name='Longitude')), - ('altitude', models.IntegerField(blank=True, null=True, verbose_name='Altitude')), + ("id", models.CharField(max_length=50, primary_key=True, serialize=False, verbose_name="Id")), + ("short_name", models.CharField(max_length=20, verbose_name="Short name")), + ("name", models.CharField(max_length=40, verbose_name="Name")), + ("latitude", models.FloatField(verbose_name="Latitude")), + ("longitude", models.FloatField(verbose_name="Longitude")), + ("altitude", models.IntegerField(blank=True, null=True, verbose_name="Altitude")), ], ), ] diff --git a/winds_mobi_zermatt/models.py b/winds_mobi_zermatt/models.py index b4ac8d3..57dbc1e 100644 --- a/winds_mobi_zermatt/models.py +++ b/winds_mobi_zermatt/models.py @@ -1,14 +1,14 @@ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class Station(models.Model): - id = models.CharField(_('Id'), max_length=50, primary_key=True) - short_name = models.CharField(_('Short name'), max_length=20) - name = models.CharField(_('Name'), max_length=40) - latitude = models.FloatField(_('Latitude')) - longitude = models.FloatField(_('Longitude')) - altitude = models.IntegerField(_('Altitude'), null=True, blank=True) + id = models.CharField(_("Id"), max_length=50, primary_key=True) + short_name = models.CharField(_("Short name"), max_length=20) + name = models.CharField(_("Name"), max_length=40) + latitude = models.FloatField(_("Latitude")) + longitude = models.FloatField(_("Longitude")) + altitude = models.IntegerField(_("Altitude"), null=True, blank=True) def __str__(self): - return f'{self.short_name} ({self.id})' + return f"{self.short_name} ({self.id})"