diff --git a/.github/workflows/check-python-versions.yml b/.github/workflows/check-python-versions.yml new file mode 100644 index 00000000..553cfe3f --- /dev/null +++ b/.github/workflows/check-python-versions.yml @@ -0,0 +1,19 @@ +name: Check Python versions + +on: + pull_request: + push: + branches: [main] + +jobs: + job: + runs-on: ubuntu-latest + + steps: + + # https://github.com/actions/checkout + - name: Checkout cc-legal-tools-app + uses: actions/checkout@v4 + + - name: Run script to check Python versions + run: ./dev/check_python_versions.sh diff --git a/.github/workflows/django-app-coverage.yml b/.github/workflows/django-app-coverage.yml index d275caa3..2a71c17f 100644 --- a/.github/workflows/django-app-coverage.yml +++ b/.github/workflows/django-app-coverage.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: - DJANGO_SETTINGS_MODULE: cc_legal_tools.settings.ephemeral + DJANGO_SETTINGS_MODULE: cc_legal_tools.settings.dev PYTHONDONTWRITEBYTECODE: 1 PYTHONFAULTHANDLER: 1 @@ -25,10 +25,10 @@ jobs: git config --global user.name "Testing User" # https://github.com/actions/setup-python - - name: Install Python 3.10 + - name: Install Python 3.11 uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install pipenv run: | @@ -49,31 +49,25 @@ jobs: path: cc-legal-tools-data - name: Install Python dependencies - run: | - pipenv sync --dev --system + run: pipenv sync --dev --system working-directory: ./cc-legal-tools-app - name: Check for missing Django migrations - run: | - ./manage.py makemigrations --check + run: ./manage.py makemigrations --check working-directory: ./cc-legal-tools-app - name: Update Django database schema - run: | - ./manage.py migrate + run: ./manage.py migrate working-directory: ./cc-legal-tools-app - name: Coverage Test - run: | - coverage run manage.py test --noinput --parallel 4 + run: coverage run manage.py test --noinput --parallel 4 working-directory: ./cc-legal-tools-app - name: Coverage Combine - run: | - coverage combine + run: coverage combine working-directory: ./cc-legal-tools-app - name: Coverage Report - run: | - coverage report + run: coverage report working-directory: ./cc-legal-tools-app diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index c7cd9534..3b2146f0 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -12,10 +12,10 @@ jobs: steps: # https://github.com/actions/setup-python - - name: Install Python 3.10 + - name: Install Python 3.11 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' - name: Install pipenv run: | diff --git a/.gitignore b/.gitignore index eea065ee..28734dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ # between them and this file: # - https://github.com/github/gitignore/blob/master/Python.gitignore +# OS generated files +.DS_Store +.DS_Store? +.Spotlight-V100 +.Trashes + # Byte-compiled / optimized / DLL files *$py.class *.py[cod] @@ -13,6 +19,7 @@ build/ # Unit test / coverage reports .coverage* +htmlcov # Django development */settings/local.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a2629e0..a0ed9887 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks exclude: \\.coverage.* default_language_version: - python: python3.10 + python: python3.11 repos: diff --git a/Dockerfile b/Dockerfile index 192e6670..1404bb43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,21 @@ # https://docs.docker.com/engine/reference/builder/ # https://hub.docker.com/_/python/ -FROM python:3.10-slim +FROM python:3.11-slim # Configure apt not to prompt during docker build ARG DEBIAN_FRONTEND=noninteractive # Python: disable bytecode (.pyc) files -# https://docs.python.org/3.9/using/cmdline.html +# https://docs.python.org/3.11/using/cmdline.html ENV PYTHONDONTWRITEBYTECODE=1 # Python: force the stdout and stderr streams to be unbuffered -# https://docs.python.org/3.9/using/cmdline.html +# https://docs.python.org/3.11/using/cmdline.html ENV PYTHONUNBUFFERED=1 # Python: enable faulthandler to dump Python traceback on catastrophic cases -# https://docs.python.org/3.9/library/faulthandler.html +# https://docs.python.org/3.11/library/faulthandler.html ENV PYTHONFAULTHANDLER=1 WORKDIR /root @@ -26,38 +26,38 @@ RUN apt-config dump \ | sed -e's/1/0/' \ | tee /etc/apt/apt.conf.d/99no-recommends-no-suggests -# Resynchronize the package index -RUN apt-get update - -# Install apt packages missing from slim docker image -RUN apt-get install -y git ssh - -# Install apt package dependencies for App -RUN apt-get install -y gcc gettext sqlite3 +# Resynchronize the package index and install packages +# https://docs.docker.com/build/building/best-practices/#apt-get +RUN apt-get update && apt-get install -y \ + gcc \ + gettext \ + git \ + sqlite3 \ + ssh \ + && rm -rf /var/lib/apt/lists/* ## Install pipenv -RUN pip install --upgrade pip -RUN pip install --upgrade setuptools -RUN pip install --upgrade pipenv +RUN pip install --upgrade \ + pip \ + pipenv \ + setuptools # Install python dependencies -COPY Pipfile . -COPY Pipfile.lock . +COPY Pipfile Pipfile.lock . RUN pipenv sync --dev --system # Create and switch to a new "cc" user RUN useradd --create-home cc WORKDIR /home/cc USER cc:cc -RUN mkdir .ssh -RUN chmod 0700 .ssh +RUN mkdir .ssh && chmod 0700 .ssh # Configure git for tests -RUN git config --global user.email 'app@docker-container' -RUN git config --global user.name 'App DockerContainer' -RUN git config --global --add safe.directory '*' +RUN git config --global user.email 'app@docker-container' \ + && git config --global user.name 'App DockerContainer' \ + && git config --global --add safe.directory '*' ## Prepare for running app -RUN mkdir cc-legal-tools-app -RUN mkdir cc-legal-tools-data +RUN mkdir cc-legal-tools-app \ + && mkdir cc-legal-tools-data WORKDIR /home/cc/cc-legal-tools-app diff --git a/Pipfile b/Pipfile index 88a693f7..7cee9069 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] Babel = "*" -Django = ">=3.2.25,<3.3" +Django = ">=4.2.17,<4.3" GitPython = ">=3.1.41" PyYAML = "*" beautifulsoup4 = "*" @@ -18,7 +18,7 @@ polib = "*" python-dateutil = "*" rdflib = "*" transifex-python = "*" -urllib3 = ">=2.0.7" # Ensure dependency is secure +urllib3 = ">=2.2.2" # Ensure dependency is secure whitenoise = "*" [dev-packages] @@ -26,17 +26,17 @@ black = ">=24.3.0" # Sync version with [static-analysis], below coverage = "*" django-debug-toolbar = "*" factory-boy = "*" -flake8 = "*" # Sync version with [static-analysis], below -isort = "*" # Sync version with [static-analysis], below -pre-commit = "*" # Sync version with [static-analysis], below +flake8 = "*" # Sync version with [static-analysis], below +isort = "*" # Sync version with [static-analysis], below +pre-commit = "*" # Sync version with [static-analysis], below tblib = "*" # Dependency of coverage (with --parallel) [static-analysis] # Also see: .github/workflows/static-analysis.yml black = ">=24.3.0" # Sync version with [dev-packages], above -flake8 = "*" # Sync version with [dev-packages], above -isort = "*" # Sync version with [dev-packages], above -pre-commit = "*" # Sync version with [dev-packages], above +flake8 = "*" # Sync version with [dev-packages], above +isort = "*" # Sync version with [dev-packages], above +pre-commit = "*" # Sync version with [dev-packages], above [requires] -python_version = "3.10" +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index b8a683c5..da3820bb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "50c668d51ae0acc490abc0ffe9bed861ef731cf387a21eabd4e4d78eb804c1e7" + "sha256": "5317e23ca8f8e30e4e6573b94511c5f023586038034528c7dab94b80c30e57ea" }, "pipfile-spec": 6, "requires": { - "python_version": "3.10" + "python_version": "3.11" }, "sources": [ { @@ -26,19 +26,20 @@ }, "asttokens": { "hashes": [ - "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", - "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0" + "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", + "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2" ], - "version": "==2.4.1" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "babel": { "hashes": [ - "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", - "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" + "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", + "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.14.0" + "markers": "python_version >= '3.8'", + "version": "==2.16.0" }, "beautifulsoup4": { "hashes": [ @@ -51,125 +52,127 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.12.14" }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "colorlog": { "hashes": [ - "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44", - "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33" + "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", + "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==6.8.2" + "version": "==6.9.0" }, "dealer": { "hashes": [ @@ -181,45 +184,45 @@ }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", + "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==3.2.25" + "markers": "python_version >= '3.8'", + "version": "==4.2.17" }, "future": { "hashes": [ "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.0.0" }, "gitdb": { "hashes": [ - "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", - "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" + "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", + "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf" ], "markers": "python_version >= '3.7'", - "version": "==4.0.11" + "version": "==4.0.12" }, "gitpython": { "hashes": [ - "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", - "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff" + "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", + "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.1.43" + "version": "==3.1.44" }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.6" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "isodate": { "hashes": [ @@ -230,165 +233,148 @@ }, "lxml": { "hashes": [ - "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", - "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", - "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", - "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", - "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", - "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", - "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", - "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", - "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", - "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", - "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", - "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", - "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", - "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", - "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", - "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", - "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", - "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", - "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", - "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", - "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", - "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", - "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", - "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", - "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", - "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", - "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", - "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", - "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", - "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", - "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", - "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", - "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", - "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", - "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", - "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", - "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", - "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", - "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", - "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", - "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", - "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", - "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", - "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", - "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", - "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", - "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", - "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", - "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", - "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", - "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", - "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", - "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", - "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", - "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", - "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", - "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", - "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", - "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", - "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", - "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", - "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", - "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", - "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", - "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", - "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", - "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", - "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", - "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", - "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", - "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", - "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", - "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", - "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", - "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", - "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", - "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", - "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", - "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", - "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", - "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", - "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", - "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", - "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", - "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", - "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", - "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", - "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", - "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", - "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", - "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", - "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", - "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", - "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", - "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", - "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", - "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", - "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", - "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", - "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", - "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", - "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", - "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", - "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", - "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", - "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", - "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", - "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", - "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", - "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f", - "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", - "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", - "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", - "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", - "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", - "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", - "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", - "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", - "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", - "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", - "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", - "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", - "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", - "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", - "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", - "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", - "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", - "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", - "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", - "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", - "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", - "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", - "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", - "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", - "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", - "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", - "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", - "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", - "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", - "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", - "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", - "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", - "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", - "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", - "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", - "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", - "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", - "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", - "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", - "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", - "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", - "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", - "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", - "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", - "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" + "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", + "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", + "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", + "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", + "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", + "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", + "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", + "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", + "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", + "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", + "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", + "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", + "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", + "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", + "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", + "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", + "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", + "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", + "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", + "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", + "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", + "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", + "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", + "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", + "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", + "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", + "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", + "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", + "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", + "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", + "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", + "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", + "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", + "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", + "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", + "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", + "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", + "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", + "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", + "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", + "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", + "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", + "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", + "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", + "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", + "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", + "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", + "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", + "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", + "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", + "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", + "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", + "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", + "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", + "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", + "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", + "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", + "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", + "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", + "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", + "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", + "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", + "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", + "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", + "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", + "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", + "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", + "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", + "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", + "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", + "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", + "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", + "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", + "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", + "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", + "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", + "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", + "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", + "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", + "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", + "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", + "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", + "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", + "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", + "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", + "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", + "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", + "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", + "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", + "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", + "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", + "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", + "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", + "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", + "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", + "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", + "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", + "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", + "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", + "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", + "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", + "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", + "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", + "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", + "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", + "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", + "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", + "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", + "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", + "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", + "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", + "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", + "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", + "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", + "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", + "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", + "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", + "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", + "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", + "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", + "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", + "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", + "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", + "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", + "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", + "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", + "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", + "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", + "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", + "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", + "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", + "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", + "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", + "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", + "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", + "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", + "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", + "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==5.2.1" + "version": "==5.3.0" }, "parsimonious": { "hashes": [ @@ -407,11 +393,11 @@ }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", + "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a" ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.2" + "markers": "python_version >= '3.9'", + "version": "==3.2.1" }, "pyseeyou": { "hashes": [ @@ -425,229 +411,225 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, - "pytz": { - "hashes": [ - "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", - "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" - ], - "version": "==2024.1" - }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "rdflib": { "hashes": [ - "sha256:0438920912a642c866a513de6fe8a0001bd86ef975057d6962c79ce4771687cd", - "sha256:9995eb8569428059b8c1affd26b25eac510d64f5043d9ce8c84e0d0036e995ae" + "sha256:164de86bd3564558802ca983d84f6616a4a1a420c7a17a8152f5016076b2913e", + "sha256:e590fa9a2c34ba33a667818b5a84be3fb8a4d85868f8038f17912ec84f912a25" ], "index": "pypi", "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==7.0.0" + "version": "==7.1.1" }, "regex": { "hashes": [ - "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5", - "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770", - "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc", - "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105", - "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d", - "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b", - "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9", - "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630", - "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6", - "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c", - "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482", - "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6", - "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a", - "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80", - "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5", - "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1", - "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f", - "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf", - "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb", - "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2", - "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347", - "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20", - "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060", - "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5", - "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73", - "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f", - "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d", - "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3", - "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae", - "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4", - "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2", - "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457", - "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c", - "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4", - "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87", - "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0", - "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704", - "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f", - "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f", - "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b", - "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5", - "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923", - "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715", - "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c", - "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca", - "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1", - "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756", - "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360", - "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc", - "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445", - "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e", - "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4", - "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a", - "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8", - "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53", - "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697", - "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf", - "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a", - "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415", - "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f", - "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9", - "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400", - "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d", - "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392", - "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb", - "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd", - "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861", - "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232", - "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95", - "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7", - "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39", - "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887", - "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5", - "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39", - "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb", - "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586", - "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97", - "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423", - "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69", - "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7", - "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1", - "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7", - "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5", - "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8", - "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91", - "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590", - "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe", - "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c", - "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64", - "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd", - "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa", - "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31", - "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988" + "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", + "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", + "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", + "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", + "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", + "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", + "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", + "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", + "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", + "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", + "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", + "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", + "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", + "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", + "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", + "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", + "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", + "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", + "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", + "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", + "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", + "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", + "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", + "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", + "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", + "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", + "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", + "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", + "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", + "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", + "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", + "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", + "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", + "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", + "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", + "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", + "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", + "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", + "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", + "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", + "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", + "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", + "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", + "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", + "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", + "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", + "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", + "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", + "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", + "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", + "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", + "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", + "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", + "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", + "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", + "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", + "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", + "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", + "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", + "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", + "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", + "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", + "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", + "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", + "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", + "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", + "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", + "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", + "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", + "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", + "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", + "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", + "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", + "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", + "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", + "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", + "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", + "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", + "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", + "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", + "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", + "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", + "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", + "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", + "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", + "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", + "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", + "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", + "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", + "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", + "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", + "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", + "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", + "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" ], - "markers": "python_version >= '3.7'", - "version": "==2023.12.25" + "markers": "python_version >= '3.8'", + "version": "==2024.11.6" }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" }, "smmap": { "hashes": [ - "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", - "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" + "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", + "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e" ], "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "version": "==5.0.2" }, "soupsieve": { "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9" ], "markers": "python_version >= '3.8'", - "version": "==2.5" + "version": "==2.6" }, "sqlparse": { "hashes": [ - "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", - "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", + "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.4" + "markers": "python_version >= '3.8'", + "version": "==0.5.3" }, "toolz": { "hashes": [ - "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85", - "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d" + "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", + "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02" ], - "markers": "python_version >= '3.7'", - "version": "==0.12.1" + "markers": "python_version >= '3.8'", + "version": "==1.0.0" }, "transifex-python": { "hashes": [ @@ -656,31 +638,23 @@ "index": "pypi", "version": "==3.5.0" }, - "typing-extensions": { - "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" - ], - "markers": "python_version < '3.11'", - "version": "==4.10.0" - }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "whitenoise": { "hashes": [ - "sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251", - "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" + "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4", + "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==6.6.0" + "markers": "python_version >= '3.9'", + "version": "==6.8.2" } }, "develop": { @@ -694,32 +668,32 @@ }, "black": { "hashes": [ - "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", - "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", - "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", - "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", - "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", - "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", - "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", - "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", - "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", - "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", - "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", - "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", - "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", - "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", - "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", - "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", - "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", - "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", - "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", - "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", - "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", - "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" + "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", + "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", + "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", + "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", + "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", + "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", + "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", + "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", + "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", + "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", + "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", + "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", + "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", + "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", + "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", + "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", + "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", + "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", + "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", + "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", + "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", + "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "markers": "python_version >= '3.9'", + "version": "==24.10.0" }, "cfgv": { "hashes": [ @@ -731,137 +705,146 @@ }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "coverage": { "hashes": [ - "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", - "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", - "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", - "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", - "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", - "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", - "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", - "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", - "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", - "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", - "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", - "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", - "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", - "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", - "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", - "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", - "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", - "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", - "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", - "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", - "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", - "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", - "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", - "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", - "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", - "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", - "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", - "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", - "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", - "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", - "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", - "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", - "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", - "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", - "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", - "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", - "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", - "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", - "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", - "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", - "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", - "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", - "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", - "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", - "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", - "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", - "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", - "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", - "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", - "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", - "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", - "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", + "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", + "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", + "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", + "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", + "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", + "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", + "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", + "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", + "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", + "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", + "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", + "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", + "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", + "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", + "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", + "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", + "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", + "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", + "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", + "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", + "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", + "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", + "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", + "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", + "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", + "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", + "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", + "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", + "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", + "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", + "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", + "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", + "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", + "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", + "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", + "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", + "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", + "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", + "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", + "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", + "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", + "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", + "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", + "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", + "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", + "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", + "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", + "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", + "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", + "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", + "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", + "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", + "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", + "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", + "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", + "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", + "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", + "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", + "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", + "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", + "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==7.4.4" + "markers": "python_version >= '3.9'", + "version": "==7.6.10" }, "distlib": { "hashes": [ - "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", - "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" ], - "version": "==0.3.8" + "version": "==0.3.9" }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", + "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc" ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==3.2.25" + "markers": "python_version >= '3.8'", + "version": "==4.2.17" }, "django-debug-toolbar": { "hashes": [ - "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4", - "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" + "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044", + "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.3.0" + "version": "==4.4.6" }, "factory-boy": { "hashes": [ - "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c", - "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1" + "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca", + "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.3.0" + "markers": "python_version >= '3.8'", + "version": "==3.3.1" }, "faker": { "hashes": [ - "sha256:998c29ee7d64429bd59204abffa9ba11f784fb26c7b9df4def78d1a70feb36a7", - "sha256:a5ddccbe97ab691fad6bd8036c31f5697cfaa550e62e000078d1935fa8a7ec2e" + "sha256:2abb551a05b75d268780b6095100a48afc43c53e97422002efbfc1272ebf5f26", + "sha256:ae074d9c7ef65817a93b448141a5531a16b2ea2e563dc5774578197c7c84060c" ], "markers": "python_version >= '3.8'", - "version": "==24.4.0" + "version": "==33.3.0" }, "filelock": { "hashes": [ - "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb", - "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], "markers": "python_version >= '3.8'", - "version": "==3.13.3" + "version": "==3.16.1" }, "flake8": { "hashes": [ - "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", - "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", + "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==7.0.0" + "version": "==7.1.1" }, "identify": { "hashes": [ - "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791", - "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e" + "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", + "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc" ], - "markers": "python_version >= '3.8'", - "version": "==2.5.35" + "markers": "python_version >= '3.9'", + "version": "==2.6.5" }, "isort": { "hashes": [ @@ -890,19 +873,19 @@ }, "nodeenv": { "hashes": [ - "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", - "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.8.0" + "version": "==1.9.1" }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.2" }, "pathspec": { "hashes": [ @@ -914,28 +897,28 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.3.6" }, "pre-commit": { "hashes": [ - "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab", - "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060" + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.7.0" + "version": "==4.0.1" }, "pycodestyle": { "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", + "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" ], "markers": "python_version >= '3.8'", - "version": "==2.11.1" + "version": "==2.12.1" }, "pyflakes": { "hashes": [ @@ -950,98 +933,83 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, - "pytz": { - "hashes": [ - "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", - "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" - ], - "version": "==2024.1" - }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "setuptools": { - "hashes": [ - "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", - "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], "markers": "python_version >= '3.8'", - "version": "==69.2.0" + "version": "==6.0.2" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" }, "sqlparse": { "hashes": [ - "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", - "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", + "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.4" + "markers": "python_version >= '3.8'", + "version": "==0.5.3" }, "tblib": { "hashes": [ @@ -1052,60 +1020,52 @@ "markers": "python_version >= '3.8'", "version": "==3.0.0" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, "typing-extensions": { "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.11'", - "version": "==4.10.0" + "markers": "python_version >= '3.8'", + "version": "==4.12.2" }, "virtualenv": { "hashes": [ - "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a", - "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197" + "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb", + "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329" ], - "markers": "python_version >= '3.7'", - "version": "==20.25.1" + "markers": "python_version >= '3.8'", + "version": "==20.28.1" } }, "static-analysis": { "black": { "hashes": [ - "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", - "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", - "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", - "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", - "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", - "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", - "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", - "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", - "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", - "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", - "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", - "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", - "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", - "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", - "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", - "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", - "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", - "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", - "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", - "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", - "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", - "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" + "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", + "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", + "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", + "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", + "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", + "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", + "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", + "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", + "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", + "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", + "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", + "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", + "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", + "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", + "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", + "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", + "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", + "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", + "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", + "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", + "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", + "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "version": "==24.8.0" }, "cfgv": { "hashes": [ @@ -1132,28 +1092,28 @@ }, "filelock": { "hashes": [ - "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb", - "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546" + "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec", + "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609" ], "markers": "python_version >= '3.8'", - "version": "==3.13.3" + "version": "==3.16.0" }, "flake8": { "hashes": [ - "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", - "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", + "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==7.0.0" + "version": "==7.1.1" }, "identify": { "hashes": [ - "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791", - "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e" + "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", + "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0" ], "markers": "python_version >= '3.8'", - "version": "==2.5.35" + "version": "==2.6.0" }, "isort": { "hashes": [ @@ -1182,19 +1142,19 @@ }, "nodeenv": { "hashes": [ - "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", - "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.8.0" + "version": "==1.9.1" }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -1206,28 +1166,28 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", + "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.3.2" }, "pre-commit": { "hashes": [ - "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab", - "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060" + "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", + "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.7.0" + "version": "==3.8.0" }, "pycodestyle": { "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", + "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" ], "markers": "python_version >= '3.8'", - "version": "==2.11.1" + "version": "==2.12.1" }, "pyflakes": { "hashes": [ @@ -1239,93 +1199,71 @@ }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "setuptools": { - "hashes": [ - "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", - "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" - ], "markers": "python_version >= '3.8'", - "version": "==69.2.0" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" - ], - "markers": "python_version < '3.11'", - "version": "==4.10.0" + "version": "==6.0.2" }, "virtualenv": { "hashes": [ - "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a", - "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197" + "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55", + "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c" ], "markers": "python_version >= '3.7'", - "version": "==20.25.1" + "version": "==20.26.4" } } } diff --git a/README.md b/README.md index e884a324..21bb51ea 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ repository. [repodata]:https://github.com/creativecommons/cc-legal-tools-data - -## Code of conduct +## Code of Conduct [`CODE_OF_CONDUCT.md`][org-coc]: > The Creative Commons team is committed to fostering a welcoming community. @@ -23,81 +22,101 @@ repository. [reporting_guide]: https://opensource.creativecommons.org/community/code-of-conduct/enforcement/ -## Contributing +## About -See [`CONTRIBUTING.md`][org-contrib]. +This application manages 639 legal tools (636 licenses and 3 public domain +tools). The current version of the licenses is 4.0 and includes 6 licenses. +They are international and are designed to operate globally, ensuring they are +robust, enforceable and easily adopted worldwide. Prior versions were adapted +to specific jurisdictions ("ported"). That is why there are 636 licenses. -[org-contrib]: https://github.com/creativecommons/.github/blob/main/CONTRIBUTING.md +Broadly speaking, each legal tool consists of three layers: +1. `deed`: a plain language summary of the legal tool +2. `legalcode`: the legal tool itself +3. `rdf`: metadata about the legal tool in RDF/XML format +With translations of the deed and translations of the legal code, this +application manages over 30,000 documents. -## Not the live site + +### Not the live site This project is not intended to serve the legal tools directly. Instead, a command line tool can be used to save all the rendered HTML and RDF/XML pages -as files. Then those files are used as part of the real CreativeCommons.org +as files. Then those files are used as part of the CreativeCommons.org site (served as static files). -## Software Versions - -- [Python 3.10][python310] specified in: - - [`.github/workflows/django-app-coverage.yml`][django-app-coverage] - - [`.github/workflows/static-analysis.yml`][static-analysis] - - [`.pre-commit-config.yaml`](.pre-commit-config.yaml) - - [`Dockerfile`](Dockerfile) - - [`Pipfile`](Pipfile) - - [`pyproject.toml`](pyproject.toml) -- [Django 3.2 (LTS)][django32] - - [`Pipfile`](Pipfile) - -[django-app-coverage]: .github/workflows/django-app-coverage.yml -[static-analysis]: .github/workflows/static-analysis.yml -[python310]: https://docs.python.org/3.10/ -[django32]: https://docs.djangoproject.com/en/3.2/ +## Setup and Usage +Once this project's required dependencies (Docker, Git, etc.) are enabled on +your system, you will be able to run the legal-tools application and generate +static files. -## Setting up the Project +For information on learning and installing the prerequisite technologies for +this project, please see [Foundational technologies — Creative Commons Open +Source][found-tech]. +[found-tech]: https://opensource.creativecommons.org/contributing-code/foundational-tech/ -### Data Repository -Visit [Cloning a Repository][gitclone] on how to clone a GitHub repository. +### Codebases Setup -The [creativecommons/cc-legal-tools-data][repodata] project repository should -be cloned into a directory adjacent to this one: +Both this repository and the [creativecommons/cc-legal-tools-data][repodata] +project repository should be cloned side by side, resulting in a structure like +the following: ``` -PARENT_DIR -├── cc-legal-tools-app (git clone of this repository) -└── cc-legal-tools-data (git clone of the cc-legal-tools-data repository) +creative-commons/ +├── cc-legal-tools-app/ (git clone of this repository) +└── cc-legal-tools-data/ (git clone of the cc-legal-tools-data repository) ``` -If it is not cloned into the default location, the Django -`DATA_REPOSITORY_DIR` Django configuration setting, or the -`DATA_REPOSITORY_DIR` environment variable can be used to configure its -location. +To achieve this, we recommend the following procedure: + +1. Create and change to a container directory, such as `creative-commons` or `cc`. + ```shell + mkdir creative-commons + cd creative-commons + ``` +2. Clone both repos using SSH or, if that does not work, HTTPS protocol. + ```shell + git clone git@github.com:creativecommons/cc-legal-tools-app.git + git clone git@github.com:creativecommons/cc-legal-tools-data.git + ``` + or + ```shell + git clone https://github.com/creativecommons/cc-legal-tools-app.git + git clone https://github.com/creativecommons/cc-legal-tools-data.git + ``` + +Visit [Cloning a repository - GitHub Docs][gitclone] for more on how to clone a +GitHub repository. [gitclone]:https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository -[repodata]:https://github.com/creativecommons/cc-legal-tools-data -### Docker Compose Setup +### Docker Prep and Initial Execution -Use the following instructions to start the project with Docker compose. -Pleaes note that CC staff use macOS for development--please help us with -documenting other operating systems if you encounter issues. +Use the following instructions to prepare and run the project with Docker +Compose. -1. Ensure the [Data Repository](#data-repository), above, is in place -2. [Install Docker Engine](https://docs.docker.com/engine/install/) -3. Ensure you are at the top level of the directory where you cloned this repository (where `manage.py` is) -4. Create Django local settings file +1. Ensure all prerequisites and repositories are in place. +2. Ensure you are at the top level of the directory where you cloned this +repository (where `manage.py` is). + ```shell + cd cc-legal-tools-app + ``` +3. Create Django local settings file from the example file. ```shell cp cc_legal_tools/settings/local.example.py cc_legal_tools/settings/local.py ``` -5. Build the containers + - Update variables in new file, if necessary. + - This file is ignored by Git. +4. Build the containers. ```shell docker compose build ``` -6. **Run the containers** +5. **Run the containers.** ```shell docker compose up ``` @@ -107,87 +126,190 @@ documenting other operating systems if you encounter issues. transparently as long as the development server is running. 2. **static** ([127.0.0.1:8006](http://127.0.0.1:8006/)): a static web server serving [creativecommons/cc-legal-tools-data][repodata]:`docs/` -7. Run database migrations +6. Initialize data. +Open a separate terminal tab, and in the same directory, run: ```shell - docker compose exec app ./manage.py migrate + ./dev/init_data.sh ``` -8. Clear data in the database - ```shell - docker compose exec app ./manage.py clear_license_data - ``` -9. Load legacy HTML in the database + 1. Deletes database (which may not yet exist) + 2. Initializes database + 3. Performs database migrations + 4. Creates supseruser (will prompt for password) + 5. Loads data + +Note: Once this full setup is performed, running Step 5 above will execute the +application on any subsequent occasion. + + +## Project Usage + +With the prerequisites installed and built, these tools can be used to generate +and manage data from the associated [data repository][repodata]. + + +### Data + +The legal tools metadata is in a database. The metadata tracks which legal +tools exist, their translations, their ports, and their characteristics like +what they permit, require, and prohibit. + +~~The metadata can be downloaded by visiting the URL path: +`127.0.0.1:8005`[`/licenses/metadata.yaml`][metadata]~~ (currently disabled) + +[metadata]: http://127.0.0.1:8005/licenses/metadata.yaml + +There are two main models (Django terminology for tables) in +[`legal_tools/models.py`](legal_tools/models.py): +1. `LegalCode` +2. `Tool` + +A Tool can be identified by a `unit` (ex. `by`, `by-nc-sa`, `devnations`) which +is a proxy for the complete set of permissions, requirements, and prohibitions; +a `version` (ex. `4.0`, `3.0)`, and an optional `jurisdiction` for ports. So we +might refer to the tool by its **identifier** "BY 3.0 AM" which would be the +3.0 version of the BY license terms as ported to the Armenia jurisdiction. For +additional information see: [**Legal Tools Namespace** - +creativecommons/cc-legal-tools-data: CC Legal Tools Data (static HTML, language +files, etc.)][namespace]. + +There are three places legal code text could be: +1. **Gettext files** (`.po` and `.mo`) in the + [creativecommons/cc-legal-tools-data][repodata] repository (legal tools with + full translation support): + - 4.0 Licenses + - CC0 +2. **Django template** + ([`legalcode_licenses_3.0_unported.html`][unportedtemplate]): + - Unported 3.0 Licenses (English-only) +3. **`html` field** (in the `LegalCode` model): + - Everything else + +The text that's in gettext files can be translated via Transifex at [Creative +Commons localization][cctransifex]. For additional information on the Django +translation domains / Transifex resources, see [How the license translation is +implemented](#how-the-tool-translation-is-implemented), below. + +Documentation: +- [Models | Django documentation | Django][djangomodels] +- [Templates | Django documentation | Django][djangotemplates] + +[namespace]: https://github.com/creativecommons/cc-legal-tools-data#legal-tools-namespace +[unportedtemplate]: templates/includes/legalcode_licenses_3.0_unported.html +[cctransifex]: https://www.transifex.com/creativecommons/public/ +[djangomodels]: https://docs.djangoproject.com/en/4.2/topics/db/models/ +[djangotemplates]: https://docs.djangoproject.com/en/4.2/topics/templates/ + + +### Translation + +See [`docs/translation.md`](docs/translation.md) + + +### Generate Static Files + +Generating static files updates the static files in the `docs/` directory of +the [creativecommons/cc-legal-tools-data][repodata] repository (the [Data +Repository](#data-repository), above). + + +#### Static Files Process + +This process will write the HTML files in the cc-legal-tools-data clone +directory under `docs/`. It will not commit the changes (`--nogit`) and will +not push any commits (`--nopush` is implied by `--nogit`). + +1. Ensure the [Data Repository](#data-repository), above, is in place +2. Ensure [Docker Compose Setup](#docker-compose-setup), above, is complete +3. Delete the contents of the `docs/` directory and then recreate/copy the + static files it should contain: ```shell - docker compose exec app ./manage.py load_html_files + docker compose exec app ./manage.py publish -v2 ``` -[repodata]:https://github.com/creativecommons/cc-legal-tools-data + +#### Publishing Changes to Git Repo + +When the site is deployed, to enable pushing and pulling the licenses data repo +with GitHub, create an SSH deploy key for the cc-legal-tools-data repo with +write permissions, and put the private key file (not password protected) +somewhere safe (owned by `www-data` if on a server), and readable only by its +owner (0o400). Then in settings, make `TRANSLATION_REPOSITORY_DEPLOY_KEY` be +the full path to that deploy key file. + + +#### Publishing Dependency Documentation + +- [Beautiful Soup Documentation — Beautiful Soup 4 documentation][bs4docs] + - [lxml - Processing XML and HTML with Python][lxml] +- [GitPython Documentation — GitPython documentation][gitpythondocs] + +[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +[gitpythondocs]: https://gitpython.readthedocs.io/en/stable/index.html +[lxml]: https://lxml.de/ + + +### Machine/metadata Layer: RDF/XML + +For details and history, see [`docs/rdf.md`](docs/rdf.md). + + +## Development + +Inside the Docker container, the Python-based tooling centers around by pre-commit and Django. + +See the [Code of Conduct](#code-of-conduct) above and the [Code of Conduct document][code_of_conduct] for more information on participant expectations and responsibilities. + + +### Contributing + +See [`CONTRIBUTING.md`][org-contrib]. + +[org-contrib]: https://github.com/creativecommons/.github/blob/main/CONTRIBUTING.md ### Manual Setup > :warning: **This section may be helpful for maintaining the project, but -> should NOT be used for development. Please use the Docker Compose Setup, +> should _NOT_ be used for development. Please use the Docker Compose Setup, > above.** -1. Development Environment - 1. Ensure the [Data Repository](#data-repository), above, is in place - 2. Install dependencies +1. Complete Docker Compose Setup, above +2. Development Environment + 1. Install dependencies - Linux: ```shell - sudo apt-get install python3.9 python3.9-dev python3-pip + sudo apt-get install python3.11 python3.11-dev python3-pip ``` ```shell pip3 install pipenv ``` - macOS: via [Homebrew](https://brew.sh/): ```shell - brew install pipenv python@3.9 + brew install pipenv python@3.11 ``` - Windows: [install Python][python-windows] and then use `pip` to install `pipenv`: ```shell pip install pipenv ``` - 3. Install Python environment and modules via pipenv to create a + 2. Install Python environment and modules via pipenv to create a virtualenv - Linux: ```shell - pipenv install --dev --python /usr/bin/python3.9 + pipenv install --dev --python /usr/bin/python3.11 ``` - macOS: via [Homebrew](https://brew.sh/): ```shell - pipenv install --dev --python /usr/local/opt/python@3.9/libexec/bin/python + pipenv install --dev --python /usr/local/opt/python@3.11/libexec/bin/python ``` - Windows: ```shell pipenv install --dev --python \User\Appdata\programs\python ``` - 4. Install pre-commit hooks + 3. Install pre-commit hooks ```shell pipenv run pre-commit install ``` -2. Configure Django - 1. Create Django local settings file - ```shell - cp cc_legal_tools/settings/local.example.py cc_legal_tools/settings/local.py - ``` - 2. Create project database - - Linux: - ```shell - sudo createdb -E UTF-8 cc_legal_tools - ``` - - macOS: - ```shell - createdb -E UTF-8 cc_legal_tools - ``` - - Windows: - ```shell - createdb -E UTF-8 cc_legal_tools - ``` - 3. Load database schema - ```shell - pipenv run ./manage.py migrate - ``` 3. Run development server ([127.0.0.1:8005](http://127.0.0.1:8005/)) ```shell pipenv run ./manage.py runserver @@ -198,15 +320,28 @@ documenting other operating systems if you encounter issues. [python-windows]:https://www.pythontutorial.net/getting-started/install-python/ -#### Manual Commands +### Software Versions -> :information_source: The rest of the documentation assumes Docker. If you are -> using a manual setup, use `pipenv run` instead of `docker compose exec app` -> for the commands below. +These are the currently designated versions of the various dependencies: +- [Python 3.11][python311] specified in: + - [`.github/workflows/django-app-coverage.yml`][django-app-coverage] + - [`.github/workflows/static-analysis.yml`][static-analysis] + - [`.pre-commit-config.yaml`](.pre-commit-config.yaml) + - [`Dockerfile`](Dockerfile) + - [`Pipfile`](Pipfile) + - [`pyproject.toml`](pyproject.toml) +- [Django 4.2 (LTS)][django42] + - [`Pipfile`](Pipfile) + +[django-app-coverage]: .github/workflows/django-app-coverage.yml +[static-analysis]: .github/workflows/static-analysis.yml +[python311]: https://docs.python.org/3.11/ +[django42]: https://docs.djangoproject.com/en/4.2/ -### Tooling +### Developer Resources +These resources are available for developing this tooling: - **[Python Guidelines — Creative Commons Open Source][ccospyguide]** - [Black][black]: the uncompromising Python code formatter - [Coverage.py][coveragepy]: Code coverage measurement for Python @@ -229,7 +364,12 @@ documenting other operating systems if you encounter issues. [precommit]: https://pre-commit.com/ -#### Helper Scripts +> :information_source: The rest of the documentation assumes Docker. If you are +> using a manual setup, use `pipenv run` instead of `docker compose exec app` +> for the commands below. + + +### Helper Scripts Best run before every commit: - `./dev/coverage.sh` - Run coverage tests and report @@ -241,19 +381,20 @@ Run as needed: - Run after each new release of [creativecommons/vocabulary-theme][vocab-theme] +Data management: +- `./dev/dump_data.sh` - Dump Django application data +- `./dev/init_data.sh` - :warning: Initialize Django application data +- `./dev/load_data.sh` - Load Django application data + Esoteric and dangerous: -- `./dev/concatenatemessages.sh` - Concatenate legacy ccEngine translations - into cc-legal-tools-app - - rarely used (only after source strings are updated) -- `./dev/resetdb.sh` - Reset Django application database data (!!DANGER!!) - - usually only helpful if you're doing model/schema work -- `./dev/updatemessages.sh` - Run Django Management nofuzzy_makemessages with - helpful options (including excluding legalcode) and compilemessages +- `./dev/updatemessages.sh` - :warning: Run Django Management + nofuzzy_makemessages with helpful options (including excluding legalcode) and + compilemessages [vocab-theme]: https://github.com/creativecommons/vocabulary-theme -#### Coverage Tests and Report +### Coverage Tests and Report The coverage tests and report are run as part of pre-commit and as a GitHub Action. To run it manually: @@ -272,7 +413,7 @@ Action. To run it manually: ### Commit Errors -#### Error building trees +#### Error Building Trees If you encounter an `error: Error building trees` error from pre-commit when you commit, try adding your files (`git add `) before committing them. @@ -287,275 +428,6 @@ The following CC projects are used to achieve a consistent look and feel: [vocabulary-theme]: https://github.com/creativecommons/vocabulary-theme -## Data - -The legal tools metadata is in a database. The metadata tracks which legal -tools exist, their translations, their ports, and their characteristics like -what they permit, require, and prohibit. - -~~The metadata can be downloaded by visiting the URL path: -`127.0.0.1:8005`[`/licenses/metadata.yaml`][metadata]~~ (currently disabled) - -[metadata]: http://127.0.0.1:8005/licenses/metadata.yaml - -There are two main models (Django terminology for tables) in -[`legal_tools/models.py`](legal_tools/models.py): -1. `LegalCode` -2. `Tool` - -A Tool can be identified by a `unit` (ex. `by`, `by-nc-sa`, `devnations`) which -is a proxy for the complete set of permissions, requirements, and prohibitions; -a `version` (ex. `4.0`, `3.0)`, and an optional `jurisdiction` for ports. So we -might refer to the tool by its **identifier** "BY 3.0 AM" which would be the -3.0 version of the BY license terms as ported to the Armenia jurisdiction. For -additional information see: [**Legal Tools Namespace** - -creativecommons/cc-legal-tools-data: CC Legal Tools Data (static HTML, language -files, etc.)][namespace]. - -There are three places legal code text could be: -1. **Gettext files** (`.po` and `.mo`) in the - [creativecommons/cc-legal-tools-data][repodata] repository (legal tools with - full translation support): - - 4.0 Licenses - - CC0 -2. **Django template** - ([`legalcode_licenses_3.0_unported.html`][unportedtemplate]): - - Unported 3.0 Licenses (English-only) -3. **`html` field** (in the `LegalCode` model): - - Everything else - -The text that's in gettext files can be translated via Transifex at [Creative -Commons localization][cctransifex]. For additional information on the Django -translation domains / Transifex resources, see [How the license translation is -implemented](#how-the-tool-translation-is-implemented), below. - -Documentation: -- [Models | Django documentation | Django][djangomodels] -- [Templates | Django documentation | Django][djangotemplates] - -[namespace]: https://github.com/creativecommons/cc-legal-tools-data#legal-tools-namespace -[unportedtemplate]: templates/includes/legalcode_licenses_3.0_unported.html -[cctransifex]: https://www.transifex.com/creativecommons/public/ -[djangomodels]: https://docs.djangoproject.com/en/3.2/topics/db/models/ -[djangotemplates]: https://docs.djangoproject.com/en/3.2/topics/templates/ - - -## Importing the existing legal tool text - -The process of getting the text into the site varies by legal tool. - -Note that once the site is up and running in production, the data in the site -will become the canonical source, and the process described here should not -need to be repeated after that. - -The implementation is the Django management command `load_html_files`, which -reads from the legacy HTML legal code files in the -[creativecommons/cc-legal-tools-data][repodata] repository, and populates the -database records and translation files. - -`load_html_files` uses [BeautifulSoup4][bs4docs] to parse the legacy HTML legal -code: -1. `import_zero_license_html()` for CC0 Public Domain tool - - HTML is handled specifically (using tag ids and classes) to populate - translation strings and to be used with specific HTML formatting when - displayed via template -2. `import_by_40_license_html()` for 4.0 License tools - - HTML is handled specifically (using tag ids and classes) to populate - translation strings and to be used with specific HTML formatting when - displayed via a template -3. `import_by_30_unported_license_html()` for unported 3.0 License tools - (English-only) - - HTML is handled specifically to be used with specific HTML formatting - when displayed via a template -4. `simple_import_license_html()` for everything else - - HTML is handled generically; only the title and license body are - identified. The body is stored in the `html` field of the - `LegalCode` model - -[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ -[repodata]: https://github.com/creativecommons/cc-legal-tools-data - - -### Import Process - -This process will read the HTML files from the specified directory, populate -`LegalCode` and `Tool` models, and create the `.po` portable object Gettext -files in [creativecommons/cc-legal-tools-data][repodata]. - -1. Ensure the [Data Repository](#data-repository), above, is in place -2. Ensure [Docker Compose Setup](#docker-compose-setup), above, is complete -3. Clear data in the database - ```shell - docker compose exec app ./manage.py clear_license_data - ``` -4. Load legacy HTML in the database - ```shell - docker compose exec app ./manage.py load_html_files - ``` -5. Optionally (and only as appropriate): - 1. Commit the `.po` portable object Gettext file changes in - [creativecommons/cc-legal-tools-data][repodata] - 2. [Translation Update Process](#translation-update-process), below - 3. [Generate Static Files](#generate-static-files), below - -[repodata]:https://github.com/creativecommons/cc-legal-tools-data - - -### Import Dependency Documentation - -- [Beautiful Soup Documentation — Beautiful Soup 4 documentation][bs4docs] - - [lxml - Processing XML and HTML with Python][lxml] -- [Quick start guide — polib documentation][polibdocs] - -[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ -[lxml]: https://lxml.de/ -[polibdocs]: https://polib.readthedocs.io/en/latest/quickstart.html - - -## Translation - -To upload/download translation files to/from Transifex, you'll need an account -there with access to these translations. Then follow the [Authentication - -Transifex API v3][transauth]: to get an API token, and set -`TRANSIFEX["API_TOKEN"]` in your environment with its value. - -The [creativecommons/cc-legal-tools-data][repodata] repository must be cloned -next to this `cc-legal-tools-app` repository. (It can be elsewhere, then you -need to set `DATA_REPOSITORY_DIR` to its location.) Be sure to clone using a -URL that starts with `git@github...` and not `https://github...`, or you won't -be able to push to it. Also see [Data Repository](#data-repository), above. - -In production, the `check_for_translation_updates` management command should be -run hourly. See [Check for Translation -Updates](#check-for-translation-updates), below. - -Also see [Publishing changes to git repo](#publishing-changes-to-git-repo), -below. - -[Babel][babel] is used for localization information. - -Documentation: -- [Babel — Babel documentation][babel] -- [Translation | Django documentation | Django][djangotranslation] - -[babel]: http://babel.pocoo.org/en/latest/index.html -[repodata]:https://github.com/creativecommons/cc-legal-tools-data -[transauth]: https://transifex.github.io/openapi/index.html#section/Authentication - - -### How the tool translation is implemented - -Django Translation uses two sets of Gettext Files in the -[creativecommons/cc-legal-tools-data][repodata] repository (the [Data -Repository](#data-repository), above). See that repository for detailed -information and definitions. - -Documentation: -- [Translation | Django documentation | Django][djangotranslation] -- Transifex API - - [Introduction to API 3.0 | Transifex Documentation][api30intro] - - [Transifex API v3][api30] - - Python SDK: [transifex-python/transifex/api][apisdk] - -[api30]: https://transifex.github.io/openapi/index.html#section/Introduction -[api30intro]: https://docs.transifex.com/api-3-0/introduction-to-api-3-0 -[apisdk]: https://github.com/transifex/transifex-python/tree/devel/transifex/api -[djangotranslation]: https://docs.djangoproject.com/en/3.2/topics/i18n/translation/ -[repodata]: https://github.com/creativecommons/cc-legal-tools-data - - -### Check for Translation Updates - -> :warning: **This functionality is currently disabled.** - -The hourly run of `check_for_translation_updates` looks to see if any of the -translation files in Transifex have newer last modification times than we know -about. It performs the following process (which can also be done manually: - -1. Ensure the [Data Repository](#data-repository), above, is in place -2. Within the [creativecommons/cc-legal-tools-data][repodata] (the [Data - Repository](#data-repository)): - 1. Checkout or create the appropriate branch. - - For example, if a French translation file for BY 4.0 has changed, the - branch name will be `cc4-fr`. - 2. Download the updated `.po` portable object Gettext file from Transifex - 3. Do the [Translation Update Process](#translation-update-process) (below) - - _This is important and easy to forget,_ but without it, Django will - keep using the old translations - 4. Commit that change and push it upstream. -3. Within this `cc-legal-tools-app` repository: - 1. For each branch that has been updated, [Generate Static - Files](#generate-static-files) (below). Use the options to update git and - push the changes. - -[repodata]:https://github.com/creativecommons/cc-legal-tools-data - - -### Check for Translation Updates Dependency Documentation - -- [GitPython Documentation — GitPython documentation][gitpythondocs] -- [Requests: HTTP for Humans™ — Requests documentation][requestsdocs] - -[gitpythondocs]: https://gitpython.readthedocs.io/en/stable/index.html -[requestsdocs]: https://docs.python-requests.org/en/master/ - - -### Translation Update Process - -This Django Admin command must be run any time the `.po` portable object -Gettext files are created or changed. - -1. Ensure the [Data Repository](#data-repository), above, is in place -2. Ensure [Docker Compose Setup](#docker-compose-setup), above, is complete -3. Compile translation messages (update the `.mo` machine object Gettext files) - ```shell - docker compose exec app ./manage.py compilemessages - ``` - - -## Generate Static Files - -Generating static files updates the static files in the `docs/` directory of -the [creativecommons/cc-legal-tools-data][repodata] repository (the [Data -Repository](#data-repository), above). - - -### Static Files Process - -This process will write the HTML files in the cc-legal-tools-data clone -directory under `docs/`. It will not commit the changes (`--nogit`) and will -not push any commits (`--nopush` is implied by `--nogit`). - -1. Ensure the [Data Repository](#data-repository), above, is in place -2. Ensure [Docker Compose Setup](#docker-compose-setup), above, is complete -3. Delete the contents of the `docs/` directory and then recreate/copy the - static files it should contain: - ```shell - docker compose exec app ./manage.py publish -v2 - ``` - - -### Publishing changes to git repo - -When the site is deployed, to enable pushing and pulling the licenses data repo -with GitHub, create an SSH deploy key for the cc-legal-tools-data repo with -write permissions, and put the private key file (not password protected) -somewhere safe (owned by `www-data` if on a server), and readable only by its -owner (0o400). Then in settings, make `TRANSLATION_REPOSITORY_DEPLOY_KEY` be -the full path to that deploy key file. - - -### Publishing Dependency Documentation - -- [Beautiful Soup Documentation — Beautiful Soup 4 documentation][bs4docs] - - [lxml - Processing XML and HTML with Python][lxml] -- [GitPython Documentation — GitPython documentation][gitpythondocs] - -[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ -[gitpythondocs]: https://gitpython.readthedocs.io/en/stable/index.html -[lxml]: https://lxml.de/ - - ## Licenses @@ -580,7 +452,7 @@ Dedication][cc-zero]. [cc-zero]: https://creativecommons.org/publicdomain/zero/1.0/ -### vocabulary-theme +### Vocabulary Theme [![CC0 1.0 Universal (CC0 1.0) Public Domain Dedication button][cc-zero-png]][cc-zero] diff --git a/cc_legal_tools/settings/base.py b/cc_legal_tools/settings/base.py index f7fd6843..4a78ce54 100644 --- a/cc_legal_tools/settings/base.py +++ b/cc_legal_tools/settings/base.py @@ -1,5 +1,5 @@ """ -Django settings for cc_legal_tools project. +Django settings for CC-Legal-Tools project. """ # Standard library @@ -11,33 +11,76 @@ import colorlog # noqa: F401 from django.conf.locale import LANG_INFO -CANONICAL_SITE = "https://creativecommons.org" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +# Paths ####################################################################### # SETTINGS_DIR is where this settings file is -SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__)) +SETTINGS_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) # DJANGO_ROOT is the directory under root that contains the settings directory, # urls.py, and other global stuff. DJANGO_ROOT = os.path.dirname(SETTINGS_DIR) # PROJECT_ROOT is the top directory under source control PROJECT_ROOT = os.path.dirname(DJANGO_ROOT) +# Location of the data repository directory. +# Look in environment for DATA_REPOSITORY_DIR. Default is next to this one. +DATA_REPOSITORY_DIR = os.path.abspath( + os.path.realpath( + os.getenv( + "DATA_REPOSITORY_DIR", + os.path.join(PROJECT_ROOT, "..", "cc-legal-tools-data"), + ) + ) +) +DISTILL_DIR = os.path.abspath( + os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "docs")) +) +LEGACY_DIR = os.path.abspath( + os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "legacy")) +) +# Localication paths +DEEDS_UX_LOCALE_PATH = os.path.abspath( + os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "locale")) +) +LEGAL_CODE_LOCALE_PATH = os.path.abspath( + os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "legalcode")) +) +LOCALE_PATHS = ( + DEEDS_UX_LOCALE_PATH, + LEGAL_CODE_LOCALE_PATH, +) +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = os.path.abspath( + os.path.realpath(os.path.join(PROJECT_ROOT, "tmp", "public", "static")) +) +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = "static/" +# Additional locations of static files +STATICFILES_DIRS = ( + os.path.abspath(os.path.realpath(os.path.join(DJANGO_ROOT, "static"))), +) + + +# Application definition ###################################################### +CANONICAL_SITE = "https://creativecommons.org" # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get("SECRET_KEY") -# Application definition - DEFAULT_AUTO_FIELD = "django.db.models.AutoField" INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", - "django.contrib.sessions", "django.contrib.messages", + "django.contrib.sessions", "django.contrib.staticfiles", - "legal_tools", "i18n", + "legal_tools", ] MIDDLEWARE = [ @@ -64,6 +107,8 @@ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", + # django.contrib.messages.context_processors.messages must be + # enabled in order to use the admin application "django.contrib.messages.context_processors.messages", "dealer.contrib.django.context_processor", ], @@ -71,11 +116,23 @@ }, ] +# The caching API is used, but there are no caching MIDDLEWARE, above, as we +# are not using site caching (which adds overhead without benefit for our uses) +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 600, + "OPTIONS": {"CULL_FREQUENCY": 0, "MAX_ENTRIES": 3000}, + }, +} + WSGI_APPLICATION = "cc_legal_tools.wsgi.application" +mimetypes.add_type("application/rdf+xml", ".rdf", True) + -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +# Database #################################################################### +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { "default": { @@ -84,8 +141,6 @@ } } -DEALER_TYPE = "git" - # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = os.path.join(PROJECT_ROOT, "tmp", "public", "media") @@ -95,7 +150,7 @@ # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = "/media/" -# https://docs.djangoproject.com/en/3.2/topics/logging/ +# https://docs.djangoproject.com/en/4.2/topics/logging/ LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -141,16 +196,6 @@ }, }, "loggers": { - "i18n.management.commands": { - "handlers": ["handle_mgmt"], - "level": "DEBUG", - "propagate": False, - }, - "legal_tools.management.commands": { - "handlers": ["handle_mgmt"], - "level": "DEBUG", - "propagate": False, - }, "django.request": { "handlers": ["mail_admins"], "level": "ERROR", @@ -161,6 +206,16 @@ "level": "ERROR", "propagate": True, }, + "i18n.management.commands": { + "handlers": ["handle_mgmt"], + "level": "DEBUG", + "propagate": False, + }, + "legal_tools.management.commands": { + "handlers": ["handle_mgmt"], + "level": "DEBUG", + "propagate": False, + }, }, "root": { "handlers": [ @@ -170,26 +225,9 @@ }, } -# Location of the data repository directory. -# Look in environment for DATA_REPOSITORY_DIR. Default is next to this one. -DATA_REPOSITORY_DIR = os.path.abspath( - os.path.realpath( - os.getenv( - "DATA_REPOSITORY_DIR", - os.path.join(PROJECT_ROOT, "..", "cc-legal-tools-data"), - ) - ) -) -DISTILL_DIR = os.path.abspath( - os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "docs")) -) -LEGACY_DIR = os.path.abspath( - os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "legacy")) -) - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ +# Internationalization ######################################################## +# https://docs.djangoproject.com/en/4.2/topics/i18n/ # Language code for this installation. LANGUAGE_CODE = "en" # "en" matches our default language code in Transifex @@ -200,26 +238,32 @@ # Percent translated that languages should be at or above TRANSLATION_THRESHOLD = 60 -DEEDS_UX_LOCALE_PATH = os.path.abspath( - os.path.abspath( - os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "locale")) - ) -) -LEGAL_CODE_LOCALE_PATH = os.path.abspath( - os.path.abspath( - os.path.realpath(os.path.join(DATA_REPOSITORY_DIR, "legalcode")) - ) -) -LOCALE_PATHS = ( - DEEDS_UX_LOCALE_PATH, - LEGAL_CODE_LOCALE_PATH, -) +TRANSIFEX = { + "API_TOKEN": os.getenv("TRANSIFEX_API_TOKEN", "[!] MISSING [!]"), + "ORGANIZATION_SLUG": "creativecommons", + "DEEDS_UX_TEAM_ID": 11342, + "DEEDS_UX_PROJECT_SLUG": "CC", + "DEEDS_UX_RESOURCE_SLUGS": [DEEDS_UX_RESOURCE_SLUG], + "LEGAL_CODE_PROJECT_SLUG": "cc-legal-code", + "LEGAL_CODE_TEAM_ID": 153501, + "LEGAL_CODE_RESOURCE_SLUGS": [ + "by-nc-nd_40", + "by-nc-sa_40", + "by-nc_40", + "by-nd_40", + "by-sa_40", + "by_40", + "zero_10", + ], +} + # Preserve Django language information # - This is used for translation/internationalization troubleshooting # - The following line MUST come before any modifications of LANG_INFO DJANGO_LANG_INFO = copy.deepcopy(LANG_INFO) + # Teach Django about a few more languages (sorted by language code) # Azerbaijani (Django defaults to RTL?!?) @@ -257,25 +301,6 @@ # Zulu LANG_INFO["zu"] = {"code": "zu"} # Remaining data from Babel -TRANSIFEX = { - "API_TOKEN": os.getenv("TRANSIFEX_API_TOKEN", "[!] MISSING [!]"), - "ORGANIZATION_SLUG": "creativecommons", - "DEEDS_UX_TEAM_ID": 11342, - "DEEDS_UX_PROJECT_SLUG": "CC", - "DEEDS_UX_RESOURCE_SLUGS": [DEEDS_UX_RESOURCE_SLUG], - "LEGAL_CODE_PROJECT_SLUG": "cc-legal-code", - "LEGAL_CODE_TEAM_ID": 153501, - "LEGAL_CODE_RESOURCE_SLUGS": [ - "by-nc-nd_40", - "by-nc-sa_40", - "by-nc_40", - "by-nd_40", - "by-sa_40", - "by_40", - "zero_10", - ], -} - # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -292,25 +317,8 @@ USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = os.path.join(PROJECT_ROOT, "tmp", "public", "static") - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = "static/" - -# Additional locations of static files -STATICFILES_DIRS = (os.path.join(DJANGO_ROOT, "static"),) - -# If using Celery, tell it to obey our logging configuration. -CELERYD_HIJACK_ROOT_LOGGER = False -# https://docs.djangoproject.com/en/1.9/topics/auth/passwords/#password-validation +# https://docs.djangoproject.com/en/4.2/topics/auth/passwords/#password-validation AUTH_PASSWORD_VALIDATORS = [ { "NAME": ( @@ -345,24 +353,6 @@ CSRF_COOKIE_HTTPONLY = True X_FRAME_OPTIONS = "DENY" -# template_fragments -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.dummy.DummyCache", - }, - "branchstatuscache": { - # Use memory caching so template fragments get cached whether we have - # memcached running or not. - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - }, -} -# This will use memcached if we have it, and otherwise just not cache. -if "CACHE_HOST" in os.environ: - CACHES["default"] = { - "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", - "LOCATION": "%(CACHE_HOST)s" % os.environ, - } - # The git branch where the official, approved, used in production translations # are. OFFICIAL_GIT_BRANCH = "main" @@ -371,5 +361,3 @@ TRANSLATION_REPOSITORY_DEPLOY_KEY = os.getenv( "TRANSLATION_REPOSITORY_DEPLOY_KEY", "" ) - -mimetypes.add_type("application/rdf+xml", ".rdf", True) diff --git a/cc_legal_tools/settings/deploy.py b/cc_legal_tools/settings/deploy.py deleted file mode 100644 index 9b15e0f3..00000000 --- a/cc_legal_tools/settings/deploy.py +++ /dev/null @@ -1,140 +0,0 @@ -# Settings for live deployed environments: vagrant, staging, production, etc -# Standard library -import os - -from .base import * # noqa: F403 - -os.environ.setdefault("CACHE_HOST", "127.0.0.1:11211") -os.environ.setdefault("BROKER_HOST", "127.0.0.1:5672") - -#: deploy environment - e.g. "staging" or "production" -ENVIRONMENT = os.environ["ENVIRONMENT"] - - -DEBUG = False - -if "DATABASE_URL" in os.environ: - # Dokku - SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] - - # Third-party - import dj_database_url - - # Update database configuration with $DATABASE_URL. - db_from_env = dj_database_url.config(conn_max_age=500) - DATABASES["default"].update(db_from_env) # noqa: F405 - - # Disable Django's own staticfiles handling in favour of WhiteNoise, for - # greater consistency between gunicorn and `./manage.py runserver`. See: - # http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development - INSTALLED_APPS.remove("django.contrib.staticfiles") # noqa: F405 - INSTALLED_APPS.extend( # noqa: F405 - ["whitenoise.runserver_nostatic", "django.contrib.staticfiles"] - ) - - MIDDLEWARE.remove( # noqa: F405 - "django.middleware.security.SecurityMiddleware" - ) - MIDDLEWARE = [ # noqa: F405 - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - ] + MIDDLEWARE # noqa: F405 - - # Allow all host headers (feel free to make this more specific) - ALLOWED_HOSTS = ["*"] - - # Simplified static file serving. - # https://warehouse.python.org/project/whitenoise/ - STATICFILES_STORAGE = ( - "whitenoise.storage.CompressedManifestStaticFilesStorage" - ) - -else: - SECRET_KEY = os.environ["SECRET_KEY"] - - DATABASES["default"]["NAME"] = os.environ.get("DB_NAME", "") # noqa: F405 - DATABASES["default"]["USER"] = os.environ.get("DB_USER", "") # noqa: F405 - DATABASES["default"]["HOST"] = os.environ.get("DB_HOST", "") # noqa: F405 - DATABASES["default"]["PORT"] = os.environ.get("DB_PORT", "") # noqa: F405 - DATABASES["default"]["PASSWORD"] = os.environ.get( # noqa: F405 - "DB_PASSWORD", "" - ) - - -STATIC_ROOT = os.getenv( - "STATIC_ROOT", os.path.join(PROJECT_ROOT, "static") # noqa: F405 -) - -MEDIA_ROOT = os.getenv( - "MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media") # noqa: F405 -) - -EMAIL_HOST = os.environ.get("EMAIL_HOST", "localhost") -EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") -EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") -EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", False) -EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", False) -# use TLS or SSL, not both: -assert not (EMAIL_USE_TLS and EMAIL_USE_SSL) -if EMAIL_USE_TLS: - default_smtp_port = 587 -elif EMAIL_USE_SSL: - default_smtp_port = 465 -else: - default_smtp_port = 25 -EMAIL_PORT = os.environ.get("EMAIL_PORT", default_smtp_port) -EMAIL_SUBJECT_PREFIX = os.getenv( - "EMAIL_SUBJECT_PREFIX", "[CC-Legal-Tools-App %s] " % ENVIRONMENT.title() -) -DEFAULT_FROM_EMAIL = os.getenv( - "DEFAULT_FROM_EMAIL", "noreply@%(DOMAIN)s" % os.environ -) -SERVER_EMAIL = DEFAULT_FROM_EMAIL - -CSRF_COOKIE_SECURE = True - -SESSION_COOKIE_SECURE = True - -SESSION_COOKIE_HTTPONLY = True - -ALLOWED_HOSTS = ["127.0.0.1", "localhost"] -if os.environ.get("DOMAIN", None) is not None: - ALLOWED_HOSTS.append(os.environ["DOMAIN"]) - -# Use template caching on deployed servers -for backend in TEMPLATES: # noqa: F405 - if backend["BACKEND"] == "django.template.backends.django.DjangoTemplates": - default_loaders = ["django.template.loaders.filesystem.Loader"] - if backend.get("APP_DIRS", False): - default_loaders.append( - "django.template.loaders.app_directories.Loader" - ) - # Django gets annoyed if you both set APP_DIRS True and specify - # your own loaders - backend["APP_DIRS"] = False - loaders = backend["OPTIONS"].get("loaders", default_loaders) - for loader in loaders: - if ( - len(loader) == 2 - and loader[0] == "django.template.loaders.cached.Loader" - ): - # We're already caching our templates - break - else: - backend["OPTIONS"]["loaders"] = [ - ("django.template.loaders.cached.Loader", loaders) - ] - -# Uncomment if using celery worker configuration -# CELERY_SEND_TASK_ERROR_EMAILS = True -# BROKER_URL = ( -# "amqp://cc_legal_tools_%(ENVIRONMENT)s:%(BROKER_PASSWORD)s@%(BROKER_HOST)s/" -# "cc_legal_tools_%(ENVIRONMENT)s" -# % os.environ -# ) - -# Environment overrides -# These should be kept to an absolute minimum -if ENVIRONMENT.upper() == "LOCAL": - # Don't send emails from the Vagrant boxes - EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/cc_legal_tools/settings/dev.py b/cc_legal_tools/settings/dev.py index c2dd1b63..4bb152fe 100644 --- a/cc_legal_tools/settings/dev.py +++ b/cc_legal_tools/settings/dev.py @@ -1,43 +1,63 @@ +# This application is expected to *always* be run in a dev context. This file +# is exists to both make it easier to allow that to change in the future and to +# help organize the settings. +# +# Additionally, this settings file is used by ephemeral deployments (ex. GitHub +# Actions). + # Standard library -import os import sys +# Third-party +from django.core.management.utils import get_random_secret_key + # First-party/Local from cc_legal_tools.settings.base import * # noqa: F403 -DEBUG = True - -INSTALLED_APPS += [ # noqa: F405 - "debug_toolbar", -] -MIDDLEWARE += ( # noqa: F405 - "debug_toolbar.middleware.DebugToolbarMiddleware", -) - -INTERNAL_IPS = ("127.0.0.1",) +ALLOWED_HOSTS = [".localhost", "127.0.0.1", "[::1]"] +INTERNAL_IPS = ALLOWED_HOSTS #: Don't send emails, just print them on stdout EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -#: Run celery tasks synchronously -CELERY_ALWAYS_EAGER = True +# https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-SECRET_KEY +# As this app is used in a ephemeral manner (only run for development and to +# publish static files), a rotating secret key works well. +SECRET_KEY = get_random_secret_key() -#: Tell us when a synchronous celery task fails -CELERY_EAGER_PROPAGATES_EXCEPTIONS = True - -SECRET_KEY = os.environ.get( - "SECRET_KEY", "_+sc$rvjx-ycj9rkgo4ls81!@clmdjrr=39-#ed7k6cqrq$19f" +# Enable tools like Firefox Web Developer: View Responsive Layouts +MIDDLEWARE.remove( # noqa: F405 + "django.middleware.clickjacking.XFrameOptionsMiddleware" ) -# Special test settings -if "test" in sys.argv: - PASSWORD_HASHERS = ( - "django.contrib.auth.hashers.SHA1PasswordHasher", - "django.contrib.auth.hashers.MD5PasswordHasher", - ) - - LOGGING["root"]["handlers"] = [] # noqa: F405 - # Make it obvious if there are unresolved variables in templates +# +# cc-legal-tools-data: .github/workflows/checks.yml contains a check for the +# string "INVALID_VARIABLE" to ensure it isn't sent to production. new_value = "INVALID_VARIABLE(%s)" TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = new_value # noqa: F405 + +if ( + "dumpdata" not in sys.argv + and "loaddata" not in sys.argv + and "publish" not in sys.argv + and "test" not in sys.argv +): + # 1) "dumpdata": avoid the Django Debug Toolbar when dumping data + # 2) "loaddata": avoid the Django Debug Toolbar when loading data + # 3) "publish": avoid debug output in published files + # 4) "test": the Django Debug Toolbar can't be used with tests: + # HINT: Django changes the DEBUG setting to False when running tests. + # By default the Django Debug Toolbar is installed because DEBUG is + # set to True. For most cases, you need to avoid installing the + # toolbar when running tests. If you feel this check is in error, you + # can set DEBUG_TOOLBAR_CONFIG['IS_RUNNING_TESTS'] = False to bypass + # this check. + DEBUG = True + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG} + INSTALLED_APPS += [ # noqa: F405 + "debug_toolbar", + ] + MIDDLEWARE += [ # noqa: F405 + "debug_toolbar.middleware.DebugToolbarMiddleware", + ] diff --git a/cc_legal_tools/settings/ephemeral.py b/cc_legal_tools/settings/ephemeral.py deleted file mode 100644 index b581d4cf..00000000 --- a/cc_legal_tools/settings/ephemeral.py +++ /dev/null @@ -1,28 +0,0 @@ -# Use **ONLY** for ephemeral deployments (ex. GitHub Actions). - -# Third-party -from django.core.management.utils import get_random_secret_key - -# First-party/Local -from cc_legal_tools.settings.base import * # noqa: F403 - -ALLOWED_HOSTS = ["127.0.0.1", "localhost"] - -#: Don't send emails, just print them on stdout -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -#: Run celery tasks synchronously -CELERY_ALWAYS_EAGER = True - -#: Tell us when a synchronous celery task fails -CELERY_EAGER_PROPAGATES_EXCEPTIONS = True - -DEBUG = True - -INTERNAL_IPS = ("127.0.0.1",) - -SECRET_KEY = get_random_secret_key() - -# Make it obvious if there are unresolved variables in templates -new_value = "INVALID_VARIABLE(%s)" -TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = new_value # noqa: F405 diff --git a/cc_legal_tools/settings/local.example.py b/cc_legal_tools/settings/local.example.py index 605dba71..2113eb07 100644 --- a/cc_legal_tools/settings/local.example.py +++ b/cc_legal_tools/settings/local.example.py @@ -4,16 +4,9 @@ # First-party/Local from cc_legal_tools.settings.dev import * # noqa: F401, F403 -DEBUG = True - -# Override settings here +# Override settings here (and uncomment "import os", above) # TRANSIFEX["API_TOKEN"] = "TRANSIFEX_API_TOKEN" # noqa: F405 # TRANSLATION_REPOSITORY_DEPLOY_KEY = os.path.join( # os.path.expanduser("~"), ".ssh", "PRIVATE_KEY_NAME", # ) - -# Enable tools like Firefox Web Developer: View Responsive Layouts -MIDDLEWARE.remove( # noqa: F405 - "django.middleware.clickjacking.XFrameOptionsMiddleware" -) diff --git a/cc_legal_tools/static/cc-legal-tools/base.css b/cc_legal_tools/static/cc-legal-tools/base.css index b70e00d8..3c0394df 100644 --- a/cc_legal_tools/static/cc-legal-tools/base.css +++ b/cc_legal_tools/static/cc-legal-tools/base.css @@ -10,9 +10,12 @@ /* Ancilliary menu */ +.ancilliary-menu { + font-size: min(.8em, 2.5vw); +} .bidi-left div.masthead > nav.ancilliary-menu { left: auto; - right: 0; + right: calc( -1 * var(--vocabulary-page-edges-space)); } .bidi-right div.masthead > nav.ancilliary-menu { left: 0; @@ -54,18 +57,39 @@ div.masthead > nav.ancilliary-menu span.locale.icon-attach:before { /* header title */ /* TODO: resolve with vocabulary-theme */ .cc-legal-tools main > header { + align-items: center; + /* (upstream vocabulary default-page class uses display: box) */ + display: flex; padding: 3em 0; } -.cc-legal-tools main > header > span.tool-icons > span.cc-icon > svg { +.cc-legal-tools,.bidi-left main > header > span.alt-titles { + order: 1; +} +.cc-legal-tools,.bidi-left main > header > span.alt-titles > span.tool-icons { + padding-right: 1em; +} +.cc-legal-tools,.bidi-right main > header > span.alt-titles > span.tool-icons { + padding-left: 1em; +} +main > header > span.alt-titles > span.tool-icons > span.cc-icon > svg { display: inline; - height: 4em; - width: 4em; + height: 2em; + width: 2em; + vertical-align: text-bottom; +} +.cc-legal-tools main > header > span.alt-titles > span.tool-identifier { + font-family: 'Roboto Condensed'; + font-size: 2.1em; + font-style: normal; + font-weight: 700; } .cc-legal-tools main > header > h1 { margin: 0.2em 0; + order: 2; } .cc-legal-tools main > header > h2 { margin: 0; + order: 3; } @@ -79,7 +103,7 @@ div.masthead > nav.ancilliary-menu span.locale.icon-attach:before { --underline-background-color: var(--vocabulary-brand-color-soft-gold); } main > aside > nav ul > li { /* TODO: resolve with vocabulary-theme */ - font-size: 1rem;/ + font-size: 1rem; } @@ -234,3 +258,19 @@ main > aside > nav ul > li { /* TODO: resolve with vocabulary-theme */ margin-left: .3em; margin-right: 0; } + +/* print media */ +@media print { + .cc-legal-tools > footer, + .cc-legal-tools > header { + display: none; + } + + .cc-legal-tools main { + display: block; + } + + .cc-legal-tools main > aside { + display: none; + } +} diff --git a/cc_legal_tools/static/cc-legal-tools/deed.css b/cc_legal_tools/static/cc-legal-tools/deed.css index c3baa6aa..516504f0 100644 --- a/cc_legal_tools/static/cc-legal-tools/deed.css +++ b/cc_legal_tools/static/cc-legal-tools/deed.css @@ -3,6 +3,7 @@ border-bottom: 5px solid var(--vocabulary-neutral-color-dark-gray); border-left: 5px solid var(--vocabulary-neutral-color-dark-gray); border-right: 5px solid var(--vocabulary-neutral-color-dark-gray); + margin-top: 2em; margin-left: -3em; margin-right: -3em; padding-left: 3em; diff --git a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/style.css b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/style.css index 3c9d9930..f73f5b22 100644 --- a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/style.css +++ b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/style.css @@ -3,7 +3,7 @@ Theme Name: CC Vocabulary Theme Author: the Creative Commons team; possumbilities, Timid Robot Author URI: https://opensource.creativecommons.org/ Description: Theme based on the Vocabulary Design System -Version: 1.3.3 +Version: 1.7 Requires at least: 5.0 Tested up to: 6.2.2 Requires PHP: 7.0 diff --git a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/library-vars.css b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/library-vars.css index e3e40523..3709453d 100644 --- a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/library-vars.css +++ b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/library-vars.css @@ -26,9 +26,9 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/SourceSansPro-Regular.woff2) format("woff2"), - url(../fonts/SourceSansPro-Regular.woff) format("woff"), - url(../fonts/SourceSansPro-Regular.otf) format("opentype"); + src: url('../fonts/SourceSansPro-Regular.woff2') format("woff2"), + url('../fonts/SourceSansPro-Regular.woff') format("woff"), + url('../fonts/SourceSansPro-Regular.otf') format("opentype"); } @font-face { @@ -36,9 +36,9 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(../fonts/SourceSansPro-SemiBold.woff2) format("woff2"), - url(../fonts/SourceSansPro-SemiBold.woff) format("woff"), - url(../fonts/SourceSansPro-SemiBold.otf) format("opentype"); + src: url('../fonts/SourceSansPro-SemiBold.woff2') format("woff2"), + url('../fonts/SourceSansPro-SemiBold.woff') format("woff"), + url('../fonts/SourceSansPro-SemiBold.otf') format("opentype"); } @font-face { @@ -46,9 +46,9 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url(../fonts/SourceSansPro-Bold.woff2) format("woff2"), - url(../fonts/SourceSansPro-Bold.woff) format("woff"), - url(../fonts/SourceSansPro-Bold.otf) format("opentype"); + src: url('../fonts/SourceSansPro-Bold.woff2') format("woff2"), + url('../fonts/SourceSansPro-Bold.woff') format("woff"), + url('../fonts/SourceSansPro-Bold.otf') format("opentype"); } @font-face { @@ -56,7 +56,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/CCAccidenzCommons-medium.otf) format("opentype"); + src: url('../fonts/CCAccidenzCommons-medium.otf') format("opentype"); } @@ -132,27 +132,27 @@ --icon-sprite-size: 1em; /* cc sprite names */ - --cc-logo: url('./../svg/cc/icons/cc-icons.svg#cc-logo'); - --cc-heart: url('./../svg/cc/icons/cc-icons.svg#cc-heart'); - --cc-heart-filled: url('./../svg/cc/icons/cc-icons.svg#cc-heart-filled'); + --cc-logo: url('../svg/cc/icons/cc-icons.svg#cc-logo'); + --cc-heart: url('../svg/cc/icons/cc-icons.svg#cc-heart'); + --cc-heart-filled: url('../svg/cc/icons/cc-icons.svg#cc-heart-filled'); + --cc-quote: url('../svg/cc/icons/cc-icons.svg#cc-quote'); /* font awesome sprite names */ - --fa-angle-down: url('./../svg/font-awesome/icons/fa-icons.svg#fa-angle-down'); - --fa-angle-left: url('./../svg/font-awesome/icons/fa-icons.svg#fa-angle-left'); - --fa-angle-right: url('./../svg/font-awesome/icons/fa-icons.svg#fa-angle-right'); - --fa-angle-up: url('./../svg/font-awesome/icons/fa-icons.svg#fa-angle-up'); - --fa-globe: url('./../svg/font-awesome/icons/fa-icons.svg#fa-globe'); - --fa-heart: url('./../svg/font-awesome/icons/fa-icons.svg#fa-heart'); - --fa-info: url('./../svg/font-awesome/icons/fa-icons.svg#fa-info'); - --fa-quote: url('./../svg/font-awesome/icons/fa-icons.svg#fa-quote'); - --fa-right-angle: url('./../svg/font-awesome/icons/fa-icons.svg#fa-right-angle'); - --fa-search: url('./../svg/font-awesome/icons/fa-icons.svg#fa-search'); - - --fa-instagram: url('./../svg/font-awesome/icons/fa-icons.svg#fa-instagram'); - --fa-twitter: url('./../svg/font-awesome/icons/fa-icons.svg#fa-twitter'); - --fa-facebook: url('./../svg/font-awesome/icons/fa-icons.svg#fa-facebook'); - --fa-linkedin: url('./../svg/font-awesome/icons/fa-icons.svg#fa-linkedin'); - --fa-mastodon: url('./../svg/font-awesome/icons/fa-icons.svg#fa-mastodon'); + --fa-angle-down: url('../svg/font-awesome/icons/fa-icons.svg#fa-angle-down'); + --fa-angle-left: url('../svg/font-awesome/icons/fa-icons.svg#fa-angle-left'); + --fa-angle-right: url('../svg/font-awesome/icons/fa-icons.svg#fa-angle-right'); + --fa-angle-up: url('../svg/font-awesome/icons/fa-icons.svg#fa-angle-up'); + --fa-globe: url('../svg/font-awesome/icons/fa-icons.svg#fa-globe'); + --fa-heart: url('../svg/font-awesome/icons/fa-icons.svg#fa-heart'); + --fa-info: url('../svg/font-awesome/icons/fa-icons.svg#fa-info'); + --fa-right-angle: url('../svg/font-awesome/icons/fa-icons.svg#fa-right-angle'); + --fa-search: url('../svg/font-awesome/icons/fa-icons.svg#fa-search'); + + --fa-bluesky: url('../svg/font-awesome/icons/fa-icons.svg#fa-bluesky'); + --fa-facebook: url('../svg/font-awesome/icons/fa-icons.svg#fa-facebook'); + --fa-instagram: url('../svg/font-awesome/icons/fa-icons.svg#fa-instagram'); + --fa-linkedin: url('../svg/font-awesome/icons/fa-icons.svg#fa-linkedin'); + --fa-mastodon: url('../svg/font-awesome/icons/fa-icons.svg#fa-mastodon'); } @@ -189,6 +189,10 @@ leaving room for semantic and accessible implementation choices */ --icon-sprite: var(--cc-heart-filled); } +.icon.cc-quote, .icon-attach.cc-quote:before { + --icon-sprite: var(--cc-quote); +} + .icon.fa-angle-down, .icon-attach.fa-angle-down:before { --icon-sprite: var(--fa-angle-down); } @@ -197,7 +201,7 @@ leaving room for semantic and accessible implementation choices */ --icon-sprite: var(--fa-angle-left); } -.icon.fa-angle-right, .icon-attach.fa-angle-rignt:before { +.icon.fa-angle-right, .icon-attach.fa-angle-right:before { --icon-sprite: var(--fa-angle-right); } @@ -217,30 +221,27 @@ leaving room for semantic and accessible implementation choices */ --icon-sprite: var(--fa-info); } -.icon.fa-quote, .icon-attach.fa-quote:before { - --icon-sprite: var(--fa-quote); -} - -.icon.fa-right-angle, .icon-attach.fa-righ-angle:before { - --icon-sprite: var(--fa-heart); +.icon.fa-right-angle, .icon-attach.fa-right-angle:before { + --icon-sprite: var(--fa-right-angle); } .icon.fa-search, .icon-attach.fa-search:before { --icon-sprite: var(--fa-search); } -.icon-replace.fa-instagram { - --icon-sprite: var(--fa-instagram); -} -.icon-replace.fa-twitter { - --icon-sprite: var(--fa-twitter); +.icon-replace.fa-bluesky { + --icon-sprite: var(--fa-bluesky); } .icon-replace.fa-facebook { --icon-sprite: var(--fa-facebook); } +.icon-replace.fa-instagram { + --icon-sprite: var(--fa-instagram); +} + .icon-replace.fa-linkedin { --icon-sprite: var(--fa-linkedin); } diff --git a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/vocabulary.css b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/vocabulary.css index 13de9a9f..3346e76d 100644 --- a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/vocabulary.css +++ b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/css/vocabulary.css @@ -86,6 +86,14 @@ body > header { margin: 0; } +/* allows the child identity-logo element to have a focus state */ +.identity-logo-wrapper { + height: 50px; + width: 191px; + display: block; + position: absolute; +} + .masthead .identity-logo { display: inline-block; text-indent: -1000px; @@ -108,7 +116,6 @@ body > header { .masthead .identity-logo:hover { background-color: var(--vocabulary-neutral-color-dark-gray); - } /* TODO: needs focus outline to be fixed */ @@ -1559,7 +1566,8 @@ main .authored-posts.highlight article { } nav.pagination { - grid-column: span 3; + grid-column: span 2; + margin: 0 auto; } nav.pagination ol { @@ -2028,7 +2036,7 @@ main blockquote { /* manually include quote icon to avoid extraneous html classes */ main blockquote p:before { - --icon-sprite: var(--fa-quote); + --icon-sprite: var(--cc-quote); display: block; content: ''; diff --git a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg index f3d4c512..45c5ab02 100644 --- a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg +++ b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/cc/icons/cc-icons.svg @@ -1,4 +1,5 @@ + @@ -77,4 +78,9 @@ + + + + + diff --git a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/font-awesome/icons/fa-icons.svg b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/font-awesome/icons/fa-icons.svg index 4ac151d1..dcc77c9e 100644 --- a/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/font-awesome/icons/fa-icons.svg +++ b/cc_legal_tools/static/wp-content/themes/vocabulary-theme/vocabulary/svg/font-awesome/icons/fa-icons.svg @@ -3,16 +3,19 @@ - + + + + @@ -45,30 +48,21 @@ - - - - - - - - + + - + - - - - + - - - + + + @@ -76,6 +70,11 @@ + + + + + @@ -86,14 +85,4 @@ - - - - - - - - - - diff --git a/cc_legal_tools/urls.py b/cc_legal_tools/urls.py index 37d47e47..3e739010 100644 --- a/cc_legal_tools/urls.py +++ b/cc_legal_tools/urls.py @@ -48,5 +48,5 @@ def custom_page_not_found(request): import debug_toolbar urlpatterns += [ - re_path(r"^__debug__/", include(debug_toolbar.urls)), + path("__debug__/", include(debug_toolbar.urls)), ] diff --git a/dev/norm_legacy_rdf.py b/dev/20230901_norm_legacy_rdf.py old mode 100755 new mode 100644 similarity index 100% rename from dev/norm_legacy_rdf.py rename to dev/20230901_norm_legacy_rdf.py diff --git a/dev/concatenatemessages.sh b/dev/20231009_concatenatemessages.sh old mode 100755 new mode 100644 similarity index 100% rename from dev/concatenatemessages.sh rename to dev/20231009_concatenatemessages.sh diff --git a/dev/20231129_modify_deeds_for_footnotes.sh b/dev/20231129_modify_deeds_for_footnotes.sh old mode 100755 new mode 100644 diff --git a/dev/20250210_update_legacy_cc0.sh b/dev/20250210_update_legacy_cc0.sh new file mode 100644 index 00000000..8fd39c2c --- /dev/null +++ b/dev/20250210_update_legacy_cc0.sh @@ -0,0 +1,337 @@ +#!/bin/bash +# +# Update legacy CC0 translation strings so that the string for the Other +# Information first paragraph. For more information, see: +# https://github.com/creativecommons/cc-legal-tools-app/issues/502 +# +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E1="$(printf "\e[1m")" # bold +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E97="$(printf "\e[97m")" # bright white foreground +E100="$(printf "\e[100m")" # bright black (gray) background +E107="$(printf "\e[107m")" # bright white background +DIR_APP="$(cd -P -- "${0%/*}/.." && pwd -P)" +DIR_PARENT="$(cd -P -- "${DIR_APP}/.." && pwd -P)" +DIR_LEGACY="$(cd -P -- "${DIR_PARENT}/cc.i18n" && pwd -P)" +DIR_DATA="$(cd -P -- "${DIR_PARENT}/cc-legal-tools-data" && pwd -P)" +SED='' + +#### FUNCTIONS ################################################################ + + +check_prerequisites() { + local _m1 _m2 _m3 + print_header 'Check prerequisites' + + # cc-legal-tools-data repositry adjacent + if [[ ! -d "${DIR_DATA}" ]] + then + _m1='The cc-legal-tools-data repository (see README.md) must be' + _m2=' cloned adjacent to this one.' + error_exit "${_m1}${_m2}" + fi + + # Legacy ccEngine localization repositry adjacent + if [[ ! -d "${DIR_LEGACY}" ]] + then + _m1='The legacy ccEngine localization repository' + _m2=' (https://github.com/creativecommons/cc.i18n) must be cloned' + _m3=' adjacent to this one.' + error_exit "${_m1}${_m2}${_m3}" + fi + + # GNU sed available + if command -v gsed >/dev/null + then + SED=$(command -v gsed) + elif sed --version >/dev/null + then + SED=$(command -v sed) + else + # shellcheck disable=SC2016 + error_exit \ + 'GNU sed is required. If on macOS install `gnu-sed` via brew.' + fi + + # gettext available + if ! command -v msgcat &>/dev/null + then + # shellcheck disable=SC2006,SC2016 + _m1="The `msgcat` command isn't available. It is provided by the" + _m2=' gettext package on apt/dpkg based Linux and from Homebrew on' + _m3=' macOS.' + error_exit "${_m1}${_m2}${_m3}" + fi + + # Docker app service running + if ! docker compose exec app true 2>/dev/null + then + _m1="The app container/services isn't avaialable. First run" + # shellcheck disable=SC2016 + _m2=' `docker compose up`.' + error_exit "${_m1}${_m2}" + fi + + print_var DIR_PARENT + print_var DIR_APP + print_var DIR_DATA + print_var DIR_LEGACY + echo +} + + +compile_mofiles() { + print_header 'Compile mo files' + docker compose exec app ./manage.py compilemessages \ + | "${SED}" --unbuffered \ + -e'/^File.*is already compiled/d' \ + -e's|/home/cc/||' + echo +} + + +concatenate_translations() { + local _dir_legacy_po _legacy_locale_1 _legacy_locale_2 _locale \ + _po_current _po_legacy_1 _po_legacy_2 + print_header 'Concatenate legacy ccEngine translations' + _dir_legacy_po="${DIR_LEGACY}/cc/i18n/po" + for _locale_dir in "${DIR_DATA}"/locale/* + do + [[ -d "${_locale_dir}" ]] || continue + _locale="${_locale_dir##*/}" + echo "${E1}${_locale}${E0}" + _po_current="${_locale_dir}/LC_MESSAGES/django.po" + echo " [current] ${_po_current#"${DIR_PARENT}/"}" + _legacy_locale_1='.INVALID.SHOULD_NOT_EXIST' + _legacy_locale_2='.INVALID.SHOULD_NOT_EXIST' + _po_legacy_1='' + _po_legacy_2='' + case ${_locale} in + es) + _legacy_locale_1="${_locale}" + _legacy_locale_2='es_ES' + ;; + kn) + _legacy_locale_1="${_locale}" + _legacy_locale_2='kn_IN' + ;; + oc_Aranes) + _legacy_locale_1='oc' + ;; + tr) + _legacy_locale_1="${_locale}" + _legacy_locale_2='tr_TR' + ;; + zh_Hans) + _legacy_locale_1='zh-Hans' + _legacy_locale_2='zh' + ;; + zh_Hant) + _legacy_locale_1='zh-Hant' + _legacy_locale_2='zh_TW' + ;; + *) + _legacy_locale_1="${_locale}" + ;; + esac + + if [[ -d "${_dir_legacy_po}/${_legacy_locale_1}" ]]; then + _po_legacy_1="${_dir_legacy_po}/${_legacy_locale_1}/cc_org.po" + echo " [legacy1] ${_po_legacy_1#"${DIR_PARENT}/"}" + msgcat \ + --output-file="${_po_current}" \ + --use-first \ + --sort-output \ + "${_po_current}" \ + "${_po_legacy_1}" + fi + + if [[ -d "${_dir_legacy_po}/${_legacy_locale_2}" ]]; then + _po_legacy_2="${_dir_legacy_po}/${_legacy_locale_2}/cc_org.po" + echo " [legacy2] ${_po_legacy_2#"${DIR_PARENT}/"}" + msgcat \ + --output-file="${_po_current}" \ + --use-first \ + --sort-output \ + "${_po_current}" \ + "${_po_legacy_2}" + fi + + done + echo +} + + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 + exit 1 +} + + +format_pofiles() { + print_header 'Django Management format_pofile' + docker compose exec app ./manage.py format_pofile locale \ + | "${SED}" --unbuffered \ + -e's|^/home/cc/||' + echo +} + + +make_messages() { + print_header 'Django Management nofuzzy_makemessages' + # shellcheck disable=SC2035 + docker compose exec app ./manage.py \ + nofuzzy_makemessages \ + --all \ + --symlinks \ + --ignore **/includes/legalcode_licenses_4.0.html \ + --ignore **/includes/legalcode_contextual_menu.html \ + --ignore **/includes/legalcode_zero.html \ + --add-location full \ + --no-obsolete \ + | "${SED}" --unbuffered \ + -e"s|^/home/cc/||" + echo +} + + +restore_update_dates() { + local _date_new _date_old _pofile + pushd "${DIR_DATA}" >/dev/null + + print_header 'Restore POT-Creation-Date' + for _pofile in $(git diff --name-only); + do + [[ "${_pofile}" =~ django\.po$ ]] || continue + _date_old="$(git diff "${_pofile}" | grep '^-"POT-Creation-Date')" \ + || continue + _date_old="${_date_old:2:${#_date_old}-5}" + _date_new="$(git diff "${_pofile}" | grep '^+"POT-Creation-Date')" + _date_new="${_date_new:2:${#_date_new}-5}" + if [[ -n "${_date_old}" ]] && [[ -n "${_date_new}" ]] + then + echo "${E1}updating ${_pofile}${E0}" + echo " pattern: ${_date_new}" + echo " replacement: ${_date_old}" + "${SED}" -e"s#${_date_new}#${_date_old}#" -i "${_pofile}" + fi + done + echo + + print_header 'Update PO-Revision-Date' + _date_new="PO-Revision-Date: $(date -u '+%F %T+00:00')" + for _pofile in $(git diff --name-only) + do + [[ "${_pofile}" =~ django\.po$ ]] || continue + _date_old="$(grep '^"PO-Revision-Date' "${_pofile}")" + _date_old="${_date_old:1:${#_date_old}-4}" + if [[ -n "${_date_old}" ]] && [[ -n "${_date_new}" ]] + then + echo "${E1}updating ${_pofile}${E0}" + echo " pattern: ${_date_old}" + echo " replacement: ${_date_new}" + "${SED}" -e"s#${_date_old}#${_date_new}#" -i "${_pofile}" + fi + done + echo + + popd >/dev/null +} + + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-69s$(date '+%T') ${E0}\n" "${@}" +} + + +print_key_val() { + local _sep + if (( ${#1} > 10 )) + then + _sep="\n " + else + _sep=' ' + fi + printf "${E97}${E100}%21s${E0}${_sep}%s\n" "${1}:" "${2}" +} + + +print_var() { + print_key_val "${1}" "${!1}" +} + + +show_reminders() { + print_header 'Reminders' + echo '- Changes were made to ccEngine repository (../cc.i18n). It is' + echo ' probably best if you restore it.' + echo '- Next run the following Django management commands:' + echo ' 1. normalize_translations' + echo ' 2. compilemessages' + echo +} + + +update_legacy_cc0() { + # publicity or privacy + # + # publicity or privacy + local _match _r1 _r2 _replace + pushd "${DIR_LEGACY}" >/dev/null + _match=']+publicity_rights[^>]+>([^<]+)' + _r1='' + _r2='\1' + _replace="${_r1}${_r2}" + print_header 'Update legacy CC0 deed translation string' + cd "${DIR_LEGACY}" + for _po in cc/i18n/po/*/cc_org.po + do + echo "cc.i18n/${_po}" + # replace dos line endings with unix line endings to address error: + # warning: internationalized messages should not contain the '\r' + # escape sequence + "${SED}" --in-place \ + -e 's/[\]r[\]n/\\n/' \ + "${_po}" + # clean-up + msgcat --output-file="${_po}" \ + --no-location --no-wrap --sort-output \ + "${_po}" + "${SED}" --in-place \ + -e "/^#, python-format/d" \ + -e's#\x1C##g' \ + "${_po}" + # update + "${SED}" --in-place --regexp-extended \ + -e "s|${_match}|${_replace}|g" \ + "${_po}" + done + echo + popd >/dev/null +} + + +#### MAIN ##################################################################### + +check_prerequisites +update_legacy_cc0 +concatenate_translations +restore_update_dates +make_messages +format_pofiles +compile_mofiles +show_reminders diff --git a/dev/check_python_versions.sh b/dev/check_python_versions.sh new file mode 100755 index 00000000..e1471e15 --- /dev/null +++ b/dev/check_python_versions.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# +# Check if all specified Python versions match Pipfile +# +set -o errtrace +set -o nounset + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E91="$(printf "\e[91m")" # bright red foreground +E92="$(printf "\e[92m")" # bright green foreground +E97="$(printf "\e[97m")" # bright white foreground +E100="$(printf "\e[100m")" # bright black (gray) background +EXIT_STATUS=0 + + +pyvercompare() { + local _path=${1} + local _match=${2} + local _field=${3} + local _unwanted="\"',[" + # extract Python version + _ver=$(awk "/${_match}/ {print \$${_field}}" \ + "${_path}") + # clean-up Python version + _ver="${_ver//[${_unwanted}]/}" + # compare Python version + if [[ "${_path}" == 'Pipfile' ]] + then + _status="✅ ${E92}athoritative Python version${E0}" + PIPFILE_VER=${_ver} + elif [[ "${_ver}" == "${PIPFILE_VER}" ]] \ + || [[ "${_ver}" == "python${PIPFILE_VER}" ]] \ + || [[ "${_ver}" == "python:${PIPFILE_VER}-slim" ]] \ + || [[ "${_ver}" == "py${PIPFILE_VER//./}" ]] + then + _status="✅ ${E92}Python version matches Pipfile${E0}" + else + _status="❌ ${E91}Python version does not match Pipfile${E0}" + EXIT_STATUS=1 + fi + # print info + printf "${E97}${E100} %9s${E0} %s\n" 'File:' "${E97}${_path}${E0}" + printf "${E97}${E100} %9s${E0} %s\n" 'Version:' "${_ver}" + printf "${E97}${E100} %9s${E0} %s\n" 'Status:' "${_status}" + echo +} + + +# Change directory to root directory of cc-legal-tools-app repository +# (grandparent directory of this script) +# shellcheck disable=SC2164 +cd "${0%/*}"/../ + +pyvercompare 'Pipfile' 'python_version =' '3' +pyvercompare '.github/workflows/django-app-coverage.yml' 'python-version:' '2' +pyvercompare '.github/workflows/static-analysis.yml' 'python-version:' '2' +pyvercompare '.pre-commit-config.yaml' 'python: python' '2' +pyvercompare 'Dockerfile' 'FROM python:' '2' +pyvercompare 'pyproject.toml' 'target-version =' '3' + +exit ${EXIT_STATUS} diff --git a/dev/coverage.sh b/dev/coverage.sh index 5b4c7d7e..f75991f3 100755 --- a/dev/coverage.sh +++ b/dev/coverage.sh @@ -2,38 +2,67 @@ # # Run coverage tests and report # -set -o errtrace -set -o nounset - # This script passes all arguments to Coverage Tests. For example, it can be # called from the cc-legal-tools-app directory like so: # # ./dev/coverage.sh --failfast -# Change directory to cc-legal-tools-app (grandparent directory of this script) -cd ${0%/*}/../ +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +DIR_REPO="$(cd -P -- "${0%/*}/.." && pwd -P)" +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E107="$(printf "\e[107m")" # bright white background -if ! docker compose exec app true 2>/dev/null; then - echo 'The app container/services is not avaialable.' 1>&2 - echo 'First run `docker compose up`.' 1>&2 +#### FUNCTIONS ################################################################ + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 exit 1 -fi +} + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-69s$(date '+%T') ${E0}\n" "${@}" +} + +#### MAIN ##################################################################### + +cd "${DIR_REPO}" -printf "\e[1m\e[7m %-80s\e[0m\n" 'Coverage Erase' -docker compose exec app coverage erase -echo 'done.' +docker compose exec app true 2>/dev/null \ + || error_exit \ + 'The Docker app container/service is not avaialable. See README.md' + +print_header 'Coverage erase' +docker compose exec app coverage erase --debug=dataio echo -printf "\e[1m\e[7m %-80s\e[0m\n" 'Coverage Tests' -docker compose exec app coverage run \ +print_header 'Coverage tests' +docker compose exec app coverage run --debug=pytest \ manage.py test --noinput --parallel 4 ${@:-} \ || exit echo -printf "\e[1m\e[7m %-80s\e[0m\n" 'Coverage Combine' +print_header 'Coverage combine' docker compose exec app coverage combine echo -printf "\e[1m\e[7m %-80s\e[0m\n" 'Coverage Report' +print_header 'Coverage html' +docker compose exec app coverage html +echo + +print_header 'Coverage report' docker compose exec app coverage report echo diff --git a/dev/dump_data.sh b/dev/dump_data.sh new file mode 100755 index 00000000..3678717b --- /dev/null +++ b/dev/dump_data.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Dump Django application data +# +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E107="$(printf "\e[107m")" # bright white background + +#### FUNCTIONS ################################################################ + + +check_docker() { + local _msg + if ! docker compose exec app true 2>/dev/null; then + _msg='The app container/services is not avaialable.' + _msg="${_msg}\n First run \`docker compose up\`." + error_exit "${_msg}" + fi +} + + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 + exit 1 +} + + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-70s$(date '+%T') ${E0}\n" "${@}" +} + + +#### MAIN ##################################################################### + +check_docker +print_header 'Export data (LegalCode and Tool models)' +data_file='../cc-legal-tools-data/config/app_data.yaml' +docker compose exec app ./manage.py dumpdata \ + --format yaml \ + --indent 2 \ + --output "${data_file}" \ + legal_tools.LegalCode legal_tools.Tool +du -h "${data_file}" +echo diff --git a/dev/init_data.sh b/dev/init_data.sh new file mode 100755 index 00000000..d86d85c6 --- /dev/null +++ b/dev/init_data.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# +# Initialize Django application data (!!DANGER!!) +# +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E33="$(printf "\e[33m")" # yellow foreground +E43="$(printf "\e[43m")" # yellow background +E107="$(printf "\e[107m")" # bright white background + +#### FUNCTIONS ################################################################ + + +check_docker() { + local _msg + if ! docker compose exec app true 2>/dev/null; then + _msg='The app container/services is not avaialable.' + _msg="${_msg}\n First run \`docker compose up\`." + error_exit "${_msg}" + fi +} + + +danger_confirm() { + local _confirm _i _prompt _rand + + printf "${E43}${E30}# %-70s$(date '+%T') ${E0}\n" \ + 'Confirmation required' + echo -e "${E33}WARNING:${E0} this scripts deletes the app database" + # Loop until user enters random number + _rand=${RANDOM}${RANDOM}${RANDOM} + _rand=${_rand:0:4} + _prompt="Type the number, ${_rand}, to continue: " + _i=0 + while read -p "${_prompt}" -r _confirm + do + if [[ "${_confirm}" == "${_rand}" ]] + then + echo + return + fi + (( _i > 1 )) && error_exit 'invalid confirmation number' + _i=$(( ++_i )) + done +} + + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 + exit 1 +} + + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-70s$(date '+%T') ${E0}\n" "${@}" +} + + +#### MAIN ##################################################################### + +check_docker +danger_confirm + +print_header 'Delete database' +docker compose exec app rm -fv db.sqlite3 +echo + +print_header 'Initialize database' +docker compose exec app sqlite3 db.sqlite3 -version +docker compose exec app sqlite3 db.sqlite3 -echo 'VACUUM;' +echo + +print_header 'Perform database migrations' +docker compose exec app ./manage.py migrate +echo + +print_header 'Create superuser' +docker compose exec app ./manage.py createsuperuser \ + --username admin --email "$(git config --get user.email)" +echo + +print_header 'Load data (LegalCode and Tool models)' +data_file='../cc-legal-tools-data/config/app_data.yaml' +du -h "${data_file}" +docker compose exec app ./manage.py loaddata \ + --app legal_tools \ + --verbosity 3 \ + "${data_file}" +echo diff --git a/dev/load_data.sh b/dev/load_data.sh new file mode 100755 index 00000000..a8d8708a --- /dev/null +++ b/dev/load_data.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Load Django application data +# +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E107="$(printf "\e[107m")" # bright white background + +#### FUNCTIONS ################################################################ + + +check_docker() { + local _msg + if ! docker compose exec app true 2>/dev/null; then + _msg='The app container/services is not avaialable.' + _msg="${_msg}\n First run \`docker compose up\`." + error_exit "${_msg}" + fi +} + + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 + exit 1 +} + + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-70s$(date '+%T') ${E0}\n" "${@}" +} + + +#### MAIN ##################################################################### + +check_docker + +print_header 'Load data (LegalCode and Tool models)' +data_file='../cc-legal-tools-data/config/app_data.yaml' +du -h "${data_file}" +docker compose exec app ./manage.py loaddata \ + --app legal_tools \ + --verbosity 3 \ + "${data_file}" +echo diff --git a/dev/resetdb.sh b/dev/resetdb.sh deleted file mode 100755 index e95cb3fe..00000000 --- a/dev/resetdb.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# Reset Django application database data (!!DANGER!!) -set -o errexit -set -o errtrace -set -o nounset - -# Change directory to cc-legal-tools-app (grandparent directory of this script) -cd ${0%/*}/../ - -if ! docker compose exec app true 2>/dev/null; then - echo 'The app container/services is not avaialable.' 1>&2 - echo 'First run `docker compose up`.' 1>&2 - exit 1 -fi - -printf "\e[1m\e[7m %-80s\e[0m\n" 'Delete Database' -docker compose exec app rm -fv db.sqlite3 -echo - -printf "\e[1m\e[7m %-80s\e[0m\n" 'Initialize Database' -docker compose exec app sqlite3 db.sqlite3 -version -docker compose exec app sqlite3 db.sqlite3 -echo 'VACUUM;' -echo - -printf "\e[1m\e[7m %-80s\e[0m\n" 'Perform migrations' -docker compose exec app ./manage.py migrate -echo - -printf "\e[1m\e[7m %-80s\e[0m\n" 'createsuperuser' -docker compose exec app ./manage.py createsuperuser \ - --username admin --email "$(git config --get user.email)" -echo diff --git a/dev/stats.sh b/dev/stats.sh new file mode 100755 index 00000000..39fc90df --- /dev/null +++ b/dev/stats.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# +# TODO: stop expanding this bash script and move it to the publishing process +# so that the information is on an HTML page that is updated with each +# publish +# +set -o errexit +set -o errtrace +set -o nounset + +# shellcheck disable=SC2154 +trap '_es=${?}; + printf "${0}: line ${LINENO}: \"${BASH_COMMAND}\""; + printf " exited with a status of ${_es}\n"; + exit ${_es}' ERR + +# https://en.wikipedia.org/wiki/ANSI_escape_code +E0="$(printf "\e[0m")" # reset +E1="$(printf "\e[1m")" # bold +E4="$(printf "\e[4m")" # underline +E30="$(printf "\e[30m")" # black foreground +E31="$(printf "\e[31m")" # red foreground +E33="$(printf "\e[33m")" # yellow foreground +E97="$(printf "\e[97m")" # bright white foreground +E100="$(printf "\e[100m")" # bright black (gray) background +E107="$(printf "\e[107m")" # bright white background +DIR_REPO="$(cd -P -- "${0%/*}/.." && pwd -P)" +DIR_PUB_LICENSES="$(cd -P -- \ + "${DIR_REPO}/../cc-legal-tools-data/docs/licenses" && pwd -P)" +DIR_PUB_PUBLICDOMAIN="$(cd -P -- \ + "${DIR_REPO}/../cc-legal-tools-data/docs/publicdomain" && pwd -P)" +PORTED_NOTE="\ +Prior to the international 4.0 version, the licenses were adapted to specific +legal jurisdictions (\"ported\"). This means there are more legal tools for +these earlier versions than there are licenses." + +#### FUNCTIONS ################################################################ + + +check_prerequisites() { + if ! command -v scc &>/dev/null + then + # shellcheck disable=SC2016 + error_exit 'The `scc` command is unavailable.' + fi +} + + +error_exit() { + # Echo error message and exit with error + echo -e "${E31}ERROR:${E0} ${*}" 1>&2 + exit 1 +} + + +print_header() { + # Print 80 character wide black on white heading with time + printf "${E30}${E107}# %-69s$(date '+%T') ${E0}\n" "${@}" +} + + +print_key_val() { + local _sep + if (( ${#1} > 10 )) + then + _sep="\n " + else + _sep=' ' + fi + printf "${E97}${E100}%21s${E0}${_sep}%s\n" "${1}:" "${2}" +} + + +print_var() { + print_key_val "${1}" "${!1}" +} + + +published_documents() { + local _count _subtotal _ver + print_header 'Published' + print_var DIR_PUB_LICENSES + print_var DIR_PUB_PUBLICDOMAIN + echo + + echo "${E1}Licenses per license version${E0}" + printf " %-7s %-4s %s\n" 'Version' 'Count' 'Licenses' + for _ver in '1.0' '2.0' '2.1' '2.5' '3.0' '4.0' + do + _count=$(find "${DIR_PUB_LICENSES}"/*/"${_ver}" \ + -maxdepth 0 -type d \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + _list=$(find "${DIR_PUB_LICENSES}"/*/"${_ver}" \ + -maxdepth 0 -type d \ + | awk -F'/' '{print $9}' \ + | sort \ + | tr '\n' ' ') + printf " %-7s %'5d " "${_ver}" "${_count}" + _chars=0 + for _license in ${_list} + do + _len=${#_license} + [[ -z "${_license}" ]] && continue + if (( _chars + _len < 61 )) + then + printf "%s " "${_license}" + else + _chars=0 + echo + printf "%18s%s " ' ' "${_license}" + fi + _chars=$(( _chars + _len + 2 )) + done + echo + done + echo + + echo "${E1}Legal tools per license version${E0}" + echo "${PORTED_NOTE}" + # per version + for _ver in '1.0' '2.0' '2.1' '2.5' '3.0' '4.0' + do + _count=$(find "${DIR_PUB_LICENSES}"/*/"${_ver}" \ + -type f -name 'deed.en.html' \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + if [[ "${_ver}" != '4.0' ]] + then + printf " %-5s%'4d\n" "${_ver}" "${_count}" + else + printf " ${E4}%-5s%'4d${E0}\n" "${_ver}" "${_count}" + fi + done + # total + _count=$(find "${DIR_PUB_LICENSES}" \ + -type f -name 'deed.en.html' \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " %-5s%'4d\n" "Total" "${_count}" + echo + + echo "${E1}Legal tools${E0}" + # licenses + _count=$(find "${DIR_PUB_LICENSES}" \ + -type f -name 'deed.en.html' \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " %-13s%'4d\n" "Licenses" "${_count}" + # public domain + _count=$(find "${DIR_PUB_PUBLICDOMAIN}" \ + -type f -name 'deed.en.html' \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " ${E4}%-13s%'4d${E0}\n" "Public domain" "${_count}" + # total + _count=$(find "${DIR_PUB_LICENSES}" "${DIR_PUB_PUBLICDOMAIN}" \ + -type f -name 'deed.en.html' \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " %-13s%'4d\n" "Total" "${_count}" + echo + + echo "${E1}Documents${E0}" + # licenses + _count=$(find "${DIR_PUB_LICENSES}" \ + -type f \( -name '*.html' -o -name 'rdf' \) \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " %-13s% '7d\n" "Licenses" "${_count}" + # public domain + _count=$(find "${DIR_PUB_PUBLICDOMAIN}" \ + -type f \( -name '*.html' -o -name 'rdf' \) \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " ${E4}%-13s% '7d${E0}\n" "Public domain" "${_count}" + # total + _count=$(find "${DIR_PUB_LICENSES}" "${DIR_PUB_PUBLICDOMAIN}" \ + -type f \( -name '*.html' -o -name 'rdf' \) \ + | wc -l \ + | sed -e's/[[:space:]]*//g') + printf " %-13s% '7d\n" "Total" "${_count}" + echo +} + + +source_code() { + print_header 'Source code' + print_var DIR_REPO + cd "${DIR_REPO}" + scc \ + --exclude-dir .git,wp-content \ + --no-duplicates \ + --dryness + echo +} + + +todo() { + print_header 'Deeds & UX translation' + echo "${E33}TODO${E0}" + echo + print_header 'Legal Code translation' + echo "${E33}TODO${E0}" + echo +} + + +#### MAIN ##################################################################### + +check_prerequisites +source_code +published_documents +todo diff --git a/docker-compose.yml b/docker-compose.yml index 0bab79f8..328762a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,21 +5,22 @@ # .env # Dockerfile # -# Use docker compose v2 for non swarm deployments -# https://docs.docker.com/compose/compose-file/compose-file-v2/ - -version: '2.4' +# https://docs.docker.com/compose/compose-file/ services: app: build: . - command: ./manage.py runserver 0.0.0.0:${PORT_APP} + command: "./manage.py runserver 0.0.0.0:${PORT_APP}" environment: - DJANGO_SETTINGS_MODULE=${DOCKER_DJANGO_SETTINGS_MODULE} ports: - "127.0.0.1:${PORT_APP}:${PORT_APP}" - restart: always + restart: on-failure volumes: - - .:/home/cc/cc-legal-tools-app - - ${RELPATH_DATA}:/home/cc/cc-legal-tools-data + - '.:/home/cc/cc-legal-tools-app' + - "${RELPATH_DATA}:/home/cc/cc-legal-tools-data" + + # static + # the static service is loaded from ../cc-legal-tools-data if the .env + # file has been properly copied and configured. diff --git a/docs-outdated/format_techspec.sh b/docs/_ARCHIVED/format_techspec.sh similarity index 100% rename from docs-outdated/format_techspec.sh rename to docs/_ARCHIVED/format_techspec.sh diff --git a/docs-outdated/index_rdf_data_issues.txt b/docs/_ARCHIVED/index_rdf_data_issues.txt similarity index 100% rename from docs-outdated/index_rdf_data_issues.txt rename to docs/_ARCHIVED/index_rdf_data_issues.txt diff --git a/docs/_ARCHIVED/load_html_import.md b/docs/_ARCHIVED/load_html_import.md new file mode 100644 index 00000000..41ceb624 --- /dev/null +++ b/docs/_ARCHIVED/load_html_import.md @@ -0,0 +1,77 @@ +## Helper Scripts + +Best run before every commit: +- `./dev/20231009_concatenatemessages.sh` - Concatenate legacy ccEngine + translations into cc-legal-tools-app + + +## Importing the existing legal tool text + +Note that once the site is up and running in production, the data in the site +will become the canonical source, and the process described here should not +need to be repeated after that. + +The implementation is the Django management command +`20231010_load_html_files.py`, which reads from the legacy HTML legal code +files in the [creativecommons/cc-legal-tools-data][repodata] repository, and +populates the database records and translation files. + +`load_html_files` uses [BeautifulSoup4][bs4docs] to parse the legacy HTML legal +code: +1. `import_zero_license_html()` for CC0 Public Domain tool + - HTML is handled specifically (using tag ids and classes) to populate + translation strings and to be used with specific HTML formatting when + displayed via template +2. `import_by_40_license_html()` for 4.0 License tools + - HTML is handled specifically (using tag ids and classes) to populate + translation strings and to be used with specific HTML formatting when + displayed via a template +3. `import_by_30_unported_license_html()` for unported 3.0 License tools + (English-only) + - HTML is handled specifically to be used with specific HTML formatting + when displayed via a template +4. `simple_import_license_html()` for everything else + - HTML is handled generically; only the title and license body are + identified. The body is stored in the `html` field of the + `LegalCode` model + +[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +[repodata]: https://github.com/creativecommons/cc-legal-tools-data + + +### Import Process + +This process will read the HTML files from the specified directory, populate +`LegalCode` and `Tool` models, and create the `.po` portable object Gettext +files in [creativecommons/cc-legal-tools-data][repodata]. + +1. Ensure the Data Repository (see [`../../README.md`](../../README.md) is in + place +2. Ensure Docker Compose Setup (see [`../../README.md`](../../README.md) is + complete +3. Clear data in the database + ```shell + docker compose exec app ./manage.py clear_license_data + ``` +4. Load legacy HTML in the database + ```shell + docker compose exec app ./manage.py load_html_files + ``` +5. Optionally (and only as appropriate): + 1. Commit the `.po` portable object Gettext file changes in + [creativecommons/cc-legal-tools-data][repodata] + 2. Translation Update Process (see [`../translation.md`](../translation.md) + 3. Generate Static Files (see [`../../README.md`](../../README.md) + +[repodata]:https://github.com/creativecommons/cc-legal-tools-data + + +### Import Dependency Documentation + +- [Beautiful Soup Documentation — Beautiful Soup 4 documentation][bs4docs] + - [lxml - Processing XML and HTML with Python][lxml] +- [Quick start guide — polib documentation][polibdocs] + +[bs4docs]: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +[lxml]: https://lxml.de/ +[polibdocs]: https://polib.readthedocs.io/en/latest/quickstart.html diff --git a/docs-outdated/provisioning.md b/docs/_ARCHIVED/provisioning.md similarity index 100% rename from docs-outdated/provisioning.md rename to docs/_ARCHIVED/provisioning.md diff --git a/docs-outdated/techspec.md b/docs/_ARCHIVED/techspec.md similarity index 100% rename from docs-outdated/techspec.md rename to docs/_ARCHIVED/techspec.md diff --git a/docs-outdated/translation.md b/docs/_ARCHIVED/translation.md similarity index 100% rename from docs-outdated/translation.md rename to docs/_ARCHIVED/translation.md diff --git a/rdf.md b/docs/rdf.md similarity index 72% rename from rdf.md rename to docs/rdf.md index c7f331fd..e1b6f204 100644 --- a/rdf.md +++ b/docs/rdf.md @@ -1,8 +1,48 @@ # RDF/XML +(Return to primary [`../README.md`](../README.md).) + ## Namespaces + +### ccREL Schema + +`schema.rdf` excerpt: +```xml + +``` +| Prefix | Name | URL | +| ------ | ------------------- | ------------------------------------------- | +| `cc` | ccREL | http://creativecommons.org/ns# | +| `owl` | OWL 2 | http://www.w3.org/2002/07/owl# | +| `rdf` | RDF XML Syntax | http://www.w3.org/1999/02/22-rdf-syntax-ns# | +| `rdfs` | RDF Schema | http://www.w3.org/2000/01/rdf-schema# | + + +### Images + +`images.rdf` excerpt: +```xml + +``` +| Prefix | Name | URL | +| ------ | ------------------- | ------------------------------------------- | +| `exif` | Exif RDF Schema | http://www.w3.org/2003/12/exif/ns# | +| `rdf` | RDF XML Syntax | http://www.w3.org/1999/02/22-rdf-syntax-ns# | + + +### Legal code + +`**/rdf` excerpt: ```xml Vocabulary to describe an Exif format picture data. All Exif 2.2 tags are defined as RDF properties, as well as several terms to help this schema + +- [Exif RDF Schema][exifrdf] + +[exifrdf]: https://www.w3.org/2003/12/exif/ + + +### FOAF Vocabulary (`foaf`) [FOAF - Wikipedia](https://en.wikipedia.org/wiki/FOAF) (retrieved 2023-07-20): > FOAF (an acronym of friend of a friend) is a machine-readable ontology @@ -55,16 +105,17 @@ [foafvocab]: http://xmlns.com/foaf/0.1/ -### OWL 2 +### OWL 2 (`owl` prefix) -[OWL 2 Web Ontology Language Document Overview (Second Edition)][owl2overiew] +[OWL 2 Web Ontology Language Document Overview (Second Edition)][owl2overview] (retrieved 2023-07-20): > The OWL 2 Web Ontology Language, informally OWL 2, is an ontology language > for the Semantic Web with formally defined meaning. OWL 2 ontologies provide > classes, properties, individuals, and data values and are stored as Semantic > Web documents. -- [OWL 2 Web Ontology Language Document Overview (Second Edition)][owl2overiew] +- [OWL 2 Web Ontology Language Document Overview (Second + Edition)][owl2overview] - [OWL 2 Web Ontology Language Structural Specification and Functional-Style Syntax (Second Edition)][owl2spec] - [OWL 2 Web Ontology Language XML Serialization (Second Edition)][owl2xml] @@ -76,13 +127,39 @@ [wikipediasameas]: https://en.wikipedia.org/wiki/SameAs -### RDF XML Syntax +### RDF XML Syntax (`rdf` prefix) - [RDF 1.1 XML Syntax](https://www.w3.org/TR/rdf-syntax-grammar/) +### RDF Schema (`rdfs` prefix) + +- [RDF Schema 1.1](https://www.w3.org/TR/rdf11-schema/) + + +## RDF canonical URL + +Due to historical reasons, the canonical URL for the legal tools in RDF uses +the HTTP (unencrypted) protocol. + +Although the legal tools are not available via HTTP, the URLs still function +correctly due to redirects. Additionally, the RDF now includes an `owl:sameAs` +element with the HTTPS URL for further compatibility. + +For example: + +| Context | Protocol | Canonical URL | Published data example | +| ------- | -------- | ------------: | ---------------------- | +| RDF | HTTP | `http://creativecommons.org/licenses/by/4.0/` | [licenses/by/4.0/rdf#L3][ex1] +| all other uses | HTTPS | `https://creativecommons.org/licenses/by/4.0/` | [licenses/by/4.0/legalcode.en.html#L390-L397][ex2] + +[ex1]: https://github.com/creativecommons/cc-legal-tools-data/blob/ba0781024c735f5cd4eb59f8a1716eb9a12df212/docs/licenses/by/4.0/rdf#L3 +[ex2]: https://github.com/creativecommons/cc-legal-tools-data/blob/ba0781024c735f5cd4eb59f8a1716eb9a12df212/docs/licenses/by/4.0/legalcode.en.html#L390-L397 + + ## Changes + ### Overview The changes between the old legacy ccEngine RDF/XML and the new CC Legal Tools diff --git a/docs/translation.md b/docs/translation.md new file mode 100644 index 00000000..3b8d934a --- /dev/null +++ b/docs/translation.md @@ -0,0 +1,132 @@ +# Translation + +(Return to primary [`../README.md`](../README.md).) + + +## Overview + +To upload/download translation files to/from Transifex, you'll need an account +there with access to these translations. Then follow the [Authentication - +Transifex API v3][transauth]: to get an API token, and set +`TRANSIFEX["API_TOKEN"]` in your environment with its value. + +The [creativecommons/cc-legal-tools-data][repodata] repository must be cloned +next to this `cc-legal-tools-app` repository. (It can be elsewhere, then you +need to set `DATA_REPOSITORY_DIR` to its location.) Be sure to clone using a +URL that starts with `git@github...` and not `https://github...`, or you won't +be able to push to it. See [`../README.md`](../README.md) for details. + +~~In production, the `check_for_translation_updates` management command should +be run hourly. See [Check for Translation +Updates](#check-for-translation-updates), below.~~ + +Also see [Publishing changes to git repo](#publishing-changes-to-git-repo), +below. + +[Babel][babel] is used for localization information. + +Documentation: +- [Babel — Babel documentation][babel] +- [Translation | Django documentation | Django][djangotranslation] + +[babel]: http://babel.pocoo.org/en/latest/index.html +[repodata]:https://github.com/creativecommons/cc-legal-tools-data +[transauth]: https://transifex.github.io/openapi/index.html#section/Authentication + + +## How the tool translation is implemented + +Django Translation uses two sets of Gettext Files in the +[creativecommons/cc-legal-tools-data][repodata] repository (the [Data +Repository](#data-repository), above). See that repository for detailed +information and definitions. + +Documentation: +- [Translation | Django documentation | Django][djangotranslation] +- Transifex API + - [Introduction to API 3.0 | Transifex Documentation][api30intro] + - [Transifex API v3][api30] + - Python SDK: [transifex-python/transifex/api][apisdk] + +[api30]: https://transifex.github.io/openapi/index.html#section/Introduction +[api30intro]: https://docs.transifex.com/api-3-0/introduction-to-api-3-0 +[apisdk]: https://github.com/transifex/transifex-python/tree/devel/transifex/api +[djangotranslation]: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/ +[repodata]: https://github.com/creativecommons/cc-legal-tools-data + + +## Add translation + +1. Add language to appropriate resource in Transifex +2. Ensure language is present in Django + - If not, update `cc_legal_tools/settings/base.py` +3. Add objects for new language translation using the `add_translation` + management + command. + - Examples: + ```shell + docker compose exec app ./manage.py add_translation -v2 --licenses -l tlh + ``` + ```shell + docker compose exec app ./manage.py add_translation -v2 --zero -l tlh + ``` +4. Synchronize repository Gettext files with Transifex +5. Compile `.mo` machine object Gettext files: + ```shell + docker compose exec app ./manage.py compilemessages + ``` + +Documentation: +- [Quick start guide — polib documentation][polibdocs] +- Also see How the tool translation is implemented documentation, above + +[polibdocs]: https://polib.readthedocs.io/en/latest/quickstart.html + + +## Synchronize repository Gettext files with Transifex + +- **TODO** document processes of synchronizing the repository Gettext files + with Transifex, including the following management commands: + - `locale_info` + - `normalize_translations` + - `compare_translations` + - `pull_translation` + - `push_translation` + - `compilemessages` + + +## Check for translation updates + +> :warning: **This functionality is currently disabled.** + +~~The hourly run of `check_for_translation_updates` looks to see if any of the +translation files in Transifex have newer last modification times than we know +about. It performs the following process (which can also be done manually:~~ + +1. ~~Ensure the Data Repository ([`../README.md`](../README.md)) is in place~~ +2. ~~Within the [creativecommons/cc-legal-tools-data][repodata] (the [Data + Repository](#data-repository)):~~ + 1. ~~Checkout or create the appropriate branch.~~ + - ~~For example, if a French translation file for BY 4.0 has changed, the + branch name will be `cc4-fr`.~~ + 2. ~~Download the updated `.po` portable object Gettext file from + Transifex~~ + 3. ~~Do the [Translation Update Process](#translation-update-process) + (below)~~ + - ~~_This is important and easy to forget,_ but without it, Django will + keep using the old translations~~ + 4. ~~Commit that change and push it upstream.~~ +3.~~ Within this `cc-legal-tools-app` repository:~~ + 1. ~~For each branch that has been updated, Generate Static + Files ([`../README.md`](../README.md)). Use the options to update git and + push the changes.~~ + +[repodata]:https://github.com/creativecommons/cc-legal-tools-data + + +Documentation: +- [GitPython Documentation — GitPython documentation][gitpythondocs] +- [Requests: HTTP for Humans™ — Requests documentation][requestsdocs] + +[gitpythondocs]: https://gitpython.readthedocs.io/en/stable/index.html +[requestsdocs]: https://docs.python-requests.org/en/master/ diff --git a/i18n/__init__.py b/i18n/__init__.py index b590d80e..2a051866 100644 --- a/i18n/__init__.py +++ b/i18n/__init__.py @@ -191,6 +191,8 @@ "za": gettext_lazy("South Africa"), } UNIT_NAMES = { + # 4.0 licenses use "NoDerivatives" instead of "NoDerivs". When appropriate, + # the following values are updated by legal_tools.views.get_tool_title() "by": gettext_lazy("Attribution"), "by-nc": gettext_lazy("Attribution-NonCommercial"), "by-nc-nd": gettext_lazy("Attribution-NonCommercial-NoDerivs"), @@ -221,7 +223,7 @@ # Django language codes are lowercase IETF language tags # # The following PCREs handle altnate codes and characters. Alternate case - # should be handled by an Apache RewriteMap. + # should be handled by an Apache configuraiton generated during publishing. "de-at": ["de[@_]at"], "en": ["en[@_-]gb", "en[@_-]us"], "en-ca": ["en[@_]ca"], diff --git a/i18n/management/commands/add_translation.py b/i18n/management/commands/add_translation.py new file mode 100644 index 00000000..5320edcf --- /dev/null +++ b/i18n/management/commands/add_translation.py @@ -0,0 +1,142 @@ +# Standard library +import datetime +import logging +import os.path +from argparse import ArgumentParser + +# Third-party +import polib +from django.conf import settings +from django.core.management import BaseCommand, CommandError + +# First-party/Local +from i18n.utils import ( + map_django_to_transifex_language_code, + save_pofile_as_pofile_and_mofile, +) +from legal_tools.models import LegalCode, Tool + +LOG = logging.getLogger(__name__) +LOG_LEVELS = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, +} +NOW = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S+0000") + + +class Command(BaseCommand): + """ + Create new Licenses 4.0 or CC Zero 1.0 LegalCode objects for a given + language. + """ + + def add_arguments(self, parser: ArgumentParser): + domains = parser.add_mutually_exclusive_group(required=True) + domains.add_argument( + "--licenses", + action="store_const", + const="licenses", + help="Add licenses 4.0 translations", + dest="domains", + ) + domains.add_argument( + "--zero", + action="store_const", + const="zero", + help="Add CC0 1.0 translation", + dest="domains", + ) + parser.add_argument( + "-l", + "--language", + action="store", + required=True, + help="limit translation language to specified Language Code", + ) + parser.add_argument( + "-n", + "--dryrun", + action="store_true", + help="dry run: do not make any changes", + ) + + def write_po_files( + self, + legal_code, + language_code, + ): + po_filename = legal_code.translation_filename() + pofile = polib.POFile() + transifex_language = map_django_to_transifex_language_code( + language_code + ) + + # Use the English message text as the message key + en_pofile_path = legal_code.get_english_pofile_path() + en_pofile_obj = polib.pofile(en_pofile_path) + for entry in en_pofile_obj: + pofile.append(polib.POEntry(msgid=entry.msgid, msgstr="")) + + # noqa: E501 + # https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html + pofile.metadata = { + "Content-Transfer-Encoding": "8bit", + "Content-Type": "text/plain; charset=UTF-8", + "Language": transifex_language, + "Language-Django": language_code, + "Language-Transifex": transifex_language, + "Language-Team": "https://www.transifex.com/creativecommons/CC/", + "MIME-Version": "1.0", + "PO-Revision-Date": NOW, + "Percent-Translated": pofile.percent_translated(), + "Project-Id-Version": legal_code.tool.resource_slug, + } + + directory = os.path.dirname(po_filename) + if not os.path.isdir(directory): + os.makedirs(directory) + # Save mofile ourself. We could call 'compilemessages' but + # it wants to compile everything, which is both overkill + # and can fail if the venv or project source is not + # writable. We know this dir is writable, so just save this + # pofile and mofile ourselves. + LOG.info(f"Writing {po_filename.replace('.po', '')}.(mo|po)") + save_pofile_as_pofile_and_mofile(pofile, po_filename) + + def add_legal_code(self, options, category, version, unit=None): + tool_parameters = {"category": category, "version": version} + if unit is not None: + tool_parameters["unit"] = unit + tools = Tool.objects.filter(**tool_parameters).order_by("unit") + for tool in tools: + title = f"{tool.unit} {tool.version} {options['language']}" + legal_code_parameters = { + "tool": tool, + "language_code": options["language"], + } + if LegalCode.objects.filter(**legal_code_parameters).exists(): + LOG.warn(f"LegalCode object already exists: {title}") + legal_code = LegalCode.objects.get(**legal_code_parameters) + else: + LOG.info(f"Creating LeglCode object: {title}") + if not options["dryrun"]: + legal_code = LegalCode.objects.create( + **legal_code_parameters + ) + if not options["dryrun"]: + po_filename = legal_code.translation_filename() + if os.path.isfile(po_filename): + LOG.debug(f"File already exists: {po_filename}") + else: + self.write_po_files(legal_code, options["language"]) + + def handle(self, **options): + LOG.setLevel(LOG_LEVELS[int(options["verbosity"])]) + if options["language"] not in settings.LANG_INFO: + raise CommandError(f"Invalid language code: {options['language']}") + if options["domains"] == "licenses": + self.add_legal_code(options, "licenses", "4.0") + elif options["domains"] == "zero": + self.add_legal_code(options, "publicdomain", "1.0", "zero") diff --git a/i18n/management/commands/nofuzzy_makemessages.py b/i18n/management/commands/nofuzzy_makemessages.py index bcd814ad..b2ff6cd5 100644 --- a/i18n/management/commands/nofuzzy_makemessages.py +++ b/i18n/management/commands/nofuzzy_makemessages.py @@ -3,8 +3,16 @@ class Command(makemessages.Command): - # For Django default msgmerge options, see: - # https://github.com/django/django/blob/stable/3.2.x/django/core/management/commands/makemessages.py#L211 - # As of 2021-10-22, the defaults are: - # msgmerge_options = ['-q', '--previous'] - msgmerge_options = ["--no-fuzzy-matching", "--previous", "--quiet"] + # For Django 4.2 default msgmerge options, see: + # https://github.com/django/django/blob/stable/4.2.x/django/core/management/commands/makemessages.py#L222 + # As of 2025-02-10, the defaults are: + # msgmerge_options = ["-q", "--backup=none", "--previous", "--update"] + # + # override options: + msgmerge_options = [ + "--backup=none", + "--no-fuzzy-matching", + "--previous", + "--quiet", + "--update", + ] diff --git a/i18n/tests/test_transifex.py b/i18n/tests/test_transifex.py index a7314cf7..75ea654c 100644 --- a/i18n/tests/test_transifex.py +++ b/i18n/tests/test_transifex.py @@ -127,29 +127,30 @@ def setUp(self): ] ) - i18n_format_xa = mock.Mock(id="XA") - i18n_format_xa.__str__ = mock.Mock(return_value=i18n_format_xa.id) - i18n_format_xb = mock.Mock(id="XB") - i18n_format_xb.__str__ = mock.Mock(return_value=i18n_format_xb.id) - i18n_format_po = mock.Mock(id="PO") - i18n_format_po.__str__ = mock.Mock(return_value=i18n_format_po.id) - i18n_format_xc = mock.Mock(id="XC") - i18n_format_xc.__str__ = mock.Mock(return_value=i18n_format_xc.id) + return_value = [] + hundred = range(0, 100) + for i in hundred: + i18n_format = mock.Mock(id=f"X{i:03}") + i18n_format.__str__ = mock.Mock(return_value=i18n_format.id) + return_value.append(i18n_format) + i18n_format = mock.Mock(id="PO") + i18n_format.__str__ = mock.Mock(return_value=i18n_format.id) + return_value.append(i18n_format) + hundred = range(100, 200) + for i in hundred: + i18n_format = mock.Mock(id=f"X{i:4}") + i18n_format.__str__ = mock.Mock(return_value=i18n_format.id) + return_value.append(i18n_format) + with mock.patch("i18n.transifex.transifex_api") as api: api.Organization.get = mock.Mock(return_value=organization) - api.I18nFormat.filter = mock.Mock( - return_value=[ - i18n_format_po, - i18n_format_xa, - i18n_format_xb, - i18n_format_xc, - ] - ) + api.I18nFormat.filter = mock.Mock(return_value=return_value) self.helper = TransifexHelper(dryrun=False) api.Organization.get.assert_called_once() organization.fetch.assert_called_once() api.I18nFormat.filter.assert_called_once() + self.assertEquals(self.helper.api_i18n_format.id, "PO") def test__empty_branch_object(self): empty = _empty_branch_object() @@ -1461,6 +1462,74 @@ def test_safesync_translation_with_both_changes(self): self.helper.clear_transifex_stats.assert_called() mock_pofile_save.assert_called_once() + def test_safesync_translation_with_mismatched_changes(self): + api = self.helper.api + language_code = "x_lang_code_x" + transifex_code = "x_trans_code_x" + resource_slug = "x_slug_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + language = mock.Mock( + id=f"l:{transifex_code}", + ) + api.Language.get = mock.Mock(return_value=language) + resource = mock.Mock( + id=f"o:{TEST_ORG_SLUG}:p:{TEST_PROJ_SLUG}:r:{resource_slug}", + attributes={"i18n_type": "PO"}, + ) + api.Resource.get = mock.Mock(return_value=resource) + translations = [ + mock.Mock( + resource_string=mock.Mock( + strings={"other": pofile_obj[0].msgid} + ), + strings={ + "other": pofile_obj[0].msgstr.replace( + "Attribution", "XXXXXXXXXXX" + ), + }, + save=mock.Mock(), + ), + mock.Mock( + resource_string=mock.Mock( + strings={"other": pofile_obj[1].msgid} + ), + strings={"other": pofile_obj[1].msgstr}, + save=mock.Mock(), + ), + ] + api.ResourceTranslation.filter = mock.Mock( + return_value=mock.Mock( + include=mock.Mock( + return_value=mock.Mock( + all=mock.Mock(return_value=translations) + ), + ), + ), + ) + self.helper.clear_transifex_stats = mock.Mock() + pofile_obj[0].msgstr = pofile_obj[0].msgstr.replace( + "Attribution", "YYYYYYYYYYY" + ) + + with mock.patch.object(polib.POFile, "save") as mock_pofile_save: + pofile_obj_new = self.helper.safesync_translation( + resource_slug, + transifex_code, + language_code, + pofile_path, + pofile_obj, + ) + + self.assertEqual( + pofile_obj_new[0].msgstr, + "YYYYYYYYYYY-NoDerivatives 4.0 International", + ) + translations[0].save.assert_not_called() + translations[1].save.assert_not_called() + self.helper.clear_transifex_stats.assert_not_called() + mock_pofile_save.assert_not_called() + def test_safesync_translation_with_both_changes_dryrun(self): api = self.helper.api self.helper.dryrun = True @@ -2167,42 +2236,19 @@ def test_upload_translation_to_transifex_resource_present(self): api.Resource.get.assert_not_called() api.ResourceTranslationsAsyncUpload.upload.assert_not_called() - def test_upload_translation_to_transifex_resource_dryrun(self): - api = self.helper.api - self.helper.dryrun = True - resource_slug = "x_slug_x" - language_code = "x_lang_code_x" - transifex_code = "x_trans_code_x" - pofile_path = "x_path_x" - pofile_obj = polib.pofile(pofile=POFILE_CONTENT) - push_overwrite = False - self.helper._resource_stats = {resource_slug: None} - self.helper._translation_stats = {resource_slug: {}} - - self.helper.upload_translation_to_transifex_resource( - resource_slug, - language_code, - transifex_code, - pofile_path, - pofile_obj, - push_overwrite, - ) - - api.Language.get.assert_called_once() - api.Resource.get.assert_called_once() - api.ResourceTranslationsAsyncUpload.upload.assert_not_called() - - def test_upload_translation_to_transifex_resource_miss_with_changes(self): + def test_upload_translation_to_transifex_resource_present_forced(self): api = self.helper.api resource_slug = "x_slug_x" language_code = "x_lang_code_x" transifex_code = "x_trans_code_x" pofile_path = "x_path_x" pofile_obj = polib.pofile(pofile=POFILE_CONTENT) - push_overwrite = False + push_overwrite = True pofile_content = get_pofile_content(pofile_obj) self.helper._resource_stats = {resource_slug: {}} - self.helper._translation_stats = {resource_slug: {}} + self.helper._translation_stats = { + resource_slug: {transifex_code: {"translated_strings": 99}} + } language = mock.Mock( id=f"l:{transifex_code}", ) @@ -2237,17 +2283,42 @@ def test_upload_translation_to_transifex_resource_miss_with_changes(self): ) self.helper.clear_transifex_stats.assert_called_once() - def test_upload_translation_to_transifex_resource_push(self): + def test_upload_translation_to_transifex_resource_dryrun(self): api = self.helper.api + self.helper.dryrun = True resource_slug = "x_slug_x" language_code = "x_lang_code_x" transifex_code = "x_trans_code_x" pofile_path = "x_path_x" pofile_obj = polib.pofile(pofile=POFILE_CONTENT) - push_overwrite = True + push_overwrite = False + self.helper._resource_stats = {resource_slug: None} + self.helper._translation_stats = {resource_slug: {}} + + self.helper.upload_translation_to_transifex_resource( + resource_slug, + language_code, + transifex_code, + pofile_path, + pofile_obj, + push_overwrite, + ) + + api.Language.get.assert_called_once() + api.Resource.get.assert_called_once() + api.ResourceTranslationsAsyncUpload.upload.assert_not_called() + + def test_upload_translation_to_transifex_resource_miss_with_changes(self): + api = self.helper.api + resource_slug = "x_slug_x" + language_code = "x_lang_code_x" + transifex_code = "x_trans_code_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + push_overwrite = False pofile_content = get_pofile_content(pofile_obj) - self.helper._resource_stats = {} - self.helper._translation_stats = {} + self.helper._resource_stats = {resource_slug: {}} + self.helper._translation_stats = {resource_slug: {}} language = mock.Mock( id=f"l:{transifex_code}", ) @@ -2330,6 +2401,37 @@ def test_upload_translation_to_transifex_resource_no_changes(self): self.assertIn("Translation upload failed", log_context.output[2]) self.helper.clear_transifex_stats.assert_not_called() + def test_upload_translation_to_transifex_resource_local_empty(self): + api = self.helper.api + resource_slug = "x_slug_x" + language_code = "x_lang_code_x" + transifex_code = "x_trans_code_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + for entry in pofile_obj: + entry.msgstr = "" + push_overwrite = False + self.helper._resource_stats = {resource_slug: None} + self.helper._translation_stats = {resource_slug: {}} + self.helper.clear_transifex_stats = mock.Mock() + + with self.assertLogs(self.helper.log, level="DEBUG") as log_context: + self.helper.upload_translation_to_transifex_resource( + resource_slug, + language_code, + transifex_code, + pofile_path, + pofile_obj, + push_overwrite, + ) + + api.Language.get.assert_not_called() + api.Resource.get.assert_not_called() + api.ResourceTranslationsAsyncUpload.upload.assert_not_called() + self.assertTrue(log_context.output[0].startswith("DEBUG:")) + self.assertIn("Skipping upload of 0% complete", log_context.output[0]) + self.helper.clear_transifex_stats.assert_not_called() + # Test: normalize_pofile_language ######################################## def test_noramalize_pofile_language_correct(self): @@ -2603,6 +2705,94 @@ def test_normalize_pofile_last_translator_incorrect(self): mock_pofile_save.assert_called() self.assertNotIn("Last-Translator", new_pofile_obj.metadata) + # Test: normalize_pofile_percent_translated ############################## + + def test_normalize_pofile_percent_translated_resource_language(self): + transifex_code = settings.LANGUAGE_CODE + resource_slug = "x_slug_x" + resource_name = "x_name_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + pofile_obj.metadata["Percent-Translated"] = ( + pofile_obj.percent_translated() + ) + + with mock.patch.object(polib.POFile, "save") as mock_pofile_save: + self.helper.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) + + mock_pofile_save.assert_not_called() + + def test_normalize_pofile_percent_translated_correct(self): + transifex_code = "x_trans_code_x" + resource_slug = "x_slug_x" + resource_name = "x_name_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + pofile_obj.metadata["Percent-Translated"] = ( + pofile_obj.percent_translated() + ) + + with mock.patch.object(polib.POFile, "save") as mock_pofile_save: + self.helper.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) + + mock_pofile_save.assert_not_called() + + def test_normalize_pofile_percent_translated_dryrun(self): + self.helper.dryrun = True + transifex_code = "x_trans_code_x" + resource_slug = "x_slug_x" + resource_name = "x_name_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + pofile_obj.metadata["Percent-Translated"] = 37 + + with mock.patch.object(polib.POFile, "save") as mock_pofile_save: + self.helper.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) + + mock_pofile_save.assert_not_called() + + def test_normalize_pofile_percent_translated_incorrect(self): + transifex_code = "x_trans_code_x" + resource_slug = "x_slug_x" + resource_name = "x_name_x" + pofile_path = "x_path_x" + pofile_obj = polib.pofile(pofile=POFILE_CONTENT) + pofile_obj.metadata["Percent-Translated"] = 37 + + with mock.patch.object(polib.POFile, "save") as mock_pofile_save: + new_pofile_obj = self.helper.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) + + mock_pofile_save.assert_called() + self.assertIn("Percent-Translated", new_pofile_obj.metadata) + self.assertEqual( + new_pofile_obj.percent_translated(), + new_pofile_obj.metadata["Percent-Translated"], + ) + # Test: normalize_pofile_project_id ###################################### def test_normalize_pofile_project_id_correct(self): diff --git a/i18n/tests/test_utils.py b/i18n/tests/test_utils.py index 03de903a..f8db95b2 100644 --- a/i18n/tests/test_utils.py +++ b/i18n/tests/test_utils.py @@ -13,7 +13,8 @@ # First-party/Local from i18n.utils import ( active_translation, - get_default_language_for_jurisdiction, + get_default_language_for_jurisdiction_deed, + get_default_language_for_jurisdiction_naive, get_jurisdiction_name, get_pofile_creation_date, get_pofile_path, @@ -160,14 +161,32 @@ def test_get_jurisdiction_name_licenses_10(self): class I18NTest(TestCase): - def test_get_language_for_jurisdiction(self): - # 'be' default is "fr" + def test_get_language_for_jurisdiction_deed(self): + # "be" jurisdiction default is "fr" self.assertEqual( - "fr", get_default_language_for_jurisdiction("be", "ar") + "fr", get_default_language_for_jurisdiction_deed("be") ) - # There is none for "xx" so we return the default instead + # "am" jurisdiction default is "hy" + # the "hy" translation is incomplete so we return the global default + # https://github.com/creativecommons/cc-legal-tools-app/issues/444 self.assertEqual( - "ar", get_default_language_for_jurisdiction("xx", "ar") + "en", get_default_language_for_jurisdiction_deed("am") + ) + # "xx" is an invalid jurisdiction + # return global default ("en") + self.assertEqual( + "en", get_default_language_for_jurisdiction_deed("xx") + ) + + def test_get_language_for_jurisdiction_legal_code(self): + # "be" jurisdiction default is "fr" + self.assertEqual( + "fr", get_default_language_for_jurisdiction_naive("be") + ) + # "xx" is an invalid jurisdiction + # return global default ("en") + self.assertEqual( + "en", get_default_language_for_jurisdiction_naive("xx") ) diff --git a/i18n/transifex.py b/i18n/transifex.py index fc34758d..59ac1df6 100644 --- a/i18n/transifex.py +++ b/i18n/transifex.py @@ -53,11 +53,7 @@ def __init__(self, dryrun: bool = True, logger: logging.Logger = None): # (^[a-z0-9._-]+$'), but the web interfaces does not (did not?). Our # Deeds & UX project slug is uppercase. # https://transifex.github.io/openapi/#tag/Projects - for project in self.api_organization.fetch( - "projects" - ): # pragma: no cover - # TODO: remove coveragepy exclusion after upgrade to Python 3.10 - # https://github.com/nedbat/coveragepy/issues/198 + for project in self.api_organization.fetch("projects"): if ( project.attributes["slug"] == transifex["DEEDS_UX_PROJECT_SLUG"] @@ -71,12 +67,9 @@ def __init__(self, dryrun: bool = True, logger: logging.Logger = None): for i18n_format in self.api.I18nFormat.filter( organization=self.api_organization - ): # pragma: no cover - # TODO: remove coveragepy exclusion after upgrade to Python 3.10 - # https://github.com/nedbat/coveragepy/issues/198 + ): if i18n_format.id == "PO": self.api_i18n_format = i18n_format - break self.projects = { "deeds_ux": { @@ -324,23 +317,38 @@ def upload_translation_to_transifex_resource( """ project_api = self.resource_to_api[resource_slug] + # Always perform following tests (regardless of push_overwrite) + # + # Raise error if attempting to push resource + if language_code == settings.LANGUAGE_CODE: + raise ValueError( + f"{self.nop}{resource_slug} {language_code}" + f" ({transifex_code}): This function," + " upload_translation_to_transifex_resource(), is for" + " translations, not sources." + ) + # Raise error if related resource is missing from Transifex + elif resource_slug not in self.resource_stats.keys(): + raise ValueError( + f"{self.nop}{resource_slug} {language_code}" + f" ({transifex_code}): Transifex does not yet contain" + " resource. The upload_resource_to_transifex() function" + " must be called before this one " + " [upload_translation_to_transifex_resource()]." + ) + # Skip push if there is nothing to push (local translation empty) + elif pofile_obj.percent_translated() == 0: + self.log.debug( + f"{self.nop}{resource_slug} {language_code}" + f" ({transifex_code}): Skipping upload of 0% complete" + f" translation: {pofile_path}" + ) + return + + # Only perform the follwoing tests if push_oversite is False if not push_overwrite: - if language_code == settings.LANGUAGE_CODE: - raise ValueError( - f"{self.nop}{resource_slug} {language_code}" - f" ({transifex_code}): This function," - " upload_translation_to_transifex_resource(), is for" - " translations, not sources." - ) - elif resource_slug not in self.resource_stats.keys(): - raise ValueError( - f"{self.nop}{resource_slug} {language_code}" - f" ({transifex_code}): Transifex does not yet contain" - " resource. The upload_resource_to_transifex() function" - " must be called before this one " - " [upload_translation_to_transifex_resource()]." - ) - elif ( + # Skip push if Transifex translation isn't empty + if ( resource_slug in self.translation_stats and transifex_code in self.translation_stats[resource_slug] and self.translation_stats[resource_slug][transifex_code].get( @@ -350,8 +358,8 @@ def upload_translation_to_transifex_resource( ): self.log.debug( f"{self.nop}{resource_slug} {language_code}" - f" ({transifex_code}): Transifex already contains" - " translation." + f" ({transifex_code}): Skipping upload of translation" + " already present on Transifex." ) return @@ -609,6 +617,34 @@ def normalize_pofile_last_translator( pofile_obj.save(pofile_path) return pofile_obj + def normalize_pofile_percent_translated( + self, + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ): + if transifex_code == settings.LANGUAGE_CODE: + return pofile_obj + + key = "Percent-Translated" + percent_translated = pofile_obj.percent_translated() + + if int(pofile_obj.metadata.get(key, 0)) == percent_translated: + return pofile_obj + + self.log.info( + f"{self.nop}{resource_name} ({resource_slug}) {transifex_code}:" + f" Correcting PO file '{key}':" + f"\n{pofile_path}: New value: '{percent_translated}'" + ) + if self.dryrun: + return pofile_obj + pofile_obj.metadata[key] = percent_translated + pofile_obj.save(pofile_path) + return pofile_obj + def normalize_pofile_project_id( self, transifex_code, @@ -663,6 +699,13 @@ def normalize_pofile_metadata( pofile_path, pofile_obj, ) + pofile_obj = self.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) pofile_obj = self.normalize_pofile_project_id( transifex_code, resource_slug, @@ -757,7 +800,7 @@ def normalize_pofile_dates( ) # Process revision date - if pofile_revision is None: + if pofile_revision is None and transifex_revision is not None: # Normalize Local PO File revision date if its empty or invalid pofile_obj = self.update_pofile_revision_datetime( resource_slug, @@ -1000,17 +1043,14 @@ def safesync_translation( ) continue - if pofile_entry.msgstr != transifex_msgstr: + elif pofile_entry.msgstr != transifex_msgstr: # Skip if neither local PO File nor Transifex are empty if ( pofile_entry.msgstr is not None and pofile_entry.msgstr != "" and transifex_msgstr is not None and transifex_msgstr != "" - ): # pragma: no cover - # TODO: remove coveragepy exclusion after upgrade to - # Python 3.10 - # https://github.com/nedbat/coveragepy/issues/198 + ): continue # Local PO file has translation and Transifex is empty elif ( @@ -1029,9 +1069,8 @@ def safesync_translation( or pofile_entry.msgstr == "" ) ): # pragma: no cover - # TODO: remove coveragepy exclusion after upgrade to - # Python 3.10 - # https://github.com/nedbat/coveragepy/issues/198 + # ¯\_(ツ)_/¯ this path is tested by: + # test_safesync_translation_with_pofile_changes # # Add missing translation changes_pofile.append(f"msgid {index:>4}: '{p_msgid}'") @@ -1169,6 +1208,13 @@ def save_transifex_to_pofile( pofile=transifex_pofile_content.decode(), encoding="utf-8" ) + # Ensure correct metadata values for items unsupported by Transifex + transifex_obj.metadata["Language-Django"] = language_code + transifex_obj.metadata["Language-Transifex"] = transifex_code + transifex_obj.metadata["Percent-Translated"] = ( + transifex_obj.percent_translated() + ) + # Overrite local PO File self.log.info( f"{self.nop}{resource_slug} {language_code} ({transifex_code}):" @@ -1489,6 +1535,15 @@ def normalize_translations( ) transifex_translated = t_stats["translated_strings"] + # Normalize percent translated + pofile_obj = self.normalize_pofile_percent_translated( + transifex_code, + resource_slug, + resource_name, + pofile_path, + pofile_obj, + ) + # Normalize Creation and Revision dates in local PO File pofile_obj = self.normalize_pofile_dates( resource_slug, diff --git a/i18n/utils.py b/i18n/utils.py index 7abcfd2a..40408e19 100644 --- a/i18n/utils.py +++ b/i18n/utils.py @@ -253,13 +253,19 @@ def map_legacy_to_django_language_code(legacy_language_code: str) -> str: return django_language_code -def get_default_language_for_jurisdiction( - jurisdiction_code, default_language=settings.LANGUAGE_CODE -): - # Input: a jurisdiction code - # Output: a CC language code +def get_default_language_for_jurisdiction_deed(jurisdiction_code): + default_language = DEFAULT_JURISDICTION_LANGUAGES.get( + jurisdiction_code, settings.LANGUAGE_CODE + ) + if default_language in settings.LANGUAGES_MOSTLY_TRANSLATED: + return default_language + else: + return settings.LANGUAGE_CODE + + +def get_default_language_for_jurisdiction_naive(jurisdiction_code): return DEFAULT_JURISDICTION_LANGUAGES.get( - jurisdiction_code, default_language + jurisdiction_code, settings.LANGUAGE_CODE ) diff --git a/legal_tools/git_utils.py b/legal_tools/git_utils.py index 186e32a7..68adb47e 100644 --- a/legal_tools/git_utils.py +++ b/legal_tools/git_utils.py @@ -67,18 +67,16 @@ def get_branch(repo_or_remote, name): if isinstance(repo_or_remote, git.Remote): remote = repo_or_remote prefix_length = len(remote.name) + 1 # "origin/" - for ref in remote.refs: # pragma: no cover - # TODO: investigate coveragepy exclusion after upgrade to - # Python 3.10. This code has been confirmed to execute during - # tests. May be an issue with Python pre-3.10 tracing. Examples: - # https://github.com/nedbat/coveragepy/issues/198 - # https://github.com/nedbat/coveragepy/issues/1175 + for ref in remote.refs: full_name = ref.name if full_name[prefix_length:] == name: return ref else: repo = repo_or_remote - return getattr(repo.heads, name) + try: + return getattr(repo.heads, name) + except AttributeError: + return None def branch_exists(repo_or_remote, name): @@ -128,14 +126,7 @@ def setup_local_branch(repo: git.Repo, branch_name: str): if branch_exists(origin, branch_name): repo.create_head(branch_name, get_branch(origin, branch_name)) branch = get_branch(repo, branch_name) - if not branch.tracking_branch(): # pragma: no cover - # TODO: investigate coveragepy exclusion after upgrade to - # Python 3.10. This code has been confirmed to execute during - # tests. May be an issue with Python pre-3.10 tracing. - # Examples: - # https://github.com/nedbat/coveragepy/issues/198 - # https://github.com/nedbat/coveragepy/issues/1175 - branch.set_tracking_branch(get_branch(origin, branch_name)) + branch.set_tracking_branch(get_branch(origin, branch_name)) assert branch.tracking_branch() branch.checkout(force=True) else: @@ -150,14 +141,7 @@ def setup_local_branch(repo: git.Repo, branch_name: str): # branch exists. branch = get_branch(repo, branch_name) branch.checkout(force=True) - if branch.tracking_branch(): # pragma: no cover - # TODO: investigate coveragepy exclusion after upgrade to Python - # 3.10. This code has been confirmed to execute during tests. May - # be an issue with Python pre-3.10 tracing. Examples: - # https://github.com/nedbat/coveragepy/issues/198 - # https://github.com/nedbat/coveragepy/issues/1175 - - # Use upstream branch tip commit + if branch.tracking_branch(): repo.head.reset( f"origin/{branch_name}", index=True, working_tree=True ) diff --git a/legal_tools/management/commands/load_html_files.py b/legal_tools/management/commands/20231010_load_html_files.py similarity index 100% rename from legal_tools/management/commands/load_html_files.py rename to legal_tools/management/commands/20231010_load_html_files.py diff --git a/legal_tools/management/commands/publish.py b/legal_tools/management/commands/publish.py index d57e33d3..4163f9d6 100644 --- a/legal_tools/management/commands/publish.py +++ b/legal_tools/management/commands/publish.py @@ -3,6 +3,7 @@ import os import socket from argparse import SUPPRESS, ArgumentParser +from copy import copy from multiprocessing import Pool from pathlib import Path from pprint import pprint @@ -16,7 +17,10 @@ # First-party/Local from i18n import DEFAULT_CSV_FILE -from i18n.utils import write_transstats_csv +from i18n.utils import ( + get_default_language_for_jurisdiction_deed, + write_transstats_csv, +) from legal_tools.git_utils import commit_and_push_changes, setup_local_branch from legal_tools.models import LegalCode, TranslationBranch, build_path from legal_tools.utils import ( @@ -25,7 +29,9 @@ save_bytes_to_file, save_redirect, save_url_as_static_file, + update_title, ) +from legal_tools.views import render_redirect ALL_TRANSLATION_BRANCHES = "###all###" LOG = logging.getLogger(__name__) @@ -77,39 +83,48 @@ def save_list(output_dir, category, language_code): ) -def save_deed(output_dir, tool, language_code): +def save_deed(output_dir, tool, language_code, opt_apache_only): # Function is at top level of module so that it can be pickled by # multiprocessing. - relpath, symlinks = tool.get_publish_files(language_code) - save_url_as_static_file( - output_dir, - url=build_path(tool.base_url, "deed", language_code), - relpath=relpath, - ) - for symlink in symlinks: - wrap_relative_symlink(output_dir, relpath, symlink) + if not opt_apache_only: + relpath, symlinks = tool.get_publish_files(language_code) + save_url_as_static_file( + output_dir, + url=build_path(tool.base_url, "deed", language_code), + relpath=relpath, + ) + for symlink in symlinks: + wrap_relative_symlink(output_dir, relpath, symlink) return tool.get_redirect_pairs(language_code) -def save_legal_code(output_dir, legal_code): +def save_legal_code(output_dir, legal_code, opt_apache_only): # Function is at top level of module so that it can be pickled by # multiprocessing. - ( - relpath, - symlinks, - redirects_data, - ) = legal_code.get_publish_files() - if relpath: - # Deed-only tools will not return a legal code relpath - save_url_as_static_file( - output_dir, - url=legal_code.legal_code_url, - relpath=relpath, - ) - for symlink in symlinks: - wrap_relative_symlink(output_dir, relpath, symlink) - for redirect_data in redirects_data: - save_redirect(output_dir, redirect_data) + if not opt_apache_only: + ( + relpath, + symlinks, + redirects_data, + ) = legal_code.get_publish_files() + if relpath: + # Deed-only tools will not return a legal code relpath + save_url_as_static_file( + output_dir, + url=legal_code.legal_code_url, + relpath=relpath, + ) + for symlink in symlinks: + wrap_relative_symlink(output_dir, relpath, symlink) + for redirect_data in redirects_data: + redirect_content = render_redirect( + title=redirect_data["title"], + destination=redirect_data["destination"], + language_code=redirect_data["language_code"], + ) + save_redirect( + output_dir, redirect_data["redirect_file"], redirect_content + ) return legal_code.get_redirect_pairs() @@ -218,9 +233,28 @@ def add_arguments(self, parser: ArgumentParser): help="Only copy and distill RDF/XML files", dest="rdf_only", ) + branch_args.add_argument( + "--apache", + "--apache-config-only", + action="store_true", + help="Only distill the Apache2 language redirects configuration", + dest="apache_only", + ) + + def check_titles(self): + LOG.info("Checking legal code titles") + log_level = copy(LOG.level) + LOG.setLevel(LOG_LEVELS[0]) + results = update_title(options={"dryrun": True}) + LOG.setLevel(log_level) + if results["records_requiring_update"] > 0: + raise CommandError( + "Legal code titles require an update. See the `update_title`" + " command." + ) def purge_output_dir(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return output_dir = self.output_dir LOG.info(f"Purging output_dir: {output_dir}") @@ -236,21 +270,21 @@ def purge_output_dir(self): os.remove(item) def call_collectstatic(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return LOG.info("Collecting static files") call_command("collectstatic", interactive=False) def write_robots_txt(self): """Create robots.txt to discourage indexing.""" - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return LOG.info("Writing robots.txt") robots = "User-agent: *\nDisallow: /\n".encode("utf-8") save_bytes_to_file(robots, os.path.join(self.output_dir, "robots.txt")) def copy_static_wp_content_files(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return hostname = socket.gethostname() output_dir = self.output_dir @@ -267,7 +301,7 @@ def copy_static_wp_content_files(self): copytree(source, destination) def copy_static_cc_legal_tools_files(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return hostname = socket.gethostname() output_dir = self.output_dir @@ -284,6 +318,8 @@ def copy_static_cc_legal_tools_files(self): copytree(source, destination) def copy_static_rdf_files(self): + if self.options["apache_only"]: + return hostname = socket.gethostname() output_dir = self.output_dir LOG.info("Copying static RDF/XML files") @@ -296,12 +332,14 @@ def copy_static_rdf_files(self): path, ) destination = os.path.join(output_dir, path) - copytree(source, destination) + copytree(source, destination, dirs_exist_ok=True) def distill_and_symlink_rdf_meta(self): """ Generate the index.rdf, images.rdf and copies the rest. """ + if self.options["apache_only"]: + return hostname = socket.gethostname() output_dir = self.output_dir dest_dir = os.path.join(output_dir, "rdf") @@ -338,7 +376,7 @@ def distill_and_symlink_rdf_meta(self): LOG.debug(f" ^{symlink}") def copy_legal_code_plaintext(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return hostname = socket.gethostname() legacy_dir = self.legacy_dir @@ -371,7 +409,7 @@ def copy_legal_code_plaintext(self): LOG.debug(f" {relative_name}") def distill_dev_index(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return hostname = socket.gethostname() output_dir = self.output_dir @@ -385,7 +423,7 @@ def distill_dev_index(self): ) def distill_lists(self): - if self.options["rdf_only"]: + if self.options["apache_only"] or self.options["rdf_only"]: return hostname = socket.gethostname() output_dir = self.output_dir @@ -411,6 +449,7 @@ def distill_legal_tools(self): output_dir = self.output_dir legal_codes = LegalCode.objects.validgroups() redirect_pairs_data = [] + default_languages_deeds = {} for group in legal_codes.keys(): tools = set() LOG.debug(f"{hostname}:{output_dir}") @@ -426,11 +465,31 @@ def distill_legal_tools(self): rdf_arguments = [] for legal_code in legal_codes[group]: tools.add(legal_code.tool) - legal_code_arguments.append((output_dir, legal_code)) + legal_code_arguments.append( + (output_dir, legal_code, self.options["apache_only"]) + ) for tool in tools: for language_code in settings.LANGUAGES_MOSTLY_TRANSLATED: - deed_arguments.append((output_dir, tool, language_code)) + deed_arguments.append( + ( + output_dir, + tool, + language_code, + self.options["apache_only"], + ) + ) rdf_arguments.append((output_dir, tool)) + if ( + tool.jurisdiction_code + and tool.jurisdiction_code not in default_languages_deeds + ): + if tool.version not in default_languages_deeds: + default_languages_deeds[tool.version] = {} + default_languages_deeds[tool.version][ + tool.jurisdiction_code + ] = get_default_language_for_jurisdiction_deed( + tool.jurisdiction_code, + ) if not self.options["rdf_only"]: redirect_pairs_data += self.pool.starmap( @@ -439,52 +498,122 @@ def distill_legal_tools(self): redirect_pairs_data += self.pool.starmap( save_legal_code, legal_code_arguments ) - self.pool.starmap(save_rdf, rdf_arguments) + if not self.options["apache_only"]: + self.pool.starmap(save_rdf, rdf_arguments) if self.options["rdf_only"]: return LOG.info("Writing Apache2 redirects configuration") + include_lines = [ + "# DO NOT EDIT MANUALLY", + "#", + "# This file was generated by the publish command.", + "# https://github.com/creativecommons/cc-legal-tools-app", + "#", + "# See related Apache2 httpd documentation:", + "# - https://httpd.apache.org/docs/2.4/mod/mod_alias.html", + "# - https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html", + "", + ] + + # Step 1: Language redirects + include_lines += [ + "#" * 79, + "# Step 1: Langauge redirects", + "# - Redirect alternate language codes to supported Django" + " language codes", + "# - Redirect legacy bug URLs to valid URLs", + "", + ] redirect_pairs = [] for pair_list in redirect_pairs_data: redirect_pairs += pair_list del redirect_pairs_data + # Add RedirectMatch for ccEngine bug URLs. Entries are added for each + # of the 4.0 licenses (versus only two regex) to increase readability. + # https://github.com/creativecommons/cc-legal-tools-app/issues/438 + for unit in ("by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"): + # deed + redirect_pairs.append( + [ + f"/licenses/{unit}/4[.]0/([^/]+)/(deed|deed[.]html)?", + f"/licenses/{unit}/4.0/deed.$1", + ] + ) + # legalcode + redirect_pairs.append( + [ + f"/licenses/{unit}/4[.]0/([^/]+)/legalcode(?:[.]html)?", + f"/licenses/{unit}/4.0/legalcode.$1", + ] + ) widths = [max(map(len, map(str, col))) for col in zip(*redirect_pairs)] + pad = widths[0] + 2 redirect_lines = [] for pair in redirect_pairs: pcre_match = f'"^{pair[0]}$"' - pad = widths[0] + 2 redirect_lines.append( f'RedirectMatch 301 {pcre_match.ljust(pad)} "{pair[1]}"' ) del redirect_pairs redirect_lines.sort(reverse=True) - include_lines = [ - "# DO NOT EDIT MANUALLY", - "#", - "# This file was generated by the publish command.", - "# https://github.com/creativecommons/cc-legal-tools-app", - "#", - "# It should be included from within an Apache2 httpd site config", - "#", - "# https://httpd.apache.org/docs/2.4/mod/mod_alias.html", - "# https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html", + include_lines += redirect_lines + del redirect_lines + + # Step 2: Redirect absent deed translations for ported licenses + # https://github.com/creativecommons/cc-legal-tools-app/issues/236 + step2_lines = [ "", - "########################################", - "# Step 1: Redirect mixed/uppercase to lowercase", - "#", - "# Must be set within virtual host context:", - "RewriteMap lowercase int:tolower", - "RewriteCond %{REQUEST_URI} ^/(licenses|publicdomain)", - "RewriteCond $1 [A-Z]", - "RewriteRule ^/?(.*)$ /${lowercase:$1} [R=301,L]", + "#" * 79, + "# Step 2: Redirect absent deed translations for ported licenses", + "# https://github.com/creativecommons/cc-legal-tools-app/issues/" + "236", "", - "########################################", - "#Step 2: Redirect alternate language codes to supported Django" - " language codes", + ] + for ver in sorted(default_languages_deeds.keys()): + for jur in sorted(default_languages_deeds[ver].keys()): + default_lang = default_languages_deeds[ver][jur] + step2_lines += [ + "RewriteCond %{REQUEST_FILENAME} !-f", + "RewriteCond %{REQUEST_FILENAME}.html !-f", + "RewriteCond %{REQUEST_FILENAME} !-l", + "RewriteCond %{REQUEST_FILENAME}.html !-l", + f"RewriteRule licenses/([a-z+-]+)/{ver}/{jur}/deed[.].*$" + f" /licenses/$1/{ver}/{jur}/deed.{default_lang} [R=301,L]", + ] + include_lines += step2_lines + del step2_lines + + # Step 3: Redirect absent deed translations for international/universal + # licenses: + # https://github.com/creativecommons/cc-legal-tools-app/issues/236 + step3_lines = [ + "", + "#" * 79, + "# Step 3: Redirect absent deed translations for international/" + "universal", + "# licenses", + "# https://github.com/creativecommons/cc-legal-tools-app/issues/" + "236", + "", + "RewriteCond %{REQUEST_FILENAME} !-f", + "RewriteCond %{REQUEST_FILENAME}.html !-f", + "RewriteCond %{REQUEST_FILENAME} !-l", + "RewriteCond %{REQUEST_FILENAME}.html !-l", + "RewriteRule licenses/([a-z-]+)/2.1/deed[.].*$" + " /licenses/$1/2.0/deed.en [R=301,L]", + "", + "RewriteCond %{REQUEST_FILENAME} !-f", + "RewriteCond %{REQUEST_FILENAME}.html !-f", + "RewriteCond %{REQUEST_FILENAME} !-l", + "RewriteCond %{REQUEST_FILENAME}.html !-l", + "RewriteRule licenses/([a-z+-]+)/([0-9.]+)/deed[.].*$" + " /licenses/$1/$2/deed.en [R=301,L]", "", ] - include_lines += redirect_lines - del redirect_lines + include_lines += step3_lines + del step3_lines + include_lines.append("# vim: ft=apache ts=4 sw=4 sts=4 sr noet") include_lines.append("") include_lines = "\n".join(include_lines).encode("utf-8") @@ -532,6 +661,7 @@ def distill_metadata_yaml(self): ) def distill_and_copy(self): + self.check_titles() self.purge_output_dir() self.call_collectstatic() self.write_robots_txt() @@ -560,7 +690,6 @@ def checkout_publish_and_push(self): self.distill_and_copy() if repo.is_dirty(untracked_files=True): # Add any changes and new files - commit_and_push_changes( repo, "Update static files generated by cc-legal-tools-app", diff --git a/legal_tools/management/commands/update_title.py b/legal_tools/management/commands/update_title.py new file mode 100644 index 00000000..f8492e39 --- /dev/null +++ b/legal_tools/management/commands/update_title.py @@ -0,0 +1,44 @@ +# Standard library +import logging +from argparse import ArgumentParser + +# Third-party +from django.core.management import BaseCommand + +# First-party/Local +from legal_tools.utils import init_utils_logger, update_title + +LOG = logging.getLogger(__name__) +LOG_LEVELS = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, +} + + +class Command(BaseCommand): + """ + Update the title property of all legal tools by normalizing legalcy titles + and normalizing translated titles for current legal tools (Licenses 4.0 and + CC0 1.0). + """ + + def add_arguments(self, parser: ArgumentParser): + # Python defaults to lowercase starting character for the first + # character of help text, but Djano appears to use uppercase and so + # shall we + parser.description = self.__doc__ + parser._optionals.title = "Django optional arguments" + parser.add_argument( + "-n", + "--dryrun", + action="store_true", + help="dry run: do not make any changes", + ) + + def handle(self, **options): + self.options = options + LOG.setLevel(LOG_LEVELS[int(options["verbosity"])]) + init_utils_logger(LOG) + update_title(options) diff --git a/legal_tools/models.py b/legal_tools/models.py index e8973e20..b65a4315 100644 --- a/legal_tools/models.py +++ b/legal_tools/models.py @@ -12,7 +12,8 @@ # First-party/Local from i18n import LANGMAP_DJANGO_TO_PCRE from i18n.utils import ( - get_default_language_for_jurisdiction, + get_default_language_for_jurisdiction_deed, + get_default_language_for_jurisdiction_naive, get_jurisdiction_name, get_pofile_path, get_translation_object, @@ -248,7 +249,9 @@ def get_publish_files(self): language_code = self.language_code tool = self.tool juris_code = tool.jurisdiction_code - language_default = get_default_language_for_jurisdiction(juris_code) + language_default = get_default_language_for_jurisdiction_naive( + juris_code + ) filename = f"legalcode.{self.language_code}.html" # Relative path @@ -345,7 +348,7 @@ def translation_domain(self): return self.tool.resource_slug def get_translation_object(self): - language_default = get_default_language_for_jurisdiction( + language_default = get_default_language_for_jurisdiction_naive( self.tool.jurisdiction_code ) return get_translation_object( @@ -551,7 +554,7 @@ def get_metadata(self): """ Return a dictionary with the metadata for this tool. """ - language_default = get_default_language_for_jurisdiction( + language_default = get_default_language_for_jurisdiction_deed( self.jurisdiction_code ) data = {} @@ -598,7 +601,9 @@ def get_publish_files(self, language_code): correctly """ juris_code = self.jurisdiction_code - language_default = get_default_language_for_jurisdiction(juris_code) + language_default = get_default_language_for_jurisdiction_deed( + juris_code + ) filename = f"deed.{language_code}.html" # Relative path diff --git a/legal_tools/tests/test_git_utils.py b/legal_tools/tests/test_git_utils.py index 36e4c88b..54071073 100644 --- a/legal_tools/tests/test_git_utils.py +++ b/legal_tools/tests/test_git_utils.py @@ -39,10 +39,11 @@ def setUp(self): self.upstream_repo_path = os.path.join(self.temp_dir_path, "upstream") os.makedirs(self.upstream_repo_path) - self.origin_repo = git.Repo.init(self.upstream_repo_path) + self.origin_repo = git.Repo.init( + self.upstream_repo_path, initial_branch="main" + ) self.origin_repo.index.commit("Initial commit") self.origin_repo.create_head("otherbranch", "HEAD") - self.origin_repo.create_head("main", "HEAD") # "checkout" main self.origin_repo.heads.main.checkout() # We want the main branch to be a different commit from otherbranch so @@ -71,28 +72,6 @@ def add_file(self, repo): @override_settings(DATA_REPOSITORY_DIR="/trans/repo") class SetupLocalBranchTest(GitTestMixin, TestCase): - def test_setup_local_branch_protocol_error(self): - with mock.patch("sys.stderr", new_callable=StringIO) as mock_err: - with self.assertRaises(SystemExit): - with mock.patch("git.remote.Remote.fetch") as mock_fetch: - mock_fetch.side_effect = git.exc.GitCommandError( - "Mock_Error", 1, stderr="protocol error" - ) - setup_local_branch(self.local_repo, "branch_name") - - self.assertEqual( - mock_err.getvalue().strip(), - "ERROR: git origin.fetch() stderr: 'protocol error'. Check git" - " remote access/authentication.", - ) - - def test_setup_local_branch_other_error(self): - with self.assertRaises(git.exc.GitCommandError): - with mock.patch("git.remote.Remote.fetch") as mock_fetch: - mock_fetch.side_effect = git.exc.GitCommandError( - "Mock_Error", 1 - ) - setup_local_branch(self.local_repo, "branch_name") def test_branch_exists_nowhere_but_parent_does(self): # No "ourbranch" locally or upstream, so we branch from origin/main @@ -155,6 +134,30 @@ def test_branch_exists_locally_and_upstream(self): self.assertEqual(upstream_commit, our_branch.commit) self.assertNotEqual(old_local_repo_commit, our_branch.commit) + def test_get_branch_present_upstream(self): + self.origin_repo.heads.otherbranch.checkout() + present_branch = get_branch( + self.local_repo.remotes.origin, "otherbranch" + ) + self.assertEqual(present_branch.name, "origin/otherbranch") + + def test_get_branch_missing_upstream(self): + self.origin_repo.heads.otherbranch.checkout() + missing_branch = get_branch( + self.local_repo.remotes.origin, "originmissing" + ) + self.assertIsNone(missing_branch) + + def test_get_branch_present_locally(self): + self.local_repo.heads.otherbranch.checkout() + present_branch = get_branch(self.local_repo, "otherbranch") + self.assertEqual(present_branch.name, "otherbranch") + + def test_get_branch_missing_locally(self): + self.local_repo.heads.otherbranch.checkout() + missing_branch = get_branch(self.local_repo, "localmissing") + self.assertIsNone(missing_branch) + def test_kill_branch(self): self.origin_repo.create_head("deletemebranch") @@ -168,6 +171,72 @@ def test_kill_branch(self): "'IterableList' object has no attribute 'deletemebranch'", ) + def test_setup_local_branch_protocol_error(self): + with mock.patch("sys.stderr", new_callable=StringIO) as mock_err: + with self.assertRaises(SystemExit): + with mock.patch("git.remote.Remote.fetch") as mock_fetch: + mock_fetch.side_effect = git.exc.GitCommandError( + "Mock_Error", 1, stderr="protocol error" + ) + setup_local_branch(self.local_repo, "branch_name") + + self.assertEqual( + mock_err.getvalue().strip(), + "ERROR: git origin.fetch() stderr: 'protocol error'. Check git" + " remote access/authentication.", + ) + + def test_setup_local_branch_other_error(self): + with self.assertRaises(git.exc.GitCommandError): + with mock.patch("git.remote.Remote.fetch") as mock_fetch: + mock_fetch.side_effect = git.exc.GitCommandError( + "Mock_Error", 1 + ) + setup_local_branch(self.local_repo, "branch_name") + + def test_setup_local_branch_with_local_missing_and_origin_present(self): + # newbranch exists upstream + self.origin_repo.create_head("newbranch") + + setup_local_branch(self.local_repo, "newbranch") + new_branch = get_branch(self.local_repo, "newbranch") + self.assertEqual(new_branch.name, "newbranch") + self.assertTrue(new_branch.tracking_branch()) + + def test_setup_local_branch_with_local_missing_and_origin_missing(self): + # newbranch does not exist upstream + + setup_local_branch(self.local_repo, "newbranch") + new_branch = get_branch(self.local_repo, "newbranch") + self.assertEqual(new_branch.name, "newbranch") + self.assertFalse(new_branch.tracking_branch()) + + def test_setup_local_branch_with_local_present_and_with_tracking(self): + # otherbranch exists and is tracking + other_branch = get_branch(self.local_repo, "otherbranch") + other_branch.set_tracking_branch( + get_branch(self.local_repo.remotes.origin, "otherbranch") + ) + + setup_local_branch(self.local_repo, "otherbranch") + other_branch = get_branch(self.local_repo, "otherbranch") + self.assertEqual(other_branch.name, "otherbranch") + self.assertTrue(other_branch.tracking_branch()) + + def test_setup_local_branch_with_local_present_and_without_tracking(self): + # otherbranch exits and isn't tracking + other_branch = get_branch(self.local_repo, "otherbranch") + # set tracking to intialize configuration + other_branch.set_tracking_branch( + get_branch(self.local_repo.remotes.origin, "otherbranch") + ) + other_branch.set_tracking_branch(None) + + setup_local_branch(self.local_repo, "otherbranch") + other_branch = get_branch(self.local_repo, "otherbranch") + self.assertEqual(other_branch.name, "otherbranch") + self.assertFalse(other_branch.tracking_branch()) + @override_settings(DATA_REPOSITORY_DIR="/trans/repo") class CommitAndPushChangesTest(GitTestMixin, TestCase): diff --git a/legal_tools/tests/test_models.py b/legal_tools/tests/test_models.py index b20aded8..8df1a03c 100644 --- a/legal_tools/tests/test_models.py +++ b/legal_tools/tests/test_models.py @@ -290,8 +290,9 @@ def test_has_english(self): self.assertTrue(lc_fr.has_english()) self.assertTrue(lc_en.has_english()) - # get_publish_files BY-NC-ND 4.0 ######################################### - # BY-NC-ND 4.0 is an international license with multiple languages + # get_publish_files BY-NC-ND 4.0 legal code ############################## + # BY-NC-ND 4.0 is an international (unported) license with multiple + # languages def test_get_publish_files_by_nc_nd4_legal_code_en(self): legal_code = LegalCodeFactory( @@ -337,7 +338,7 @@ def test_get_publish_files_by_nc_nd_4_legal_code_zh_hant(self): returned_list, ) - # get_publish_files BY-NC 3.0 CA ######################################### + # get_publish_files BY-NC 3.0 CA legal code ############################## # BY-NC 3.0 CA is a ported license with multiple languages def test_get_publish_files_by_nc3_legal_code_ca_en(self): @@ -363,7 +364,7 @@ def test_get_publish_files_by_nc3_legal_code_ca_en(self): returned_list, ) - # get_publish_files BY-SA 3.0 AM ######################################### + # get_publish_files BY-SA 3.0 AM legal code ############################## # BY-SA 3.0 AM is a ported license with a single language def test_get_publish_files_by_sa3_legal_code_am_hy(self): @@ -389,8 +390,8 @@ def test_get_publish_files_by_sa3_legal_code_am_hy(self): returned_list, ) - # LegalCode.get_publish_files CC0 1.0 #################################### - # CC0 1.0 is an unported dedication with multiple languages + # get_publish_files CC0 1.0 legal code ################################### + # CC0 1.0 is a univeral (unported) dedication with multiple languages def test_get_publish_files_zero_legal_code_en(self): legal_code = LegalCodeFactory( @@ -436,8 +437,8 @@ def test_get_publish_files_zero_legal_code_nl(self): returned_list, ) - # get_publish_files Mark 1.0 ############################################# - # Mark 1.0 is an unported deed-only declaration + # get_publish_files Mark 1.0 legal code ################################## + # Mark 1.0 is a universal (unported) deed-only declaration def test_get_publish_files_mark_legal_code_en(self): legal_code = LegalCodeFactory( @@ -732,8 +733,9 @@ def test_get_metadata(self): for key in expected_data.keys(): self.assertEqual(expected_data[key], data[key]) - # get_publish_files BY-NC-ND 4.0 ######################################### - # BY-NC-ND 4.0 is an international license with multiple languages + # get_publish_files BY-NC-ND 4.0 deed #################################### + # BY-NC-ND 4.0 is an international (unported) license with multiple + # languages def test_get_publish_files_by_nc_nd4_deed_en(self): language_code = "en" @@ -755,7 +757,7 @@ def test_get_publish_files_by_nc_nd4_deed_en(self): returned_list, ) - # get_publish_files BY-NC 3.0 CA ######################################### + # get_publish_files BY-NC 3.0 CA deed #################################### # BY-NC 3.0 CA is a ported license with multiple languages def test_get_publish_files_by_nc3_deed_ca_en(self): @@ -779,8 +781,9 @@ def test_get_publish_files_by_nc3_deed_ca_en(self): returned_list, ) - # get_publish_files BY-NC-ND 4.0 ######################################### - # BY-NC-ND 4.0 is an international license with multiple languages + # get_publish_files BY-NC-ND 4.0 deed #################################### + # BY-NC-ND 4.0 is an international (unported) license with multiple + # languages def test_get_publish_files_by_nc_nd_4_deed_zh_hant(self): # English content is returned as translation.activate() is not used @@ -803,9 +806,34 @@ def test_get_publish_files_by_nc_nd_4_deed_zh_hant(self): returned_list, ) - # get_publish_files BY-SA 3.0 AM ######################################### + # get_publish_files BY-SA 3.0 AM deed #################################### # BY-SA 3.0 AM is a ported license with a single language + def test_get_publish_files_by_sa3_deed_am_en(self): + # English content is returned as translation.activate() is not used + language_code = "en" + tool = ToolFactory( + category="licenses", + jurisdiction_code="am", + unit="by-sa", + version="3.0", + ) + + returned_list = tool.get_publish_files(language_code) + + # symlinks should be populated as the Armenian Deeds & UX translation + # is incomplete + # https://github.com/creativecommons/cc-legal-tools-app/issues/444 + self.assertEqual( + [ + # relpath + "licenses/by-sa/3.0/am/deed.en.html", + # symlinks + ["deed.html", "index.html"], + ], + returned_list, + ) + def test_get_publish_files_by_sa3_deed_am_hy(self): # English content is returned as translation.activate() is not used language_code = "hy" @@ -818,18 +846,21 @@ def test_get_publish_files_by_sa3_deed_am_hy(self): returned_list = tool.get_publish_files(language_code) + # symlinks shouldn't be populated as the Armenian Deeds & UX + # translation is incomplete + # https://github.com/creativecommons/cc-legal-tools-app/issues/444 self.assertEqual( [ # relpath "licenses/by-sa/3.0/am/deed.hy.html", # symlinks - ["deed.html", "index.html"], + [], ], returned_list, ) - # get_publish_files CC0 1.0 ############################################## - # CC0 1.0 is an unported dedication with multiple languages + # get_publish_files CC0 1.0 deed ######################################### + # CC0 1.0 is a universal (unported) dedication with multiple languages def test_get_publish_files_zero_deed_en(self): language_code = "en" diff --git a/legal_tools/tests/test_utils.py b/legal_tools/tests/test_utils.py index 4caf6f57..2d84dc83 100644 --- a/legal_tools/tests/test_utils.py +++ b/legal_tools/tests/test_utils.py @@ -149,27 +149,12 @@ def test_relative_symlink(self): def test_save_redirect(self): output_dir = "/OUTPUT_DIR" - redirect_data = { - "destination": "DESTINATION", - "language_code": "LANGUAGE_CODE", - "redirect_file": ("FILE_PATH"), - "title": "TITLE", - } - - with mock.patch( - "legal_tools.utils.render_redirect", - return_value="STRING", - ) as mock_render: - with mock.patch( - "legal_tools.utils.save_bytes_to_file" - ) as mock_save: - utils.save_redirect(output_dir, redirect_data) - - mock_render.assert_called_with( - title="TITLE", - destination="DESTINATION", - language_code="LANGUAGE_CODE", - ) + redirect_file = "FILE_PATH" + redirect_content = "STRING" + + with mock.patch("legal_tools.utils.save_bytes_to_file") as mock_save: + utils.save_redirect(output_dir, redirect_file, redirect_content) + mock_save.assert_called_with("STRING", "/OUTPUT_DIR/FILE_PATH") @@ -309,8 +294,6 @@ def test_parse_legal_code_filename(self): self.assertEqual(expected_result, result) with self.assertRaisesMessage(ValueError, "Invalid language_code="): utils.parse_legal_code_filename("by_3.0_es_aaa") - with self.assertRaisesMessage(ValueError, "What language? "): - utils.parse_legal_code_filename("by_3.0_zz") class GetToolUtilityTest(TestCase): @@ -569,3 +552,79 @@ def validate_udpate_source(): # Subsequent run to test with wrong data and verify behavior of # repeated runs validate_udpate_source() + + +class TitleTest(TestCase): + def setup(self): + for version in ("1.0", "4.0"): + ToolFactory(category="licenses", unit="by", version=version) + for tool in Tool.objects.all(): + LegalCodeFactory(tool=tool, language_code="fr") + title_en = utils.get_tool_title_en( + tool.unit, + tool.version, + tool.category, + tool.jurisdiction_code, + ) + LegalCodeFactory(tool=tool, title=title_en, language_code="en") + LegalCodeFactory(tool=tool, title=title_en, language_code="nl") + if tool.version == "1.0": + LegalCodeFactory( + tool=tool, + title="Namensnennung 1.0 Generic", + language_code="de", + ) + elif tool.version == "4.0": + LegalCodeFactory( + tool=tool, + title="Namensnennung 4.0 International", + language_code="de", + ) + + def test_get_tool_title(self): + self.setup() + unit = "by" + category = "licenses" + jurisdiction = "" + titles = {} + + with self.assertNumQueries(6): + for version in ("1.0", "4.0"): + for language_code in ("de", "en", "fr", "nl"): + title = utils.get_tool_title( + unit=unit, + version=version, + category=category, + jurisdiction=jurisdiction, + language_code=language_code, + ) + titles[f"{version}{language_code}"] = title + + self.assertEqual("Namensnennung 1.0 Generic", titles["1.0de"]) + self.assertEqual("Attribution 1.0 Generic", titles["1.0en"]) + self.assertEqual("Attribution 1.0 Générique", titles["1.0fr"]) + self.assertEqual("Naamsvermelding 1.0 Unported", titles["1.0nl"]) + self.assertEqual("Namensnennung 4.0 International", titles["4.0de"]) + self.assertEqual("Attribution 4.0 International", titles["4.0en"]) + self.assertEqual("Attribution 4.0 International", titles["4.0fr"]) + self.assertEqual("Naamsvermelding 4.0 Internationaal", titles["4.0nl"]) + + def test_update_titles_dryrun(self): + self.setup() + + with self.assertNumQueries(9): + results = utils.update_title({"dryrun": True}) + + self.assertEqual( + {"records_updated": 0, "records_requiring_update": 4}, results + ) + + def test_update_titles_with_updates(self): + self.setup() + + with self.assertNumQueries(13): + results = utils.update_title({"dryrun": False}) + + self.assertEqual( + {"records_updated": 4, "records_requiring_update": 0}, results + ) diff --git a/legal_tools/tests/test_views.py b/legal_tools/tests/test_views.py index 0e6ec19d..50c1ab69 100644 --- a/legal_tools/tests/test_views.py +++ b/legal_tools/tests/test_views.py @@ -4,12 +4,14 @@ # Third-party from django.conf import settings +from django.core.cache import cache from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone from django.utils.translation.trans_real import DjangoTranslation # First-party/Local +from i18n.utils import get_default_language_for_jurisdiction_deed from legal_tools.models import UNITS_LICENSES, LegalCode, Tool, build_path from legal_tools.rdf_utils import ( convert_https_to_http, @@ -25,6 +27,7 @@ branch_status_helper, get_category_and_category_title, get_deed_rel_path, + get_legal_code_replaced_rel_path, normalize_path_and_lang, render_redirect, ) @@ -240,6 +243,26 @@ def setUp(self): LegalCodeFactory(tool=tool, language_code="es") LegalCodeFactory(tool=tool, language_code="fr") + self.by_30_es = ToolFactory( + base_url="https://creativecommons.org/licenses/by/3.0/es/", + category="licenses", + unit="by", + version="3.0", + jurisdiction_code="es", + is_replaced_by=self.by_40, + permits_derivative_works=True, + permits_reproduction=True, + permits_distribution=True, + permits_sharing=True, + requires_share_alike=False, + requires_notice=True, + requires_attribution=True, + prohibits_commercial_use=False, + prohibits_high_income_nation_use=False, + ) + # global default language + LegalCodeFactory(tool=self.by_30_es, language_code="en") + self.by_sa_30_es = ToolFactory( base_url="https://creativecommons.org/licenses/by-sa/3.0/es/", category="licenses", @@ -257,9 +280,8 @@ def setUp(self): prohibits_commercial_use=False, prohibits_high_income_nation_use=False, ) - LegalCodeFactory( # Jurisdiction default language - tool=self.by_sa_30_es, language_code="es" - ) + # Jurisdiction default language + LegalCodeFactory(tool=self.by_sa_30_es, language_code="es") LegalCodeFactory(tool=self.by_sa_30_es, language_code="ca") self.by_sa_30_igo = ToolFactory( @@ -279,9 +301,8 @@ def setUp(self): prohibits_commercial_use=False, prohibits_high_income_nation_use=False, ) - LegalCodeFactory( # Jurisdiction default language - tool=self.by_sa_30_igo, language_code="en" - ) + # Jurisdiction default language + LegalCodeFactory(tool=self.by_sa_30_igo, language_code="en") LegalCodeFactory(tool=self.by_sa_30_igo, language_code="fr") self.by_30_th = ToolFactory( @@ -301,9 +322,8 @@ def setUp(self): prohibits_commercial_use=False, prohibits_high_income_nation_use=False, ) - LegalCodeFactory( # Jurisdiction default language - tool=self.by_30_th, language_code="th" - ) + # Jurisdiction default language + LegalCodeFactory(tool=self.by_30_th, language_code="th") self.by_sa_20_es = ToolFactory( base_url="https://creativecommons.org/licenses/by-sa/2.0/es/", @@ -322,9 +342,8 @@ def setUp(self): prohibits_commercial_use=False, prohibits_high_income_nation_use=False, ) - LegalCodeFactory( # Jurisdiction default language - tool=self.by_sa_20_es, language_code="es" - ) + # Jurisdiction default language + LegalCodeFactory(tool=self.by_sa_20_es, language_code="es") self.devnations = ToolFactory( base_url="https://creativecommons.org/licenses/devnations/2.0/", @@ -373,6 +392,104 @@ def setUp(self): super().setUp() +class ViewHelperFunctionsTest(ToolsTestsMixin, TestCase): + def test_get_category_and_category_title_category_tool(self): + category, category_title = get_category_and_category_title( + category=None, + tool=None, + ) + self.assertEqual(category, "licenses") + self.assertEqual(category_title, "Licenses") + + tool = Tool.objects.get(unit="by", version="4.0") + category, category_title = get_category_and_category_title( + category=None, + tool=tool, + ) + self.assertEqual(category, "licenses") + self.assertEqual(category_title, "Licenses") + + def test_get_category_and_category_title_category_publicdomain(self): + category, category_title = get_category_and_category_title( + category="publicdomain", + tool=None, + ) + self.assertEqual(category, "publicdomain") + self.assertEqual(category_title, "Public Domain") + + @override_settings(LANGUAGES_MOSTLY_TRANSLATED=["x1", "x2"]) + def test_get_deed_rel_path_mostly_translated_language_code(self): + expected_deed_rel_path = "deed.x1" + deed_rel_path = get_deed_rel_path( + deed_url="/deed.x1", + path_start="/", + language_code="x1", + language_default="x2", + ) + self.assertEqual(expected_deed_rel_path, deed_rel_path) + + @override_settings(LANGUAGES_MOSTLY_TRANSLATED=["x1", "x2"]) + def test_get_deed_rel_path_less_translated_language_code(self): + expected_deed_rel_path = "deed.x2" + deed_rel_path = get_deed_rel_path( + deed_url="/deed.x3", + path_start="/", + language_code="x3", + language_default="x2", + ) + self.assertEqual(expected_deed_rel_path, deed_rel_path) + + @override_settings( + LANGUAGE_CODE="x1", + LANGUAGES_MOSTLY_TRANSLATED=[], + ) + def test_get_deed_rel_path_less_translated_language_default(self): + expected_deed_rel_path = "deed.x1" + deed_rel_path = get_deed_rel_path( + deed_url="/deed.x3", + path_start="/", + language_code="x3", + language_default="x2", + ) + self.assertEqual(expected_deed_rel_path, deed_rel_path) + + def test_get_legal_code_replaced_rel_path_cache_miss(self): + tool = Tool.objects.get( + unit="by", + version="3.0", + jurisdiction_code="", + ) + path_start = "/licenses/by/3.0" + language_code = "en" + language_default = get_default_language_for_jurisdiction_deed(None) + + cache.clear() + self.assertFalse(cache.has_key("by-4.0--en-replaced_deed_str")) + self.assertFalse(cache.has_key("by-4.0--en-replaced_legal_code_title")) + _, _, _, _ = get_legal_code_replaced_rel_path( + tool.is_replaced_by, path_start, language_code, language_default + ) + self.assertEqual( + cache.get("by-4.0--en-replaced_deed_title"), + "Deed - Attribution 4.0 International", + ) + self.assertEqual( + cache.get("by-4.0--en-replaced_legal_code_title"), + "Legal Code - Attribution 4.0 International", + ) + + def test_normalize_path_and_lang(self): + request_path = "/licenses/by/3.0/de/legalcode" + jurisdiction = "de" + norm_request_path, norm_language_code = normalize_path_and_lang( + request_path, + jurisdiction, + language_code=None, + ) + self.assertEqual(norm_request_path, f"{request_path}.de") + self.assertEqual(norm_language_code, "de") + + class ViewDevHomeTest(ToolsTestsMixin, TestCase): def test_view_dev_index_view(self): url = reverse("dev_index") @@ -512,6 +629,39 @@ def test_deed_translation_by_40_gl(self): self.assertContains(rsp, "Sen restricións adicionais") self.assertContains(rsp, "Notas") + def test_deed_translation_by_30_es_gl(self): + # Test with valid Deed & UX and *invalid* Legal Code translations + # (with no jurisdiction default language legal code, but with + # global default lanage legal code) + language_code = "gl" + tool = Tool.objects.get( + unit="by", + version="3.0", + jurisdiction_code="es", + ) + url = os.path.join( + "/", + tool.category, + tool.unit, + tool.version, + tool.jurisdiction_code, + f"deed.{language_code}", + ) + rsp = self.client.get(url) + text = rsp.content.decode("utf-8") + self.assertEqual(f"{rsp.status_code} {url}", f"200 {url}") + if ( + "INVALID_VARIABLE" in text + ): # Some unresolved variable in the template + msgs = ["INVALID_VARIABLE in output"] + for line in text.splitlines(): + if "INVALID_VARIABLE" in line: + msgs.append(line) + self.fail("\n".join(msgs)) + self.assertContains(rsp, "Atribución") + self.assertContains(rsp, "Sen restricións adicionais") + self.assertContains(rsp, "Notas") + def test_view_deed_template_body_tools(self): lc = LegalCode.objects.get( tool__unit="by", tool__version="4.0", language_code="en" @@ -737,81 +887,6 @@ class ViewLegalCodeTest(TestCase): # self.assertContains(rsp, 'lang="de"') # self.assertEqual(lc, context["legal_code"]) - def test_get_category_and_category_title_category_tool(self): - category, category_title = get_category_and_category_title( - category=None, - tool=None, - ) - self.assertEqual(category, "licenses") - self.assertEqual(category_title, "Licenses") - - tool = ToolFactory( - category="licenses", - base_url="https://creativecommons.org/licenses/by/4.0/", - version="4.0", - ) - category, category_title = get_category_and_category_title( - category=None, - tool=tool, - ) - self.assertEqual(category, "licenses") - self.assertEqual(category_title, "Licenses") - - def test_get_category_and_category_title_category_publicdomain(self): - category, category_title = get_category_and_category_title( - category="publicdomain", - tool=None, - ) - self.assertEqual(category, "publicdomain") - self.assertEqual(category_title, "Public Domain") - - def test_normalize_path_and_lang(self): - request_path = "/licenses/by/3.0/de/legalcode" - jurisdiction = "de" - norm_request_path, norm_language_code = normalize_path_and_lang( - request_path, - jurisdiction, - language_code=None, - ) - self.assertEqual(norm_request_path, f"{request_path}.de") - self.assertEqual(norm_language_code, "de") - - @override_settings(LANGUAGES_MOSTLY_TRANSLATED=["x1", "x2"]) - def test_get_deed_rel_path_mostly_translated_language_code(self): - expected_deed_rel_path = "deed.x1" - deed_rel_path = get_deed_rel_path( - deed_url="/deed.x1", - path_start="/", - language_code="x1", - language_default="x2", - ) - self.assertEqual(expected_deed_rel_path, deed_rel_path) - - @override_settings(LANGUAGES_MOSTLY_TRANSLATED=["x1", "x2"]) - def test_get_deed_rel_path_less_translated_language_code(self): - expected_deed_rel_path = "deed.x2" - deed_rel_path = get_deed_rel_path( - deed_url="/deed.x3", - path_start="/", - language_code="x3", - language_default="x2", - ) - self.assertEqual(expected_deed_rel_path, deed_rel_path) - - @override_settings( - LANGUAGE_CODE="x1", - LANGUAGES_MOSTLY_TRANSLATED=[], - ) - def test_get_deed_rel_path_less_translated_language_default(self): - expected_deed_rel_path = "deed.x1" - deed_rel_path = get_deed_rel_path( - deed_url="/deed.x3", - path_start="/", - language_code="x3", - language_default="x2", - ) - self.assertEqual(expected_deed_rel_path, deed_rel_path) - def test_view_legal_code_identifying_jurisdiction_default_language(self): language_code = "de" lc = LegalCodeFactory( @@ -1054,32 +1129,33 @@ def setUp(self): language_code="fr", ) - def test_simple_branch(self): - url = reverse( - "branch_status", kwargs=dict(id=self.translation_branch.id) - ) - with mock.patch("legal_tools.views.git"): - with mock.patch.object(LegalCode, "get_pofile"): - with mock.patch( - "legal_tools.views.branch_status_helper" - ) as mock_helper: - mock_helper.return_value = { - "official_git_branch": settings.OFFICIAL_GIT_BRANCH, - "branch": self.translation_branch, - "commits": [], - "last_commit": None, - } - r = self.client.get(url) - # Call a second time to test cache and fully exercise - # branch_status() - self.client.get(url) - mock_helper.assert_called_with(mock.ANY, self.translation_branch) - self.assertTemplateUsed(r, "dev/branch_status.html") - context = r.context - self.assertEqual(self.translation_branch, context["branch"]) - self.assertEqual( - settings.OFFICIAL_GIT_BRANCH, context["official_git_branch"] - ) + # TODO: evalute when branch status is re-implemented + # def test_simple_branch(self): + # url = reverse( + # "branch_status", kwargs=dict(id=self.translation_branch.id) + # ) + # with mock.patch("legal_tools.views.git"): + # with mock.patch.object(LegalCode, "get_pofile"): + # with mock.patch( + # "legal_tools.views.branch_status_helper" + # ) as mock_helper: + # mock_helper.return_value = { + # "official_git_branch": settings.OFFICIAL_GIT_BRANCH, + # "branch": self.translation_branch, + # "commits": [], + # "last_commit": None, + # } + # r = self.client.get(url) + # # Call a second time to test cache and fully exercise + # # branch_status() + # self.client.get(url) + # mock_helper.assert_called_with(mock.ANY, self.translation_branch) + # self.assertTemplateUsed(r, "dev/branch_status.html") + # context = r.context + # self.assertEqual(self.translation_branch, context["branch"]) + # self.assertEqual( + # settings.OFFICIAL_GIT_BRANCH, context["official_git_branch"] + # ) def test_branch_helper_local_branch_exists(self): mock_repo = mock.MagicMock() diff --git a/legal_tools/urls.py b/legal_tools/urls.py index 0e37f281..d8f74702 100644 --- a/legal_tools/urls.py +++ b/legal_tools/urls.py @@ -30,7 +30,7 @@ # See Converter functions, below, for a description of the following regex: RE_CATEGORY = r"licenses|publicdomain" RE_JURISDICTION = r"[a-z]{2}|igo|scotland" -RE_UNIT = r"(?i)[-a-z0-9+]+" +RE_UNIT = r"[-a-z0-9+]+" RE_VERSION = r"[0-9]+[.][0-9]+" diff --git a/legal_tools/utils.py b/legal_tools/utils.py index a5e6ac69..2053a370 100644 --- a/legal_tools/utils.py +++ b/legal_tools/utils.py @@ -5,16 +5,22 @@ # Third-party from bs4 import NavigableString +from colorlog.escape_codes import escape_codes from django.conf import settings +from django.core.cache import cache from django.urls import get_resolver +from django.utils import translation # First-party/Local import legal_tools.models +from i18n import UNIT_NAMES from i18n.utils import ( - get_default_language_for_jurisdiction, + active_translation, + get_default_language_for_jurisdiction_naive, + get_jurisdiction_name, + get_translation_object, map_legacy_to_django_language_code, ) -from legal_tools.views import render_redirect LOG = logging.getLogger(__name__) @@ -41,7 +47,7 @@ def save_bytes_to_file(filebytes, output_filename): if os.path.isfile(dirname): os.remove(dirname) os.makedirs(dirname, mode=0o755, exist_ok=True) - with open(output_filename, "w+b") as f: + with open(output_filename, "wb") as f: f.write(filebytes) @@ -83,18 +89,12 @@ def relative_symlink(src1, src2, dst): os.close(dir_fd) -def save_redirect(output_dir, redirect_data): - relpath = redirect_data["redirect_file"] - content = render_redirect( - title=redirect_data["title"], - destination=redirect_data["destination"], - language_code=redirect_data["language_code"], - ) - path, filename = os.path.split(relpath) +def save_redirect(output_dir, redirect_file, redirect_content): + path, filename = os.path.split(redirect_file) padding = " " * (len(os.path.dirname(path)) + 8) LOG.debug(f"{padding}*{filename}") - output_filename = os.path.join(output_dir, relpath) - save_bytes_to_file(content, output_filename) + output_filename = os.path.join(output_dir, redirect_file) + save_bytes_to_file(redirect_content, output_filename) def parse_legal_code_filename(filename): @@ -143,17 +143,17 @@ def parse_legal_code_filename(filename): if parts: language_code = map_legacy_to_django_language_code(parts.pop(0)) if jurisdiction: - language_code = language_code or get_default_language_for_jurisdiction( - jurisdiction, "" + language_code = ( + language_code + or get_default_language_for_jurisdiction_naive(jurisdiction) ) else: language_code = language_code or settings.LANGUAGE_CODE - if not language_code: - raise ValueError(f"What language? filename={filename}") + + # Valid Django language_codes are extended in settings with the + # defaults in: + # https://github.com/django/django/blob/main/django/conf/global_settings.py if language_code not in settings.LANG_INFO: - # Valid Django language_codes are extended in settings with the - # defaults in: - # https://github.com/django/django/blob/main/django/conf/global_settings.py raise ValueError(f"{filename}: Invalid language_code={language_code}") base_url = compute_base_url(category, unit, version, jurisdiction) @@ -288,6 +288,80 @@ def clean_string(s): return s +def get_tool_title(unit, version, category, jurisdiction, language_code): + """ + Determine tool title: + 1. If English, use English + 2. Attempt to pull translated title from DB + 3. Translate title using Deeds & UX translation domain + """ + prefix = f"{unit}-{version}-{jurisdiction}-{language_code}-" + tool_title = cache.get(f"{prefix}title", "") + if tool_title: + return tool_title + + # English is easy given it is the default + tool_title_en = get_tool_title_en(unit, version, category, jurisdiction) + if language_code == "en": + tool_title = tool_title_en # already applied clean_string() + cache.add(f"{prefix}title", tool_title) + return tool_title + + # Use the legal code title, if it exists + try: + legal_code = legal_tools.models.LegalCode.objects.get( + tool__category=category, + tool__version=version, + tool__unit=unit, + tool__jurisdiction_code=jurisdiction, + language_code=language_code, + ) + except legal_tools.models.LegalCode.DoesNotExist: + legal_code = False + if legal_code: + tool_title_db = clean_string(legal_code.title) + if tool_title_db and tool_title_db != tool_title_en: + tool_title = tool_title_db + cache.add(f"{prefix}title", tool_title) + return tool_title + + # Translate title using Deeds & UX translation domain + with translation.override(language_code): + tool_name = UNIT_NAMES.get(unit, "UNIMPLEMENTED") + jurisdiction_name = get_jurisdiction_name( + category, unit, version, jurisdiction + ) + tool_title = clean_string(f"{tool_name} {version} {jurisdiction_name}") + + cache.add(f"{prefix}title", tool_title) + return tool_title + + +def get_tool_title_en(unit, version, category, jurisdiction): + prefix = f"{unit}-{version}-{jurisdiction}-en-" + tool_title_en = cache.get(f"{prefix}title", "") + if tool_title_en: + return tool_title_en + + # Retrieve title parts untranslated (English) + with translation.override(None): + tool_name = str(UNIT_NAMES.get(unit, "UNIMPLEMENTED")) + jurisdiction_name = str( + get_jurisdiction_name(category, unit, version, jurisdiction) + ) + # Licenses before 4.0 use "NoDerivs" instead of "NoDerivatives" + if version not in ("1.0", "2.0", "2.1", "2.5", "3.0"): + tool_name = tool_name.replace("NoDerivs", "NoDerivatives") + tool_title_en = f"{tool_name} {version} {jurisdiction_name}" + tool_title_en = tool_title_en.replace( + " Intergovernmental Organization", " IGO" + ) + tool_title_en = clean_string(tool_title_en) + + cache.add(f"{prefix}title", tool_title_en) + return tool_title_en + + def update_is_replaced_by(): """ Update the is_replaced_by property of all licenses by doing simple unit @@ -383,3 +457,108 @@ def update_source(): LOG.info(f"Remove {tool.resource_name} source: '{tool.source}'") tool.source = None tool.save() + + +def update_title(options): + """ + Update the title property of all legal tools by normalizing legacy titles + and normalizing translated titles for current legal tools (Licenses 4.0 and + CC0 1.0). + """ + bold = escape_codes["bold"] + green = escape_codes["green"] + red = escape_codes["red"] + reset = escape_codes["reset"] + pad = " " * 14 + + results = {"records_updated": 0, "records_requiring_update": 0} + if options["dryrun"]: + message = "requires update (dryrun)" + else: + message = "changed" + + LOG.info("Updating legal code object titles in database") + legal_code_objects = legal_tools.models.LegalCode.objects.all() + for legal_code in legal_code_objects: + tool = legal_code.tool + category = tool.category + version = tool.version + unit = tool.unit + jurisdiction = tool.jurisdiction_code + language_code = legal_code.language_code + language_name = translation.get_language_info(language_code)["name"] + full_identifier = f"{bold}{tool.identifier()} {language_name}{reset}" + old_title = legal_code.title + new_title = None + + # English is easy given it is the default + tool_title_en = get_tool_title_en( + unit, version, category, jurisdiction + ) + if language_code == "en": + new_title = tool_title_en # already applied clean_string() + else: + if ( + category == "licenses" + and version in ("1.0", "2.0", "2.1", "2.5", "3.0") + ) and unit != "zero": + # Query database for title extracted from legacy HTML and clean + # it + new_title_db = clean_string(old_title) + if new_title_db and new_title_db != tool_title_en: + new_title = new_title_db + else: + # Translate title using legal code translation domain for legal + # code that is in Transifex (ex. CC0, Licenses 4.0) + slug = f"{unit}_{version}".replace(".", "") + language_default = get_default_language_for_jurisdiction_naive( + jurisdiction + ) + current_translation = get_translation_object( + slug, language_code, language_default + ) + tool_title_lc = "" + with active_translation(current_translation): + tool_title_lc = clean_string( + translation.gettext(tool_title_en) + ) + # Only use legal code translation domain version if translation + # was successful (does not match English). There are deed + # translations in languages for which we do not yet have legal + # code translations. + if tool_title_lc != tool_title_en: + new_title = tool_title_lc + if not new_title: + # Translate title using Deeds & UX translation domain + with translation.override(language_code): + tool_name = UNIT_NAMES.get(unit, "UNIMPLEMENTED") + jurisdiction_name = get_jurisdiction_name( + category, unit, version, jurisdiction + ) + new_title = clean_string( + f"{tool_name} {version} {jurisdiction_name}" + ) + + if old_title == new_title: + LOG.debug(f'{full_identifier} title unchanged: "{old_title}"') + else: + if options["dryrun"]: + results["records_requiring_update"] += 1 + else: + legal_code.title = new_title + legal_code.save() + results["records_updated"] += 1 + LOG.info( + f"{full_identifier} title {message}:" + f'\n{pad}{red}- "{reset}{old_title}{red}"{reset}' + f'\n{pad}{green}+ "{reset}{new_title}{green}"{reset}' + ) + + if options["dryrun"]: + count = results["records_requiring_update"] + LOG.info(f"legal code object titles requiring an update: {count}") + else: + count = results["records_updated"] + LOG.info(f"legal code object titles updated: {count}") + + return results diff --git a/legal_tools/views.py b/legal_tools/views.py index 0257e8ca..0c9e8a03 100644 --- a/legal_tools/views.py +++ b/legal_tools/views.py @@ -5,23 +5,22 @@ from typing import Iterable # Third-party -import git import yaml from bs4 import BeautifulSoup from bs4.dammit import EntitySubstitution from bs4.formatter import HTMLFormatter from django.conf import settings -from django.core.cache import caches +from django.core.cache import cache from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string from django.utils import translation # First-party/Local -from i18n import UNIT_NAMES from i18n.utils import ( active_translation, - get_default_language_for_jurisdiction, + get_default_language_for_jurisdiction_deed, + get_default_language_for_jurisdiction_naive, get_jurisdiction_name, load_deeds_ux_translations, map_django_to_transifex_language_code, @@ -37,6 +36,7 @@ generate_legal_code_rdf, order_rdf_xml, ) +from legal_tools.utils import get_tool_title NUM_COMMITS = 3 PLAIN_TEXT_TOOL_IDENTIFIERS = [ @@ -84,15 +84,6 @@ def get_category_and_category_title(category=None, tool=None): return category, category_title -def get_tool_title(tool): - tool_name = UNIT_NAMES.get(tool.unit, "UNIMPLEMENTED") - jurisdiction_name = get_jurisdiction_name( - tool.category, tool.unit, tool.version, tool.jurisdiction_code - ) - tool_title = f"{tool_name} {tool.version} {jurisdiction_name}" - return tool_title - - def get_languages_and_links_for_deeds_ux(request_path, selected_language_code): languages_and_links = [] @@ -201,34 +192,52 @@ def get_legal_code_replaced_rel_path( try: # Same language legal_code = LegalCode.objects.valid().get( - tool=tool, - language_code=language_code, + tool=tool, language_code=language_code ) except LegalCode.DoesNotExist: try: # Jurisdiction default language legal_code = LegalCode.objects.valid().get( - tool=tool, - language_code=language_default, + tool=tool, language_code=language_default ) except LegalCode.DoesNotExist: # Global default language legal_code = LegalCode.objects.valid().get( - tool=tool, - language_code=settings.LANGUAGE_CODE, + tool=tool, language_code=settings.LANGUAGE_CODE ) - identifier = legal_code.tool.identifier() - lazy_deed = translation.gettext_lazy("Deed") - title = get_tool_title(legal_code.tool) - replaced_deed_title = f"{identifier} {lazy_deed} | {title}" + title = get_tool_title( + tool.unit, + tool.version, + tool.category, + tool.jurisdiction_code, + legal_code.language_code, + ) + prefix = ( + f"{tool.unit}-{tool.version}-" + f"{tool.jurisdiction_code}-{legal_code.language_code}-" + ) + replaced_deed_title = cache.get(f"{prefix}replaced_deed_title", "") + if not replaced_deed_title: + with translation.override(legal_code.language_code): + deed_str = translation.gettext("Deed") + replaced_deed_title = f"{deed_str} - {title}" + cache.add(f"{prefix}replaced_deed_title", replaced_deed_title) replaced_deed_path = get_deed_rel_path( legal_code.deed_url, path_start, language_code, language_default, ) - lazy_legal_code = translation.gettext_lazy("Legal Code") - replaced_legal_code_title = f"{identifier} {lazy_legal_code} | {title}" + replaced_legal_code_title = cache.get( + f"{prefix}replaced_legal_code_title", "" + ) + if not replaced_legal_code_title: + with translation.override(legal_code.language_code): + legal_code_str = translation.gettext("Legal Code") + replaced_legal_code_title = f"{legal_code_str} - {title}" + cache.add( + f"{prefix}replaced_legal_code_title", replaced_legal_code_title + ) replaced_legal_code_path = os.path.relpath( legal_code.legal_code_url, path_start ) @@ -248,9 +257,14 @@ def name_local(legal_code): def normalize_path_and_lang(request_path, jurisdiction, language_code): if not language_code: - language_code = get_default_language_for_jurisdiction( - jurisdiction, settings.LANGUAGE_CODE - ) + if "legalcode" in request_path: + language_code = get_default_language_for_jurisdiction_naive( + jurisdiction + ) + else: + language_code = get_default_language_for_jurisdiction_deed( + jurisdiction + ) if not request_path.endswith(f".{language_code}"): request_path = f"{request_path}.{language_code}" return request_path, language_code @@ -365,7 +379,9 @@ def view_list(request, category, language_code=None): ) if language_code not in settings.LANGUAGES_MOSTLY_TRANSLATED: raise Http404(f"invalid language: {language_code}") + translation.activate(language_code) + list_licenses, list_publicdomain = get_list_paths(language_code, None) # Get the list of units and languages that occur among the tools # to let the template iterate over them as it likes. @@ -387,7 +403,7 @@ def view_list(request, category, language_code=None): lc_unit = lc.tool.unit lc_version = lc.tool.version lc_identifier = lc.tool.identifier() - lc_language_default = get_default_language_for_jurisdiction( + lc_language_default = get_default_language_for_jurisdiction_naive( lc.tool.jurisdiction_code, ) lc_lang_code = lc.language_code @@ -454,7 +470,7 @@ def view_list(request, category, language_code=None): "category": category, "category_title": category_title, "category_list": category_list, - "language_default": get_default_language_for_jurisdiction(None), + "language_default": settings.LANGUAGE_CODE, "languages_and_links": languages_and_links, "list_licenses": list_licenses, "list_publicdomain": list_publicdomain, @@ -485,38 +501,53 @@ def view_deed( return view_page_not_found( request, Http404(f"invalid language: {language_code}") ) - translation.activate(language_code) path_start = os.path.dirname(request.path) - language_default = get_default_language_for_jurisdiction(jurisdiction) - - list_licenses, list_publicdomain = get_list_paths( - language_code, language_default - ) + language_default = get_default_language_for_jurisdiction_deed(jurisdiction) try: tool = Tool.objects.get( unit=unit, version=version, jurisdiction_code=jurisdiction ) except Tool.DoesNotExist as e: + translation.activate(language_code) return view_page_not_found(request, e) - tool_title = get_tool_title(tool) try: # Try to load legal code with specified language legal_code = tool.get_legal_code_for_language_code(language_code) except LegalCode.DoesNotExist: - # Else load legal code with default language - legal_code = tool.get_legal_code_for_language_code(language_default) + try: + # Next, try to load legal code with default language for the + # jurisdiction + legal_code = tool.get_legal_code_for_language_code( + get_default_language_for_jurisdiction_naive(jurisdiction) + ) + except LegalCode.DoesNotExist: + # Last, load legal code with global default language (English) + legal_code = tool.get_legal_code_for_language_code( + settings.LANGUAGE_CODE + ) + + tool_title = get_tool_title( + unit, version, category, jurisdiction, language_code + ) legal_code_rel_path = os.path.relpath( legal_code.legal_code_url, path_start ) + translation.activate(language_code) + + list_licenses, list_publicdomain = get_list_paths( + language_code, language_default + ) + category, category_title = get_category_and_category_title( category, tool, ) + languages_and_links = get_languages_and_links_for_deeds_ux( request_path=request.path, selected_language_code=language_code, @@ -588,7 +619,9 @@ def view_legal_code( request.path, language_code = normalize_path_and_lang( request.path, jurisdiction, language_code ) - language_default = get_default_language_for_jurisdiction(jurisdiction) + language_default = get_default_language_for_jurisdiction_naive( + jurisdiction + ) list_licenses, list_publicdomain = get_list_paths( language_code, language_default @@ -621,7 +654,19 @@ def view_legal_code( else: translation.activate(settings.LANGUAGE_CODE) tool = legal_code.tool - tool_title = get_tool_title(tool) + + # get_tool_title manipulates the translation domain and, therefore, MUST + # be called before we Activate Legal Code translation + tool_title = get_tool_title( + unit, version, category, jurisdiction, language_code + ) + # get_legal_code_replaced_rel_path calls get_tool_title, see note above + _, _, replaced_title, replaced_path = get_legal_code_replaced_rel_path( + tool.is_replaced_by, + path_start, + language_code, + language_default, + ) # Activate Legal Code translation current_translation = legal_code.get_translation_object() @@ -644,13 +689,6 @@ def view_legal_code( language_default, ) - _, _, replaced_title, replaced_path = get_legal_code_replaced_rel_path( - tool.is_replaced_by, - path_start, - language_code, - language_default, - ) - if tool.identifier() in PLAIN_TEXT_TOOL_IDENTIFIERS: plain_text_url = "legalcode.txt" @@ -758,32 +796,34 @@ def branch_status_helper(repo, translation_branch): } -# using cache_page seems to break django-distill (weird error about invalid -# host "testserver"). Do our caching more directly. -# @cache_page(timeout=5 * 60, cache="branchstatuscache") -def view_branch_status(request, id): - translation_branch = get_object_or_404(TranslationBranch, id=id) - cache = caches["branchstatuscache"] - cachekey = ( - f"{settings.DATA_REPOSITORY_DIR}-{translation_branch.branch_name}" - ) - html_response = cache.get(cachekey) - if html_response is None: - with git.Repo(settings.DATA_REPOSITORY_DIR) as repo: - context = branch_status_helper(repo, translation_branch) - html_response = render( - request, - "dev/branch_status.html", - context, - ) - html_response.content = bytes( - BeautifulSoup( - html_response.content, features="lxml" - ).prettify(), - "utf-8", - ) - cache.set(cachekey, html_response, 5 * 60) - return html_response +# TODO: evalute when branch status is re-implemented +# # using cache_page seems to break django-distill (weird error about invalid +# # host "testserver"). Do our caching more directly. +# # @cache_page(timeout=5 * 60, cache="branchstatuscache") +def view_branch_status(request, id): # pragma: no cover + # translation_branch = get_object_or_404(TranslationBranch, id=id) + # cache = caches["branchstatuscache"] + # cachekey = ( + # f"{settings.DATA_REPOSITORY_DIR}-{translation_branch.branch_name}" + # ) + # html_response = cache.get(cachekey) + # if html_response is None: + # with git.Repo(settings.DATA_REPOSITORY_DIR) as repo: + # context = branch_status_helper(repo, translation_branch) + # html_response = render( + # request, + # "dev/branch_status.html", + # context, + # ) + # html_response.content = bytes( + # BeautifulSoup( + # html_response.content, features="lxml" + # ).prettify(), + # "utf-8", + # ) + # cache.set(cachekey, html_response, 5 * 60) + # return html_response + pass def view_metadata(request): diff --git a/manage.py b/manage.py index b8e6650b..8f10f98c 100755 --- a/manage.py +++ b/manage.py @@ -1,25 +1,15 @@ #!/usr/bin/env python # Standard library import logging -import os import sys +# Third-party +from django.core.management import execute_from_command_line + LOG = logging.getLogger("management.commands") def main(): - if "DATABASE_URL" in os.environ: - os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "cc_legal_tools.settings.deploy" - ) - else: - os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "cc_legal_tools.settings.local" - ) - - # Third-party - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index 68bec516..858bfe82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ force-exclude = ''' ) ''' line-length = 79 -target-version = ['py310', 'py311'] +target-version = ['py311', 'py312'] [tool.coverage.run] diff --git a/templates/base.html b/templates/base.html index 26ab6737..029d457e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,7 @@ - {% block title %}{% endblock %} | Creative Commons + {% block title %}{% endblock %} - Creative Commons {% for option in languages_and_links %} {% if option.cc_language_code == language_default %} diff --git a/templates/deed.html b/templates/deed.html index 7aeb76c9..a6539876 100644 --- a/templates/deed.html +++ b/templates/deed.html @@ -6,7 +6,7 @@ {% block body_class%}walkthrough-page{% endblock %} {% block title %} -{{ tool.identifier }} {% trans "Deed" %} | {{ tool_title }} +{% trans "Deed" %} - {{ tool_title }} {% endblock %} @@ -41,7 +41,7 @@
diff --git a/templates/dev/404.html b/templates/dev/404.html index 268a13fc..8bfca91f 100644 --- a/templates/dev/404.html +++ b/templates/dev/404.html @@ -19,10 +19,4 @@

Not found :(

{% endblock %} -{% block extra-js %} - - -{% endblock %} {# vim: ft=jinja.html ts=2 sw=2 sts=2 sr et #} diff --git a/templates/dev/index.html b/templates/dev/index.html index 2fb7f9f2..b2097a70 100644 --- a/templates/dev/index.html +++ b/templates/dev/index.html @@ -170,9 +170,15 @@

ccREL

  • + {% if not distilling %} {% static 'rdf/schema.rdf' %} + {% else %} + + /rdf/schema.rdf + + {% endif %}
  • diff --git a/templates/includes/footer.html b/templates/includes/footer.html index 2b23b1e8..c0e3e4ce 100644 --- a/templates/includes/footer.html +++ b/templates/includes/footer.html @@ -21,7 +21,7 @@

    Contact Us