diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86406f5..9e3b7aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,25 +4,45 @@ on: [push, pull_request] jobs: test: + name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest + env: + PYTHONPATH: ${{ github.workspace }} + + strategy: + matrix: + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install coverage codecov pytest - pip install -r packages/requirements-dev.txt - - name: Run tests - run: | - coverage run -m pytest - - name: Generate coverage report - run: coverage xml - - name: Upload coverage to Codecov - run: codecov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install coverage codecov pytest poetry + pip install -r packages/requirements-dev.txt + + - name: Run tests with coverage + run: pytest --cov=iranian_cities --cov-report=xml + + - name: Run Tox tests + run: tox + + - name: Run pre-commit hooks + run: pre-commit run --all-files --config=.pre-commit-config-ci.yaml + + - name: Upload coverage to Codecov + run: codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index e18e8c9..f85536e 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,7 @@ manage.py # Ignore all migrations files except __init__.py **/migrations/* -!**/migrations/__init__.py \ No newline at end of file +!**/migrations/__init__.py + +bar_codes/ +qr_codes/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8842c2..e6d0c88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,10 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.3.0 hooks: + - id: check-toml + - id: check-yaml + files: \.yaml$ - id: trailing-whitespace exclude: (migrations/|tests/|docs/|static/|media/).* - id: end-of-file-fixer @@ -15,46 +18,88 @@ repos: - id: check-docstring-first exclude: (migrations/|tests/|docs/|static/|media/).* - - repo: https://github.com/pre-commit/mirrors-isort - rev: v5.10.1 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 2.2.1 hooks: - - id: isort - exclude: (migrations/|tests/|docs/|static/|media/).* + - id: pyproject-fmt - - repo: https://github.com/psf/black - rev: 23.3.0 + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 1.3.1 hooks: - - id: black - args: ["--config=pyproject.toml"] - exclude: (migrations/|tests/|docs/|static/|media/).* + - id: tox-ini-fmt + + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.5.5 hooks: - id: ruff args: ["--config=pyproject.toml"] + exclude: (migrations/|tests/|docs/|static/|media/|apps.py).* + + - repo: https://github.com/pre-commit/mirrors-isort + rev: v5.10.1 + hooks: + - id: isort exclude: (migrations/|tests/|docs/|static/|media/).* - - repo: https://github.com/PyCQA/docformatter - rev: v1.7.5 + - repo: https://github.com/psf/black + rev: 23.3.0 hooks: - - id: docformatter - args: ["--in-place", "--recursive", "--blank"] + - id: black + args: ["--config=pyproject.toml"] + exclude: (migrations/|tests/|docs/|static/|media/).* - repo: https://github.com/commitizen-tools/commitizen rev: v3.28.0 hooks: - id: commitizen - exclude: (migrations/|tests/|docs/|static/|media/).* - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: - id: bandit args: ["-c", "pyproject.toml", "-r", "."] - additional_dependencies: [ "bandit[toml]" ] + additional_dependencies: ["bandit[toml]"] exclude: (migrations/|tests/|docs/|static/|media/).* + - repo: https://github.com/adamchainz/blacken-docs + rev: 1.18.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==24.4.2 + files: '\.rst$' + + - repo: https://github.com/rstcheck/rstcheck + rev: "v6.2.4" + hooks: + - id: rstcheck + args: ["--report-level=warning"] + files: ^docs/(.*/)*.*\.rst$ + additional_dependencies: ["Sphinx==6.2.1"] + + - repo: https://github.com/regebro/pyroma + rev: "4.2" + hooks: + - id: pyroma + always_run: false + files: | + (?x)^( + README.md| + pyproject.toml| + )$ + + - repo: https://github.com/DanielNoord/pydocstringformatter + rev: v0.7.3 + hooks: + - id: pydocstringformatter + args: ["--max-summary-lines=2", "--linewrap-full-docstring"] + files: "sage_qrcode" + - repo: local hooks: - id: pytest @@ -67,7 +112,7 @@ repos: always_run: true - id: pylint - name: pylint + name: Pylint entry: pylint language: system types: [python] @@ -76,7 +121,11 @@ repos: - "-rn" - "-sn" - "--rcfile=pyproject.toml" + - "--fail-under=9.0" files: ^sage_qrcode/ ci: - skip: [pylint] + skip: [ + pylint, + pytest + ] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 10a0c7d..6098f34 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -10,53 +10,69 @@ Thank you for your interest in contributing to our package! This document outlin cd django-sage-qrcode ``` -2. **Create a Branch**: Create a new branch for your feature or bugfix. + +2. **Initialize Git Flow**: Set up Git Flow to manage your branches efficiently. ```bash - git checkout -b feature/your-feature-name + git flow init ``` + Follow the prompts to configure Git Flow. The default options usually suffice. + +3. **Create a Branch**: Create a new branch for your feature or bugfix using Git Flow. + + - **Creating a Feature Branch**: + ```bash + git flow feature start your-feature-name + ``` + This will create and check out a new branch from the `develop` branch. + + - **Creating a Bugfix Branch**: + ```bash + git flow bugfix start your-bugfix-name + ``` + This will create and check out a new branch specifically for the bugfix. -3. **Install Dependencies**: Use Poetry to install dependencies. +4. **Install Dependencies**: Use Poetry to install dependencies. ```bash poetry install ``` -4. **Write Code and Tests**: Make your changes and write tests for your new code. +5. **Write Code and Tests**: Make your changes and write tests for your new code. -5. **Run Code Quality Checks**: Ensure code quality with pre-commit, Ruff, and Pylint. +6. **Run Code Quality Checks**: Ensure code quality with pre-commit, Ruff, and Pylint. ```bash - pre-commit run --all-files - ruff check django_sage_qrcode --fix - black django_sage_qrcode/ - isort django_sage_qrcode/ - pylint django_sage_qrcode + poetry run pre-commit run --all-files + poetry run ruff check sage_qrcode/ --fix + poetry run black sage_qrcode/ + poetry run isort sage_qrcode/ + poetry run pylint sage_qrcode/ ``` -6. **Run Tests**: Ensure all tests pass using Poetry. +7. **Run Tests**: Ensure all tests pass using Poetry. ```bash poetry run pytest ``` -7. **Commit Changes**: Use Commitizen to commit your changes. +8. **Commit Changes**: Use Commitizen to commit your changes. ```bash cz commit ``` -8. **Push and Create a PR**: Push your changes and create a pull request. +9. **Push and Create a PR**: Push your changes and create a pull request. ```bash git push origin feature/your-feature-name ``` -9. **Bump Version**: Use Commitizen to bump the version. +10. **Bump Version**: Use Commitizen to bump the version. ```bash cz bump ``` -10. **Generate Changelog**: Use Commitizen to generate the changelog. +11. **Generate Changelog**: Use Commitizen to generate the changelog. ```bash cz changelog ``` -11. **Export Dependencies**: Export dependencies for development and production. +12. **Export Dependencies**: Export dependencies for development and production. ```bash poetry export -f requirements.txt --output packages/requirements.txt --without-hashes poetry export -f requirements.txt --dev --output packages/requirements-dev.txt --without-hashes @@ -87,7 +103,7 @@ feat(core): initialize the core module ### 2. Release with build and tag version ``` -chore(release): build and tag version 1.0.0 +build(release): build and tag version 1.0.0 - Built the project for production - Created a new tag for version 1.0.0 @@ -123,7 +139,7 @@ docs(sphinx): update API documentation ### 6. Update dependencies (packages) ``` -chore(deps): update project dependencies +build(deps): update project dependencies - Updated all outdated npm packages - Resolved compatibility issues with new package versions @@ -132,7 +148,7 @@ chore(deps): update project dependencies ### 7. Update version for build and publish ``` -chore(version): update version to 2.1.0 for build and publish +build(version): update version to 2.1.0 for build and publish - Incremented version number to 2.1.0 - Updated package.json with the new version diff --git a/README.md b/README.md index b35ede0..312c789 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ # django-sage-qrcode + +## Installation + +### Using `pip` with `virtualenv` + +1. **Create a Virtual Environment**: + + ```bash + python -m venv .venv + ``` + +2. **Activate the Virtual Environment**: + + - On Windows: + + ```bash + .venv\Scripts\activate + ``` + + - On macOS/Linux: + + ```bash + source .venv/bin/activate + ``` + +3. **Install `django-sage-qrcode`**: + + ```bash + pip install django-sage-qrcode + ``` + +### Using `poetry` + +1. **Initialize Poetry** (if not already initialized): + + ```bash + poetry init + ``` + +2. **Install Dependencies**: + + ```bash + poetry install + ``` + +3. **Install `django-sage-qrcode`**: + + ```bash + poetry add django-sage-qrcode + ``` + +4. **Apply Migrations**: + + After installation, make sure to run the following commands to create the necessary database tables: + + ```bash + poetry run python manage.py makemigrations + poetry run python manage.py migrate + ``` + +## Django Settings Configuration + +### Installed Apps + +To use `django-sage-qrcode`, add it to your `INSTALLED_APPS` in the Django settings: + +```python +INSTALLED_APPS = [ + ... + "sage_qrcode", + "sage_tools", + "colorfield", + "polymorphic", + ... +] +``` + +## Setup for Testing + +Before running tests, ensure you set the `DJANGO_SETTINGS_MODULE` environment variable. This can be done in your terminal by running: + +```bash +export DJANGO_SETTINGS_MODULE=your_project_name.settings +``` + +Alternatively, you can configure this in the `tox.ini` file under `[testenv]` like this: + +```ini +[testenv] +passenv = DJANGO_SETTINGS_MODULE +``` + +This will ensure the correct settings module is used during testing. diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst index 92e0211..af08994 100644 --- a/docs/source/getting_started/installation.rst +++ b/docs/source/getting_started/installation.rst @@ -67,9 +67,9 @@ To use `django-sage-qrcode`, add it to your `INSTALLED_APPS` in the Django setti .. code-block:: python INSTALLED_APPS = [ - ... + # other packages "sage_qrcode", + "sage_tools", "colorfield", "polymorphic", - ... ] diff --git a/docs/source/getting_started/models.rst b/docs/source/getting_started/models.rst index ee1dd0a..4dec69d 100644 --- a/docs/source/getting_started/models.rst +++ b/docs/source/getting_started/models.rst @@ -70,27 +70,38 @@ To integrate these models into the Django admin interface, register them in the .. code-block:: python from django.contrib import admin - from django_sage_qrcode.models import TelegramQRCode, TikTokQRCode, WhatsAppQRCode, XQRCode, WifiQRCode + from django_sage_qrcode.models import ( + TelegramQRCode, + TikTokQRCode, + WhatsAppQRCode, + XQRCode, + WifiQRCode, + ) + @admin.register(TelegramQRCode) class TelegramQRCodeAdmin(admin.ModelAdmin): - list_display = ['profile_url'] + list_display = ["profile_url"] + @admin.register(TikTokQRCode) class TikTokQRCodeAdmin(admin.ModelAdmin): - list_display = ['profile_url'] + list_display = ["profile_url"] + @admin.register(WhatsAppQRCode) class WhatsAppQRCodeAdmin(admin.ModelAdmin): - list_display = ['phone_number', 'message'] + list_display = ["phone_number", "message"] + @admin.register(XQRCode) class XQRCodeAdmin(admin.ModelAdmin): - list_display = ['profile_url'] + list_display = ["profile_url"] + @admin.register(WifiQRCode) class WifiQRCodeAdmin(admin.ModelAdmin): - list_display = ['ssid', 'encryption'] + list_display = ["ssid", "encryption"] This will allow you to manage the different QR code models directly from the Django admin interface. diff --git a/docs/source/getting_started/service.rst b/docs/source/getting_started/service.rst index 83d0a0d..4f49eeb 100644 --- a/docs/source/getting_started/service.rst +++ b/docs/source/getting_started/service.rst @@ -66,7 +66,9 @@ Example Usage contact_qr = ContactQRCode() # Generate a WiFi QR code - contact_qr.generate_wifi_qr_code(ssid='MyWiFi', password='mypassword', security_type='WPA',save=True) + contact_qr.generate_wifi_qr_code( + ssid="MyWiFi", password="mypassword", security_type="WPA", save=True + ) PaymentQRCode Class @@ -119,20 +121,20 @@ Example Usage # Generate an EPC payment QR code payment_qr.generate_epc_qr_code( - name='John Doe', - iban='DE89370400440532013000', + name="John Doe", + iban="DE89370400440532013000", amount=100.50, - text='Payment for services', - save=True + text="Payment for services", + save=True, ) # Generate a Bitcoin payment QR code payment_qr.generate_bitcoin_qr_code( - address='1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", amount=0.005, - label='Donation', - message='Thanks for your support!', - save=True + label="Donation", + message="Thanks for your support!", + save=True, ) @@ -171,7 +173,7 @@ Example Usage barcode = BarcodeProxy() # Generate a barcode for a given data string - barcode.generate_barcode(data='123456789012') + barcode.generate_barcode(data="123456789012") # Save the barcode barcode.save_barcode() @@ -219,7 +221,4 @@ Example Usage social_qr = SocialMediaQRCode() # Generate a QR code for a social media URL with an icon - social_qr.create_social_media_url( - url='https://instagram.com/example', - save=True - ) + social_qr.create_social_media_url(url="https://instagram.com/example", save=True) diff --git a/poetry.lock b/poetry.lock index c92b675..dafc11d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -88,40 +88,9 @@ files = [ {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "bandit" version = "1.7.9" @@ -206,15 +175,94 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -377,7 +425,6 @@ argcomplete = ">=1.12.1,<3.6" charset-normalizer = ">=2.1.0,<4" colorama = ">=0.4.1,<0.5.0" decli = ">=0.6.0,<0.7.0" -importlib_metadata = {version = ">=8.0.0,<9", markers = "python_version < \"3.10\""} jinja2 = ">=2.10.3" packaging = ">=19" pyyaml = ">=3.08" @@ -472,6 +519,55 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "decli" version = "0.6.2" @@ -511,18 +607,17 @@ files = [ [[package]] name = "django" -version = "4.2.15" +version = "4.2.16" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.15-py3-none-any.whl", hash = "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30"}, - {file = "Django-4.2.15.tar.gz", hash = "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a"}, + {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, + {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, ] [package.dependencies] asgiref = ">=3.6.0,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -593,19 +688,21 @@ files = [ Django = ">=2.1" [[package]] -name = "djangorestframework" -version = "3.15.2" -description = "Web APIs for Django, made easy." +name = "django-sage-tools" +version = "0.3.5" +description = "Reusable, generic mixins for Django" optional = false -python-versions = ">=3.8" +python-versions = "<4.0,>=3.8" files = [ - {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, - {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, + {file = "django_sage_tools-0.3.5-py3-none-any.whl", hash = "sha256:e511bcc8f682402d6710e0755dedc622c00bc78d0537d81f3997203ab65fde68"}, + {file = "django_sage_tools-0.3.5.tar.gz", hash = "sha256:c825f80ca2bc10b2c61ad5ab3176d32cc66d324886eaaf8fdb2362d1bd0379f8"}, ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -django = ">=4.2" +cryptography = ">=43.0.0,<44.0.0" +django = {version = ">=4.2,<5.3", markers = "python_version >= \"3.10\""} +mimesis = ">=11.1.0,<12.0.0" +pillow = ">=10.4.0,<11.0.0" [[package]] name = "docutils" @@ -684,25 +781,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "8.4.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -873,6 +951,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mimesis" +version = "11.1.0" +description = "Mimesis: Fake Data Generator." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "mimesis-11.1.0-py3-none-any.whl", hash = "sha256:574715564c937cd40eba23a0d184febbe04e538d5d120bfa5b951775200f3084"}, + {file = "mimesis-11.1.0.tar.gz", hash = "sha256:5f3839751190f6eef7f453dfafb8f2f38dbdcda11bb3ad742589c216c24985f1"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1130,6 +1219,17 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydes" version = "2.0.1" @@ -1156,13 +1256,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.6" +version = "3.2.7" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, ] [package.dependencies] @@ -1178,7 +1278,6 @@ mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -1320,17 +1419,6 @@ files = [ [package.extras] images = ["pillow"] -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -1460,7 +1548,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -1503,9 +1590,6 @@ files = [ {file = "segno-1.6.1.tar.gz", hash = "sha256:f23da78b059251c36e210d0cf5bfb1a9ec1604ae6e9f3d42f9a7c16d306d847e"}, ] -[package.dependencies] -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} - [[package]] name = "sengo" version = "0.0.2" @@ -1520,6 +1604,26 @@ files = [ [package.dependencies] arichuvadi = ">=0.0.4" +[[package]] +name = "setuptools" +version = "74.1.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] + [[package]] name = "six" version = "1.16.0" @@ -1559,7 +1663,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.20" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.13" @@ -1872,29 +1975,10 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] -[[package]] -name = "zipp" -version = "3.20.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] docs = [] [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "5e15204c3430fee5fe47317470fdcf6ae7b5debb54018e1436528280f9769a69" +python-versions = ">=3.10,<4.0" +content-hash = "1f61584714bcbb4e7f5575589ab8628f1c7397406dc6ac99e1f63fe167abb422" diff --git a/pyproject.toml b/pyproject.toml index 73b27d8..4975433 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,47 @@ +[build-system] +build-backend = "poetry.core.masonry.api" +requires = [ "poetry-core" ] + [tool.poetry] name = "django_sage_qrcode" -version = "0.1.0" +version = "0.1.1" description = "A project for QR code generation and NFC integration" -authors = ["Radin Ghahremani ","Sepehr Akbarzadeh "] +authors = [ "Radin Ghahremani ", "Sepehr Akbarzadeh " ] readme = "README.md" +keywords = [ "django", "QR code", "NFC", "barcode", "django application" ] packages = [ - { include = "django_sage_qrcode" } + { include = "django_sage_qrcode" }, +] + +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Framework :: Django", ] +[tool.poetry.urls] +"Documentation" = "https://django-sage-qrcode.readthedocs.io/en/latest/" +"Source Code" = "https://github.com/sageteamorg/django-sage-qrcode" +"Issues" = "https://github.com/sageteamorg/django-sage-qrcode/issues" + [tool.poetry.dependencies] -python = "^3.8" -django = "^4.2" +python = ">=3.10,<4.0" +django = [ + { version = ">=4.2,<5.0", python = "3.10" }, + { version = ">=4.2,<5.3", python = ">=3.10" }, +] sengo = "^0.0.2" qrcode-artistic = "^3.0.2" django-polymorphic = "^3.1.0" django-colorfield = "^0.11.0" -djangorestframework = "^3.15.2" nfcpy = "^1.0.4" ndeflib = "^0.3.3" ndef = "^0.2" python-barcode = "^0.15.1" pyshorteners = "^1.0.1" +django-sage-tools = "^0.3.5" [tool.poetry.group.dev.dependencies] black = "^24.4.2" @@ -37,81 +57,13 @@ pytest = "^8.3.2" pytest-cov = "^5.0.0" ruff = "^0.5.7" commitizen = "^3.29.0" -bandit = {extras = ["toml"], version = "^1.7.9"} +bandit = { extras = [ "toml" ], version = "^1.7.9" } django-debug-toolbar = "^4.4.6" django-migration-linter = "^5.1.0" +setuptools = "^74.1.2" [tool.poetry.extras] -docs = ["sphinx", "sphinx-rtd-theme"] - -[tool.pytest.ini_options] -addopts = "--cov --cov-report=term-missing --cov-report=html --cov-fail-under=90" -DJANGO_SETTINGS_MODULE = "kernel.settings" -python_files = ["tests.py", "test_*.py"] -testpaths = ["tests"] -norecursedirs = [ - "migrations", - "static", - "media", - "node_modules", - "env", - "venv", - ".venv", - "dist", - "build", - "kernel" -] - -[tool.bandit] -targets = ["./sage_qrcode"] -exclude_dirs = [ - "tests", - "migrations", -] -severity = "medium" -confidence = "medium" -max_lines = 500 -progress = true -reports = true -output_format = "screen" -output_file = "bandit_report.txt" -include = ["B101", "B102"] -exclude_tests = ["B301", "B302"] - -[tool.bandit.plugins] -B104 = { check_typed_list = true } - -[tool.coverage.run] -omit = [ - "*/migrations/*", - "kernel/*", - "*/apps.py", - "manage.py", - "*/admin/*", -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "if self\\.debug", - "raise AssertionError", - "if 0:", - "if __name__ == .__main__.:" -] -[tool.ruff] -line-length = 88 -exclude = [ - "venv/*", - ".venv/*", - "build/*", - "dist/*", - ".git/*", - "__pycache__/*", - "*.egg-info/*", - ".mypy_cache/*", - ".pytest_cache/*", - "migrations/*" -] +docs = [ "sphinx", "sphinx-rtd-theme" ] [tool.black] line-length = 88 @@ -136,79 +88,189 @@ exclude = ''' )/ ''' +[tool.ruff] +line-length = 88 +exclude = [ + "*.egg-info/*", + ".git/*", + ".mypy_cache/*", + ".pytest_cache/*", + ".venv/*", + "__pycache__/*", + "build/*", + "dist/*", + "migrations/*", + "venv/*", +] + +lint.select = [ + "C90", # Select custom or extended error code C90 + "E", # Select all PEP8 error codes + "F", # Select all potential runtime errors + "W", # Select all warnings +] +lint.ignore = [ + "E203", # Ignore whitespace before ':', ';', or '#' + "E501", # Ignore line length issues (lines longer than 88 characters) + "F401", +] + [tool.isort] profile = "black" line_length = 88 skip = [ - "venv", - ".venv", - "build", - "dist", - ".git", - "__pycache__", - "*.egg-info", - ".mypy_cache", - ".pytest_cache", - "migrations", - "static", - "media", - "node_modules", - "env" + "venv", + ".venv", + "build", + "dist", + ".git", + "__pycache__", + "*.egg-info", + ".mypy_cache", + ".pytest_cache", + "migrations", + "static", + "media", + "node_modules", + "env", + "kernel", ] -[tool.commitizen] -name = "cz_conventional_commits" -version = "0.1.3" - -[tool.commitizen.settings] -increment_types = ["feat", "fix"] - [tool.pylint] disable = [ - "C0114", - "C0115", - "C0116", - "E1101", - "W0212", - "C0330", + "C0103", # Invalid constant name + "C0114", # Missing module docstring + "C0115", # Missing class docstring + "C0116", # Missing function or method docstring + "E1101", # Instance of 'Foo' has no 'bar' member + "W0212", # Access to a protected member + "C0301", # Line too long + "C0411", # Wrong import order + "W0611", # Unused imports + "W0613", # Unused arguments + "W0622", # Redefining built-in names + "R0903", # Too few public methods + "R0801", # Duplicate code + "W0621", + "C0415", + "R1719", # The if expression can be replaced with 'bool(test)' + "R1705", # Unnecessary "elif" after "return" + "R0401", + "W0223", ] max-line-length = 88 ignore = [ - "migrations/*", - "venv/*", - "build/*", - "dist/*", - ".git/*", - "__pycache__/*", - "*.egg-info/*", - ".mypy_cache/*", - ".pytest_cache/*" + "migrations/*", + "venv/*", + "build/*", + "dist/*", + ".git/*", + "__pycache__/*", + "*.egg-info/*", + ".mypy_cache/*", + ".pytest_cache/*", ] +django-settings-module = "kernel.settings" load-plugins = [ - "pylint_django", - "pylint.extensions.docparams", -] + "pylint_django", + "pylint.extensions.docparams", -django-settings-module = "kernel.settings" +] good-names = [ - "qs", - "pk", - "id", + "qs", # QuerySet abbreviation + "pk", # Primary key abbreviation + "id", # Identifier ] suggestion-mode = true const-rgx = "([A-Z_][A-Z0-9_]*)|(__.*__)" attr-rgx = "[a-z_][a-z0-9_]{2,30}$" variable-rgx = "[a-z_][a-z0-9_]{2,30}$" argument-rgx = "[a-z_][a-z0-9_]{2,30}$" -argument-name-hint = [ - "cls", - "self", -] method-rgx = "[a-z_][a-z0-9_]{2,30}$" function-rgx = "[a-z_][a-z0-9_]{2,30}$" class-rgx = "[A-Z_][a-zA-Z0-9]+$" module-rgx = "(([a-z_][a-z0-9_]*)|(__.*__))$" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.pytest.ini_options] +addopts = "--cov --cov-report=term-missing --cov-report=html" +DJANGO_SETTINGS_MODULE = "kernel.settings" +python_files = [ "tests.py", "test_*.py" ] +testpaths = [ "tests" ] +markers = [ + "admin: mark test to check admin tests", + "models_str_method: mark test to check __str__ methods of models", + "models_code_field: mark test to check code fields in models", + "models_code_field_unique: mark test to check code fields are unique", + "settings_admin_permission: mark test to check admin permissions", + "settings_checks: mark test to check settings configuration", +] +norecursedirs = [ + "migrations", + "static", + "media", + "node_modules", + "env", + "venv", + ".venv", + "dist", + "build", + "kernel", +] + +[tool.coverage.run] +omit = [ + "*/migrations/*", + "kernel/*", + "*/apps.py", + "manage.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "if 0:", + "if __name__ == .__main__.:", +] + +[tool.mypy] +mypy_path = "stubs" +disallow_untyped_calls = true +disallow_untyped_defs = true +ignore_missing_imports = true +explicit_package_bases = true +exclude = ''' +^docs/source/conf.py| +^build/| +^tests/| +^stubs/| +^kernel/ +''' + +[tool.bandit] +targets = [ "./sage_shop" ] +exclude_dirs = [ + "tests", + "migrations", +] +severity = "medium" +confidence = "medium" +max_lines = 500 +progress = true +reports = true +output_format = "screen" +output_file = "bandit_report.txt" +include = [ "B101", "B102" ] +exclude_tests = [ "B301", "B302" ] + +[tool.bandit.plugins] +B104 = { check_typed_list = true } + +[tool.commitizen] +name = "cz_conventional_commits" +version = "0.1.0" + +[tool.commitizen.settings] +increment_types = [ "feat", "fix" ] diff --git a/sage_qrcode/admin/__init__.py b/sage_qrcode/admin/__init__.py index ff28816..8c32f33 100644 --- a/sage_qrcode/admin/__init__.py +++ b/sage_qrcode/admin/__init__.py @@ -1,18 +1,18 @@ -from .barcode import BarcodeParentAdmin,BarcodeUrlAdmin,BarcodeTextAdmin +from .barcode import BarcodeParentAdmin, BarcodeTextAdmin, BarcodeUrlAdmin from .base import QRCodeParentAdmin from .contact import VCardQRCodeAdmin, WiFiQRCodeAdmin +from .payment import BitcoinQRCodeAdmin, EPCQRCodeAdmin from .social_media import ( + FacebookQRCodeAdmin, + InstagramQRCodeAdmin, + LinkedInQRCodeAdmin, MediaUrlAdmin, SkypeQRCodeAdmin, - TikTokQRCodeAdmin, SnapchatQRCodeAdmin, - XQRCodeAdmin, - LinkedInQRCodeAdmin, - FacebookQRCodeAdmin, TelegramQRCodeAdmin, - InstagramQRCodeAdmin, + TikTokQRCodeAdmin, + XQRCodeAdmin, ) -from .payment import EPCQRCodeAdmin, BitcoinQRCodeAdmin __all__ = [ "BarcodeParentAdmin", diff --git a/sage_qrcode/admin/actions/__init__.py b/sage_qrcode/admin/actions/__init__.py index 1356d43..bc2593d 100644 --- a/sage_qrcode/admin/actions/__init__.py +++ b/sage_qrcode/admin/actions/__init__.py @@ -1,3 +1,3 @@ -from .download import download_qr_code, download_barcode_action +from .download import download_barcode_action, download_qr_code __all__ = ["download_qr_code", "download_barcode_action"] diff --git a/sage_qrcode/admin/actions/download.py b/sage_qrcode/admin/actions/download.py index 789af32..fd76e20 100644 --- a/sage_qrcode/admin/actions/download.py +++ b/sage_qrcode/admin/actions/download.py @@ -1,13 +1,39 @@ -# actions.py +"""This module provides custom admin actions for downloading QR codes and +barcodes directly from the Django admin interface. + +Admin actions are a powerful feature in Django, enabling custom functionality +for selected objects in the list view. In this module, two actions are defined +and registered with the Django admin: + +1. `download_qr_code_action`: + - Allows administrators to download the QR code associated with the selected + object(s) from the admin interface. + - The action checks if a single QR code is selected; if not, it prompts the + user to select exactly one item to proceed with the download. + - This ensures that users don't inadvertently attempt to download multiple + QR codes at once, which could lead to confusion or errors. + +2. `download_barcode_action`: + - Similar to the QR code action, this allows administrators to download the + barcode associated with the selected object(s) from the admin interface. + - It also checks that exactly one barcode is selected, guiding the user + accordingly. + +These actions enhance the usability of the admin interface by allowing quick +and direct downloads of generated QR codes and barcodes, which can be useful +for verification, sharing, or printing purposes. The use of the Django `HttpResponse` +object and custom utility functions (`download_qr_code` and `download_barcode`) +facilitates the handling of these downloads, ensuring a smooth user experience. +""" from django.contrib import admin from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ -from sage_qrcode.utils.admin import download_qr_code, download_barcode +from sage_qrcode.utils.admin import download_barcode, download_qr_code -@admin.action(description=_("Download selected qrcode for user")) +@admin.action(description=_("Download selected QRcode for user")) def download_qr_code_action(modeladmin, request, queryset): response = download_qr_code(request, queryset) if isinstance(response, HttpResponse): diff --git a/sage_qrcode/admin/barcode.py b/sage_qrcode/admin/barcode.py index 7d8a378..94902b5 100644 --- a/sage_qrcode/admin/barcode.py +++ b/sage_qrcode/admin/barcode.py @@ -1,27 +1,45 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ -from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin +from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin -from sage_qrcode.models import Barcode, BarcodeUrl, BarcodeText -from sage_qrcode.utils.admin import generate_barcode_image, save_barcode_image from sage_qrcode.admin.actions import download_barcode_action +from sage_qrcode.models import Barcode, BarcodeText, BarcodeUrl +from sage_qrcode.utils.admin import generate_barcode_image, save_barcode_image @admin.register(Barcode) class BarcodeParentAdmin(PolymorphicParentModelAdmin): base_model = Barcode child_models = (BarcodeUrl, BarcodeText) - list_display = ("title","get_barcode_type", "color", "second_color", "created", "modified") - list_filter = ("created", "modified", "color", "second_color") + list_display = ( + "title", + "get_barcode_type", + "color", + "second_color", + "created_at", + "modified_at", + ) + list_filter = ("created_at", "modified_at", "color", "second_color") search_fields = ("title",) actions = [download_barcode_action] fieldsets = ( - (None, {"fields": ("title", "color", "second_color")}), + ( + None, + { + "fields": ("title", "color", "second_color"), + "description": _( + "Provide the title and choose the primary and secondary colors for the barcode." + ), + }, + ), ( _("Image"), { "fields": ("bar_code_image",), + "description": _( + "This field will display the generated barcode image after saving." + ), }, ), ) @@ -41,15 +59,26 @@ class BarcodeUrlAdmin(PolymorphicChildModelAdmin, BarcodeParentAdmin): base_model = BarcodeUrl show_in_index = True list_display = ("title",) - list_filter = ("created", "modified", "url", "color", "second_color") + list_filter = ("created_at", "modified_at", "url", "color", "second_color") search_fields = ("title", "url") fieldsets = ( - (None, {"fields": ("title", "url", "color", "second_color")}), + ( + None, + { + "fields": ("title", "url", "color", "second_color"), + "description": _( + "Enter the title, URL, and select the primary and secondary colors for the barcode." + ), + }, + ), ( _("Image"), { "fields": ("bar_code_image",), + "description": _( + "This field will display the generated barcode image after saving." + ), }, ), ) @@ -71,18 +100,29 @@ class BarcodeTextAdmin(PolymorphicChildModelAdmin, BarcodeParentAdmin): "body", "color", "second_color", - "created", - "modified", + "created_at", + "modified_at", ) - list_filter = ("created", "modified", "color", "second_color") + list_filter = ("created_at", "modified_at", "color", "second_color") search_fields = ("title", "body") fieldsets = ( - (None, {"fields": ("title", "body", "color", "second_color")}), + ( + None, + { + "fields": ("title", "body", "color", "second_color"), + "description": _( + "Enter the title, text content, and select the primary and secondary colors for the barcode." + ), + }, + ), ( _("Image"), { "fields": ("bar_code_image",), + "description": _( + "This field will display the generated barcode image after saving." + ), }, ), ) diff --git a/sage_qrcode/admin/base.py b/sage_qrcode/admin/base.py index d8d879b..8bd4979 100644 --- a/sage_qrcode/admin/base.py +++ b/sage_qrcode/admin/base.py @@ -1,30 +1,31 @@ from django.contrib import admin -from polymorphic.admin import PolymorphicParentModelAdmin from django.utils.translation import gettext_lazy as _ +from polymorphic.admin import PolymorphicParentModelAdmin +from sage_qrcode.helpers.filters import QRCodeTypeFilter from sage_qrcode.models import ( - MediaUrl, - TikTokQRCode, + BitcoinQRCode, + EPCQRCode, FacebookQRCode, - WifiQRCode, InstagramQRCode, LinkedInQRCode, - VCardQRCode, + MediaUrl, + QRCode, SkypeQRCode, - BitcoinQRCode, - TelegramQRCode, SnapchatQRCode, + TelegramQRCode, + TikTokQRCode, + VCardQRCode, + WifiQRCode, XQRCode, - QRCode, - EPCQRCode, ) -from sage_qrcode.helpers.filters import QRCodeTypeFilter from sage_qrcode.utils.admin import ( + download_qr_code, generate_qr_code, save_qr_code_image, - download_qr_code, ) + @admin.register(QRCode) class QRCodeParentAdmin(PolymorphicParentModelAdmin): base_model = QRCode @@ -45,9 +46,9 @@ class QRCodeParentAdmin(PolymorphicParentModelAdmin): ) actions = [download_qr_code] - list_display = ("id","get_qr_code_type" ,"created", "modified") - list_filter = ("created", "modified", QRCodeTypeFilter) - search_fields = ("id", "created", "modified") + list_display = ("id", "get_qr_code_type", "created_at", "modified_at") + list_filter = ("created_at", "modified_at", QRCodeTypeFilter) + search_fields = ("id", "created_at", "modified_at") @admin.display(description=_("QR Code Type")) def get_qr_code_type(self, obj): diff --git a/sage_qrcode/admin/contact.py b/sage_qrcode/admin/contact.py index 54f0830..7f9bbb1 100644 --- a/sage_qrcode/admin/contact.py +++ b/sage_qrcode/admin/contact.py @@ -1,10 +1,10 @@ -from polymorphic.admin import PolymorphicChildModelAdmin from django.contrib import admin from django.utils.translation import gettext_lazy as _ +from polymorphic.admin import PolymorphicChildModelAdmin -from sage_qrcode.models import VCardQRCode, WifiQRCode -from sage_qrcode.forms import VCardQRCodeForm, WiFiQRCodeForm from sage_qrcode.admin.base import QRCodeParentAdmin +from sage_qrcode.forms import VCardQRCodeForm, WiFiQRCodeForm +from sage_qrcode.models import VCardQRCode, WifiQRCode @admin.register(VCardQRCode) @@ -26,7 +26,10 @@ class VCardQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): "phone", "url", "custom_gif", - ) + ), + "description": _( + "Provide the basic contact details and an optional custom GIF for the VCard QR code." + ), }, ), ( @@ -34,6 +37,9 @@ class VCardQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): { "classes": ("collapse",), "fields": ("org", "size", "color", "second_color", "third_color"), + "description": _( + "Configure additional details like organization, size, and color options for the QR code." + ), }, ), ) @@ -51,12 +57,23 @@ class WiFiQRCodeAdmin(QRCodeParentAdmin): search_fields = ("ssid", "security") fieldsets = ( - (None, {_("fields"): ("ssid", "password", "security", "custom_gif")}), + ( + None, + { + "fields": ("ssid", "password", "security", "custom_gif"), + "description": _( + "Enter the Wi-Fi network details and optionally add a custom GIF for the QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Adjust the QR code's size and color settings for customization." + ), }, ), ) diff --git a/sage_qrcode/admin/payment.py b/sage_qrcode/admin/payment.py index 8a2f54d..7d2baee 100644 --- a/sage_qrcode/admin/payment.py +++ b/sage_qrcode/admin/payment.py @@ -1,28 +1,38 @@ -from polymorphic.admin import PolymorphicChildModelAdmin -from django.utils.translation import gettext_lazy as _ from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from polymorphic.admin import PolymorphicChildModelAdmin -from sage_qrcode.models import BitcoinQRCode, EPCQRCode -from sage_qrcode.forms import BitForm, EPCQRCodeForm from sage_qrcode.admin.base import QRCodeParentAdmin +from sage_qrcode.forms import BitForm, EPCQRCodeForm +from sage_qrcode.models import BitcoinQRCode, EPCQRCode @admin.register(EPCQRCode) class EPCQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): base_model = EPCQRCode form = EPCQRCodeForm - # show_in_index = False list_display = ("name", "iban") list_filter = ("name", "amount") search_fields = ("name", "iban") fieldsets = ( - (None, {"fields": ("name", "iban", "text", "amount", "custom_gif")}), + ( + None, + { + "fields": ("name", "iban", "text", "amount", "custom_gif"), + "description": _( + "Enter the payee's name, IBAN, optional text message, and amount for the EPC QR code. Optionally, you can also add a custom GIF." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Adjust the QR code's size and colors to customize its appearance." + ), }, ), ) @@ -40,7 +50,15 @@ class BitcoinQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("bitcoin_address", "label", "message") fieldsets = ( - (None, {"fields": ("bitcoin_address", "amount", "label", "message")}), + ( + None, + { + "fields": ("bitcoin_address", "amount", "label", "message"), + "description": _( + "Enter the Bitcoin address, amount, and optional label and message for the Bitcoin QR code." + ), + }, + ), ( _("Advanced options"), { @@ -51,6 +69,9 @@ class BitcoinQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): "second_color", "third_color", ), + "description": _( + "Customize the QR code by adjusting its size and color settings." + ), }, ), ) diff --git a/sage_qrcode/admin/social_media.py b/sage_qrcode/admin/social_media.py index 6d7094a..fb92f21 100644 --- a/sage_qrcode/admin/social_media.py +++ b/sage_qrcode/admin/social_media.py @@ -2,19 +2,19 @@ from django.utils.translation import gettext_lazy as _ from polymorphic.admin import PolymorphicChildModelAdmin +from sage_qrcode.admin.base import QRCodeParentAdmin +from sage_qrcode.forms import MediaUrlForm, TikTokForm, XForm from sage_qrcode.models import ( - MediaUrl, - TikTokQRCode, FacebookQRCode, InstagramQRCode, LinkedInQRCode, + MediaUrl, SkypeQRCode, SnapchatQRCode, - XQRCode, TelegramQRCode, + TikTokQRCode, + XQRCode, ) -from sage_qrcode.forms import TikTokForm, MediaUrlForm, XForm -from sage_qrcode.admin.base import QRCodeParentAdmin @admin.register(SkypeQRCode) @@ -27,16 +27,22 @@ class SkypeQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the Skype URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -56,16 +62,22 @@ class TikTokQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the TikTok profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -84,16 +96,22 @@ class SnapchatQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the Snapchat profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -112,16 +130,22 @@ class XQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the X profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -140,16 +164,22 @@ class LinkedInQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the LinkedIn profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -167,16 +197,22 @@ class FacebookQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the Facebook profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -195,16 +231,22 @@ class TelegramQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the Telegram profile or group URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -223,16 +265,22 @@ class InstagramQRCodeAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the Instagram profile URL to generate the corresponding QR code." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), - "fields": ( - "size", - "color", - "second_color", - "third_color", + "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." ), }, ), @@ -252,12 +300,23 @@ class MediaUrlAdmin(PolymorphicChildModelAdmin, QRCodeParentAdmin): search_fields = ("url",) fieldsets = ( - (None, {"fields": ("url",)}), + ( + None, + { + "fields": ("url",), + "description": _( + "Enter the media URL to generate the corresponding QR code. Optionally, add a custom GIF." + ), + }, + ), ( _("Advanced options"), { "classes": ("collapse",), "fields": ("size", "color", "second_color", "third_color"), + "description": _( + "Customize the QR code by adjusting its size and colors." + ), }, ), ) diff --git a/sage_qrcode/apps.py b/sage_qrcode/apps.py index bd242d3..0b61331 100644 --- a/sage_qrcode/apps.py +++ b/sage_qrcode/apps.py @@ -1,9 +1,11 @@ from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ class QrcodeConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "sage_qrcode" + verbose_name = _("SAGE QRcode") def ready(self) -> None: - import sage_qrcode.check \ No newline at end of file + import sage_qrcode.check diff --git a/sage_qrcode/check.py b/sage_qrcode/check.py index f749d27..fff2bcc 100644 --- a/sage_qrcode/check.py +++ b/sage_qrcode/check.py @@ -1,14 +1,17 @@ +from importlib.util import find_spec + from django.conf import settings from django.core.checks import Error, register -from importlib.util import find_spec + @register() def check_installed_apps(app_configs, **kwargs): errors = [] required_apps = [ - 'polymorphic', - 'sage_qrcode', - 'colorfield', + "polymorphic", + "sage_qrcode", + "sage_tools", + "colorfield", ] for app in required_apps: @@ -24,13 +27,15 @@ def check_installed_apps(app_configs, **kwargs): return errors + @register() def check_required_libraries(app_configs, **kwargs): errors = [] required_libraries = [ - 'polymorphic', - 'sage_qrcode', - 'colorfield', + "polymorphic", + "sage_qrcode", + "sage_tools", + "colorfield", ] for library in required_libraries: @@ -44,4 +49,4 @@ def check_required_libraries(app_configs, **kwargs): ) ) - return errors \ No newline at end of file + return errors diff --git a/sage_qrcode/forms/__init__.py b/sage_qrcode/forms/__init__.py index 59d023a..81f7672 100644 --- a/sage_qrcode/forms/__init__.py +++ b/sage_qrcode/forms/__init__.py @@ -1,10 +1,10 @@ from .forms import ( - WiFiQRCodeForm, - VCardQRCodeForm, - TikTokForm, - MediaUrlForm, - EPCQRCodeForm, BitForm, + EPCQRCodeForm, + MediaUrlForm, + TikTokForm, + VCardQRCodeForm, + WiFiQRCodeForm, XForm, ) diff --git a/sage_qrcode/forms/forms.py b/sage_qrcode/forms/forms.py index 6e944fb..d89f44e 100644 --- a/sage_qrcode/forms/forms.py +++ b/sage_qrcode/forms/forms.py @@ -1,17 +1,18 @@ +import os +import uuid + from django import forms + from sage_qrcode.models import ( - VCardQRCode, - WifiQRCode, - MediaUrl, + BitcoinQRCode, EPCQRCode, + MediaUrl, TikTokQRCode, - BitcoinQRCode, + VCardQRCode, + WifiQRCode, XQRCode, ) -import uuid -import os - class VCardQRCodeForm(forms.ModelForm): class Meta: @@ -20,7 +21,8 @@ class Meta: def save(self, commit=True): """Custom save method to handle the storage of a custom GIF file - associated with a VCard QR Code.""" + associated with a VCard QR Code. + """ instance = super().save(commit=False) custom_gif = self.cleaned_data.get("custom_gif", None) if custom_gif: @@ -46,7 +48,8 @@ class Meta: def save(self, commit=True): """Custom save method to handle the storage of a custom GIF file - associated with a WiFi QR Code.""" + associated with a WiFi QR Code. + """ instance = super().save(commit=False) custom_gif = self.cleaned_data.get("custom_gif", None) @@ -88,7 +91,8 @@ class Meta: def save(self, commit=True): """Custom save method to handle the storage of a custom GIF file - associated with a Media URL QR Code.""" + associated with a Media URL QR Code. + """ instance = super().save(commit=False) custom_gif = self.cleaned_data.get("custom_gif", None) if custom_gif: @@ -113,7 +117,8 @@ class Meta: def save(self, commit=True): """Custom save method to handle the storage of a custom GIF file - associated with an EPC QR Code.""" + associated with an EPC QR Code. + """ instance = super().save(commit=False) custom_gif = self.cleaned_data.get("custom_gif", None) if custom_gif: @@ -146,7 +151,8 @@ class Meta: def save(self, commit=True): """Custom save method to handle the storage of a custom GIF file - associated with a Bitcoin QR Code.""" + associated with a Bitcoin QR Code. + """ instance = super().save(commit=False) custom_gif = self.cleaned_data.get("custom_gif", None) if custom_gif: diff --git a/sage_qrcode/helpers/filters/qrcodeadmin.py b/sage_qrcode/helpers/filters/qrcodeadmin.py index 7b479b9..18a814d 100644 --- a/sage_qrcode/helpers/filters/qrcodeadmin.py +++ b/sage_qrcode/helpers/filters/qrcodeadmin.py @@ -1,12 +1,7 @@ from django.contrib.admin import SimpleListFilter from django.utils.translation import gettext_lazy as _ -from sage_qrcode.models import ( - VCardQRCode, - WifiQRCode, - MediaUrl, - EPCQRCode, -) +from sage_qrcode.models import EPCQRCode, MediaUrl, VCardQRCode, WifiQRCode class QRCodeTypeFilter(SimpleListFilter): @@ -14,7 +9,6 @@ class QRCodeTypeFilter(SimpleListFilter): This filter allows the admin interface to display QR codes based on their type, such as VCard, WiFi, Social Media, Media URL, or EPC. - """ title = _("QR Code Type") @@ -31,7 +25,6 @@ def lookups(self, request, model_admin): Returns: list: A list of tuples containing filter options. - """ return ( ("vcard", _("VCard QR Code")), @@ -50,7 +43,6 @@ def queryset(self, request, queryset): Returns: QuerySet: The filtered queryset based on the selected QR code type. - """ if self.value() == "vcard": return queryset.instance_of(VCardQRCode) diff --git a/sage_qrcode/helpers/validators/__init__.py b/sage_qrcode/helpers/validators/__init__.py index 9559297..b7115e7 100644 --- a/sage_qrcode/helpers/validators/__init__.py +++ b/sage_qrcode/helpers/validators/__init__.py @@ -1,5 +1,22 @@ -from .phone_number import ValidatorE164 +from .bitcoin import BitcoinAddressValidator, validate_bitcoin_address +from .event import EventTimeRangeValidator, validate_event_time_range +from .geo_location import ( + LatitudeValidator, + LongitudeValidator, + validate_latitude, + validate_longitude, +) +from .iban import IBANValidator, validate_iban +from .image import ImageFileValidator, SizeValidator, validate_image_file, validate_size +from .phone_number import ValidatorE164, validate_phone_number from .socials import ( + FacebookValidator, + InstagramValidator, + LinkedInValidator, + SkypeValidator, + SnapchatValidator, + TelegramValidator, + TikTokValidator, validate_facebook, validate_instagram, validate_linkedin, @@ -8,25 +25,7 @@ validate_telegram, validate_tiktok, validate_x, - SkypeValidator, - TikTokValidator, - FacebookValidator, - LinkedInValidator, - SnapchatValidator, - TelegramValidator, - InstagramValidator, ) -from .geo_location import ( - LatitudeValidator, - LongitudeValidator, - validate_latitude, - validate_longitude, -) -from .image import ImageFileValidator, SizeValidator, validate_image_file, validate_size -from .iban import IBANValidator, validate_iban -from .event import EventTimeRangeValidator, validate_event_time_range -from .bitcoin import BitcoinAddressValidator, validate_bitcoin_address -from .phone_number import validate_phone_number __all__ = [ "ValidatorE164", diff --git a/sage_qrcode/helpers/validators/bitcoin.py b/sage_qrcode/helpers/validators/bitcoin.py index bdd0ef6..447e64c 100644 --- a/sage_qrcode/helpers/validators/bitcoin.py +++ b/sage_qrcode/helpers/validators/bitcoin.py @@ -1,4 +1,5 @@ import re + from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ @@ -16,7 +17,6 @@ class BitcoinAddressValidator: message (str): Error message returned when the validation fails. code (str): Error code returned when the validation fails. regex (Pattern): Compiled regular expression for validating Bitcoin addresses. - """ message = _("Enter a valid Bitcoin address.") @@ -37,7 +37,6 @@ def __call__(self, value): Raises: ValidationError: If the Bitcoin address is not valid. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) diff --git a/sage_qrcode/helpers/validators/event.py b/sage_qrcode/helpers/validators/event.py index 963c370..bdffb5c 100644 --- a/sage_qrcode/helpers/validators/event.py +++ b/sage_qrcode/helpers/validators/event.py @@ -10,7 +10,6 @@ class EventTimeRangeValidator: This validator checks that the end time of an event is after the start time, ensuring that events have a valid time range. - """ message = _("End time must be after start time.") @@ -31,7 +30,6 @@ def __call__(self, start_time, end_time): Raises: ValidationError: If the end time is not after the start time. - """ if end_time <= start_time: raise ValidationError( diff --git a/sage_qrcode/helpers/validators/geo_location.py b/sage_qrcode/helpers/validators/geo_location.py index 8346117..0a8369c 100644 --- a/sage_qrcode/helpers/validators/geo_location.py +++ b/sage_qrcode/helpers/validators/geo_location.py @@ -13,7 +13,6 @@ class LatitudeValidator: Attributes: message (str): Error message returned when the validation fails. code (str): Error code returned when the validation fails. - """ message = _("Enter a valid latitude between -90 and 90.") @@ -27,7 +26,6 @@ def __call__(self, value): Raises: ValidationError: If the latitude is not between -90 and 90. - """ if value < -90 or value > 90: raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -43,7 +41,6 @@ class LongitudeValidator: Attributes: message (str): Error message returned when the validation fails. code (str): Error code returned when the validation fails. - """ message = _("Enter a valid longitude between -180 and 180.") @@ -57,7 +54,6 @@ def __call__(self, value): Raises: ValidationError: If the longitude is not between -180 and 180. - """ if value < -180 or value > 180: raise ValidationError(self.message, code=self.code, params={"value": value}) diff --git a/sage_qrcode/helpers/validators/iban.py b/sage_qrcode/helpers/validators/iban.py index 44de246..08d16b4 100644 --- a/sage_qrcode/helpers/validators/iban.py +++ b/sage_qrcode/helpers/validators/iban.py @@ -1,4 +1,5 @@ import re + from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ @@ -15,7 +16,6 @@ class IBANValidator: message (str): Error message returned when the validation fails. code (str): Error code returned when the validation fails. regex (Pattern): Compiled regular expression for validating IBANs. - """ message = _("Enter a valid IBAN.") diff --git a/sage_qrcode/helpers/validators/image.py b/sage_qrcode/helpers/validators/image.py index d13eb2c..8529925 100644 --- a/sage_qrcode/helpers/validators/image.py +++ b/sage_qrcode/helpers/validators/image.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError +from django.core.files.images import get_image_dimensions from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ -from django.core.files.images import get_image_dimensions @deconstructible @@ -15,7 +15,6 @@ class ImageFileValidator: message (str): The error message to be returned if validation fails. code (str): The error code to be used if validation fails. max_size (int, optional): The maximum file size allowed in bytes. - """ message = _("Upload a valid image file.") @@ -30,7 +29,6 @@ def __init__(self, message=None, code=None, max_size=None): message (str, optional): Custom error message. code (str, optional): Custom error code. max_size (int, optional): Maximum allowed file size in bytes. - """ if message is not None: self.message = message @@ -48,7 +46,6 @@ def __call__(self, value): Raises: ValidationError: If the file is not a valid image or exceeds the maximum allowed size. - """ # Check if the file is an image try: @@ -72,7 +69,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message, code, and max_size, False otherwise. - """ return ( isinstance(other, ImageFileValidator) @@ -93,7 +89,6 @@ class SizeValidator: code (str): The error code to be used if validation fails. min_value (int): The minimum allowed value for the size. max_value (int): The maximum allowed value for the size. - """ message = _("Size must be between 1 and 1000.") @@ -110,7 +105,6 @@ def __init__(self, message=None, code=None, min_value=None, max_value=None): code (str, optional): Custom error code. min_value (int, optional): Minimum allowed value. max_value (int, optional): Maximum allowed value. - """ if message is not None: self.message = message @@ -129,7 +123,6 @@ def __call__(self, value): Raises: ValidationError: If the value is outside the specified range. - """ if value is not None and (value < self.min_value or value > self.max_value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -142,7 +135,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message, code, min_value, and max_value, False otherwise. - """ return ( isinstance(other, SizeValidator) diff --git a/sage_qrcode/helpers/validators/phone_number.py b/sage_qrcode/helpers/validators/phone_number.py index 4a46fba..093b9b3 100644 --- a/sage_qrcode/helpers/validators/phone_number.py +++ b/sage_qrcode/helpers/validators/phone_number.py @@ -1,4 +1,5 @@ import re + from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ @@ -15,7 +16,6 @@ class ValidatorE164: message (str): Error message to be used if validation fails. code (str): Error code to be used if validation fails. regex (Pattern): Compiled regular expression pattern for E.164 validation. - """ message = _("Invalid phone number format. Must be in E.164 format.") @@ -28,7 +28,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -44,7 +43,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the E.164 format. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -57,7 +55,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, ValidatorE164) diff --git a/sage_qrcode/helpers/validators/socials.py b/sage_qrcode/helpers/validators/socials.py index a034b71..135ca2b 100644 --- a/sage_qrcode/helpers/validators/socials.py +++ b/sage_qrcode/helpers/validators/socials.py @@ -1,4 +1,5 @@ import re + from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ @@ -14,7 +15,6 @@ class TikTokValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for TikTok URL validation. - """ message = _("Enter a valid TikTok profile URL.") @@ -30,7 +30,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -46,7 +45,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -59,7 +57,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, TikTokValidator) @@ -78,7 +75,6 @@ class SnapchatValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for Snapchat URL validation. - """ message = _("Enter a valid Snapchat profile URL.") @@ -94,7 +90,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -110,7 +105,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -123,7 +117,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, SnapchatValidator) @@ -142,7 +135,6 @@ class InstagramValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for Instagram URL validation. - """ message = _("Enter a valid Instagram profile URL.") @@ -158,7 +150,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -174,7 +165,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -187,7 +177,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, InstagramValidator) @@ -206,7 +195,6 @@ class FacebookValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for Facebook URL validation. - """ message = _("Enter a valid Facebook profile URL.") @@ -222,7 +210,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -238,7 +225,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -251,7 +237,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, FacebookValidator) @@ -270,7 +255,6 @@ class TelegramValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for Telegram URL validation. - """ message = _("Enter a valid Telegram profile URL.") @@ -284,7 +268,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -300,7 +283,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -313,7 +295,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, TelegramValidator) @@ -332,7 +313,6 @@ class LinkedInValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for LinkedIn URL validation. - """ message = _("Enter a valid LinkedIn profile URL.") @@ -348,7 +328,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -364,7 +343,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -377,7 +355,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, LinkedInValidator) @@ -396,7 +373,6 @@ class XValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for X URL validation. - """ message = _("Enter a valid X profile URL.") @@ -411,7 +387,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -426,7 +401,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -439,7 +413,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, XValidator) @@ -458,7 +431,6 @@ class SkypeValidator: message (str): Error message returned if the validation fails. code (str): Error code returned if the validation fails. regex (Pattern): Compiled regular expression pattern for Skype URL validation. - """ message = _("Enter a valid Skype profile URL.") @@ -474,7 +446,6 @@ def __init__(self, message=None, code=None): Args: message (str, optional): Custom error message. code (str, optional): Custom error code. - """ if message is not None: self.message = message @@ -490,7 +461,6 @@ def __call__(self, value): Raises: ValidationError: If the value does not match the regex pattern. - """ if not self.regex.match(value): raise ValidationError(self.message, code=self.code, params={"value": value}) @@ -503,7 +473,6 @@ def __eq__(self, other): Returns: bool: True if both instances have the same message and code, False otherwise. - """ return ( isinstance(other, SkypeValidator) diff --git a/sage_qrcode/mixins/__init__.py b/sage_qrcode/mixins/__init__.py deleted file mode 100644 index c9efa02..0000000 --- a/sage_qrcode/mixins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .time import TimestampMixin - -__all__ = ["TimestampMixin"] diff --git a/sage_qrcode/mixins/time.py b/sage_qrcode/mixins/time.py deleted file mode 100644 index 1ac1da3..0000000 --- a/sage_qrcode/mixins/time.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class TimestampMixin(models.Model): - """""" - - created = models.DateTimeField( - _("Created"), - auto_now_add=True, - help_text=_("Date and time of record creation"), - ) - - modified = models.DateTimeField( - _("Modified"), - auto_now=True, - help_text=_("Date and time of record modification"), - ) - - class Meta: - abstract = True diff --git a/sage_qrcode/models/__init__.py b/sage_qrcode/models/__init__.py index c3cdba9..dc59f4c 100644 --- a/sage_qrcode/models/__init__.py +++ b/sage_qrcode/models/__init__.py @@ -1,21 +1,21 @@ +from .barcode import Barcode, BarcodeText, BarcodeUrl +from .bitcoin import BitcoinQRCode from .card import VCardQRCode +from .epc import EPCQRCode from .qrcode import QRCode from .social_media import ( - WhatsAppQRCode, - SkypeQRCode, - SnapchatQRCode, - TelegramQRCode, FacebookQRCode, - XQRCode, InstagramQRCode, LinkedInQRCode, MediaUrl, + SkypeQRCode, + SnapchatQRCode, + TelegramQRCode, TikTokQRCode, + WhatsAppQRCode, + XQRCode, ) from .wifi import WifiQRCode -from .epc import EPCQRCode -from .bitcoin import BitcoinQRCode -from .barcode import Barcode, BarcodeUrl, BarcodeText __all__ = [ "VCardQRCode", diff --git a/sage_qrcode/models/barcode.py b/sage_qrcode/models/barcode.py index e2fcf95..c6dc5a9 100644 --- a/sage_qrcode/models/barcode.py +++ b/sage_qrcode/models/barcode.py @@ -1,17 +1,17 @@ +from colorfield.fields import ColorField from django.db import models from django.utils.translation import gettext_lazy as _ - -from colorfield.fields import ColorField from polymorphic.models import PolymorphicModel +from sage_tools.mixins.models import TimeStampMixin -from sage_qrcode.mixins import TimestampMixin from sage_qrcode.helpers.validators import validate_image_file -class Barcode(PolymorphicModel, TimestampMixin): +class Barcode(PolymorphicModel, TimeStampMixin): """Abstract base class for all QR code types.""" bar_code_image = models.ImageField( + verbose_name=_("Bar Code Image"), upload_to="bar_codes/", blank=True, null=True, @@ -21,6 +21,7 @@ class Barcode(PolymorphicModel, TimestampMixin): ) title = models.CharField( + verbose_name=_("Title"), max_length=255, null=True, blank=True, @@ -29,23 +30,28 @@ class Barcode(PolymorphicModel, TimestampMixin): ) color = ColorField( + verbose_name=_("Color"), format="hex", - help_text=_("Color of the BAR code."), + default="#000000", null=True, blank=True, + help_text=_("Color of the BAR code."), db_comment="The color of the BAR code in hexadecimal format.", ) second_color = ColorField( + verbose_name=_("Second Color"), format="hex", - help_text=_("Second color of the QR code."), + default="##FFFFFF", null=True, blank=True, + help_text=_("Second color of the QR code."), db_comment="The second color of the BAR code in hexadecimal format.", ) class Meta: verbose_name = _("Bar Code") verbose_name_plural = _("Bar Codes") + db_table = "sage_qrcode_barcode" def __str__(self): return f"{self.__class__.__name__} {self.pk} - {self.title or 'No Title'}" @@ -60,13 +66,12 @@ class BarcodeUrl(Barcode): A Barcode URL QR code stores the URL of media content such as videos or audio. When scanned, it directs the user to the Barcode url content. - """ url = models.URLField( + verbose_name=_("barcode URL"), help_text=_("URL of the barcode content."), db_comment="The URL of the barcode content.", - verbose_name=_("barcode URL"), ) def __str__(self): @@ -80,6 +85,7 @@ class Meta: ordering = ["url"] verbose_name = _(" URL Barcode") verbose_name_plural = _("URL Barcode") + db_table = "sage_qrcode_barcode_url" class BarcodeText(Barcode): @@ -87,14 +93,13 @@ class BarcodeText(Barcode): A Text bar stores the URL of media content such as videos or audio. When scanned, it directs the user to the media content. - """ body = models.TextField( + verbose_name=_("body"), blank=True, help_text=_("Body of the barcode."), db_comment="The body of the barcode.", - verbose_name=_("body"), ) def __str__(self): @@ -108,3 +113,4 @@ class Meta: ordering = ["body"] verbose_name = _("Barcode Text") verbose_name_plural = _("Barcode Texts") + db_table = "sage_qrcode_barcode_txt" diff --git a/sage_qrcode/models/bitcoin.py b/sage_qrcode/models/bitcoin.py index e76155b..20f53b0 100644 --- a/sage_qrcode/models/bitcoin.py +++ b/sage_qrcode/models/bitcoin.py @@ -1,9 +1,9 @@ -from django.db import models from django.core.validators import MinValueValidator +from django.db import models from django.utils.translation import gettext_lazy as _ -from sage_qrcode.models.qrcode import QRCode from sage_qrcode.helpers.validators import validate_bitcoin_address +from sage_qrcode.models.qrcode import QRCode class BitcoinQRCode(QRCode): @@ -12,17 +12,17 @@ class BitcoinQRCode(QRCode): A Bitcoin Payment QR code stores Bitcoin payment information. When scanned, it opens the Bitcoin wallet app with the encoded payment information. - """ bitcoin_address = models.CharField( + verbose_name=_("Bitcoin Address"), max_length=34, validators=[validate_bitcoin_address], help_text=_("Bitcoin address. Example: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'"), db_comment="The Bitcoin address for the payment.", - verbose_name=_("Bitcoin Address"), ) amount = models.DecimalField( + verbose_name=_("Amount"), max_digits=10, decimal_places=8, null=False, @@ -30,20 +30,19 @@ class BitcoinQRCode(QRCode): validators=[MinValueValidator(0.00000001)], help_text=_("Amount of Bitcoin to send. Example: '0.01'"), db_comment="The amount of Bitcoin to send.", - verbose_name=_("Amount"), ) label = models.CharField( + verbose_name=_("Label"), max_length=255, blank=True, help_text=_("Label for the transaction."), db_comment="An optional label for the transaction.", - verbose_name=_("Label"), ) message = models.TextField( + verbose_name=_("Message"), blank=True, help_text=_("Message for the transaction."), db_comment="An optional message for the transaction.", - verbose_name=_("Message"), ) def __str__(self): @@ -57,3 +56,4 @@ class Meta: ordering = ["bitcoin_address"] verbose_name = _("Bitcoin QR Code") verbose_name_plural = _("Bitcoin QR Codes") + db_table = "sage_qrcode_bitcoin_qr" diff --git a/sage_qrcode/models/card.py b/sage_qrcode/models/card.py index d3f698e..0ad8ddc 100644 --- a/sage_qrcode/models/card.py +++ b/sage_qrcode/models/card.py @@ -1,9 +1,9 @@ -from django.db import models from django.core.exceptions import ValidationError +from django.db import models from django.utils.translation import gettext_lazy as _ -from sage_qrcode.models.qrcode import QRCode from sage_qrcode.helpers.validators import validate_phone_number +from sage_qrcode.models.qrcode import QRCode class VCardQRCode(QRCode): @@ -12,60 +12,59 @@ class VCardQRCode(QRCode): A VCard QR code stores information about an individual, such as their name, email, phone number, and more. When scanned, it allows the user to easily add the contact details to their address book. - """ full_name = models.CharField( + verbose_name=_("Full Name"), max_length=255, help_text=_("Full name of the individual."), db_comment="The name of the individual represented in the VCard QR code.", - verbose_name=_("Full Name"), ) display_name = models.CharField( + verbose_name=_("Display Name"), max_length=255, null=True, blank=True, help_text=_("Display name of the individual."), db_comment="An optional display name for the individual.", - verbose_name=_("Display Name"), ) email = models.EmailField( + verbose_name=_("Email"), null=True, blank=True, help_text=_("Email address of the individual."), db_comment="The email address of the individual.", - verbose_name=_("Email"), ) phone = models.CharField( + verbose_name=_("Phone Number"), max_length=20, null=True, blank=True, validators=[validate_phone_number], help_text=_("Phone number of the individual."), db_comment="The phone number of the individual.", - verbose_name=_("Phone Number"), ) url = models.URLField( + verbose_name=_("Website URL"), null=True, blank=True, help_text=_("URL of the individual's website or profile."), db_comment="The URL to the individual's website or profile.", - verbose_name=_("Website URL"), ) org = models.CharField( + verbose_name=_("Organization"), max_length=255, null=True, blank=True, help_text=_("Name of the organization."), db_comment="The organization name associated with the individual.", - verbose_name=_("Organization"), ) address = models.TextField( + verbose_name=_("Address"), null=True, blank=True, help_text=_("Physical address of the individual."), db_comment="The physical address of the individual.", - verbose_name=_("Address"), ) phone_number = models.CharField( _("Phone Number"), @@ -101,3 +100,4 @@ class Meta: ordering = ["full_name"] verbose_name = _("VCard QR Code") verbose_name_plural = _("VCard QR Codes") + db_table = "sage_qrcode_vcard_qr" diff --git a/sage_qrcode/models/epc.py b/sage_qrcode/models/epc.py index 1fb4b7c..f9d4438 100644 --- a/sage_qrcode/models/epc.py +++ b/sage_qrcode/models/epc.py @@ -1,9 +1,9 @@ +from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from django.core.validators import MinValueValidator -from sage_qrcode.models.qrcode import QRCode from sage_qrcode.helpers.validators import validate_iban +from sage_qrcode.models.qrcode import QRCode class EPCQRCode(QRCode): @@ -12,35 +12,34 @@ class EPCQRCode(QRCode): An EPC QR code stores payment details such as the beneficiary's name, IBAN, and the amount. It allows for easy scanning and processing of payments. - """ name = models.CharField( + verbose_name=_("Beneficiary Name"), max_length=255, help_text=_("Name of the EPC beneficiary."), db_comment="The name of the beneficiary for the EPC QR code.", - verbose_name=_("Beneficiary Name"), ) iban = models.CharField( + verbose_name=_("IBAN"), max_length=34, validators=[validate_iban], help_text=_("IBAN of the EPC beneficiary."), db_comment="The IBAN of the beneficiary for the EPC QR code.", - verbose_name=_("IBAN"), ) amount = models.DecimalField( + verbose_name=_("Payment Amount"), max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)], help_text=_("Payment amount."), db_comment="The amount to be paid using the EPC QR code.", - verbose_name=_("Payment Amount"), ) text = models.TextField( + verbose_name=_("Additional Text"), blank=True, help_text=_("Additional text for the EPC QR code."), db_comment="Optional additional text for the EPC QR code.", - verbose_name=_("Additional Text"), ) def __str__(self): @@ -57,3 +56,4 @@ class Meta: ordering = ["name"] verbose_name = _("EPC QR Code") verbose_name_plural = _("EPC QR Codes") + db_table = "sage_qrcode_epc_qr" diff --git a/sage_qrcode/models/qrcode.py b/sage_qrcode/models/qrcode.py index 352239d..ee1f359 100644 --- a/sage_qrcode/models/qrcode.py +++ b/sage_qrcode/models/qrcode.py @@ -1,17 +1,17 @@ +from colorfield.fields import ColorField from django.db import models from django.utils.translation import gettext_lazy as _ - -from colorfield.fields import ColorField from polymorphic.models import PolymorphicModel +from sage_tools.mixins.models import TimeStampMixin -from sage_qrcode.mixins import TimestampMixin from sage_qrcode.helpers.validators import validate_image_file, validate_size -class QRCode(PolymorphicModel, TimestampMixin): +class QRCode(PolymorphicModel, TimeStampMixin): """Abstract base class for all QR code types.""" qr_code_image = models.ImageField( + verbose_name=_("QR Code Image"), upload_to="qr_codes/", blank=True, null=True, @@ -20,6 +20,7 @@ class QRCode(PolymorphicModel, TimestampMixin): db_comment="The image file of the generated QR code.", ) custom_gif = models.ImageField( + verbose_name=_("Custom GIF"), upload_to="custom_gifs/", blank=True, null=True, @@ -28,6 +29,7 @@ class QRCode(PolymorphicModel, TimestampMixin): db_comment="An optional custom GIF that can be embedded in the QR code.", ) title = models.CharField( + verbose_name=_("Title"), max_length=255, null=True, blank=True, @@ -35,38 +37,43 @@ class QRCode(PolymorphicModel, TimestampMixin): db_comment="A descriptive title for the QR code.", ) size = models.PositiveSmallIntegerField( + verbose_name=_("Size"), validators=[validate_size], - help_text=_("Size of the QR code image."), null=True, blank=True, + help_text=_("Size of the QR code image."), db_comment="The size (dimensions) of the QR code image.", ) color = ColorField( + verbose_name=_("Color"), format="hex", - help_text=_("Color of the QR code."), null=True, blank=True, + help_text=_("Color of the QR code."), db_comment="The color of the QR code in hexadecimal format.", ) second_color = ColorField( + verbose_name=_("Second Color"), format="hex", - help_text=_("Second color of the QR code."), null=True, blank=True, + help_text=_("Second color of the QR code."), db_comment="The second color of the QR code in hexadecimal format.", ) third_color = ColorField( + verbose_name=_("Third Color"), format="hex", - help_text=_("Third color of the QR code."), null=True, blank=True, + help_text=_("Third color of the QR code."), db_comment="The third color of the BAR code in hexadecimal format.", ) class Meta: verbose_name = _("QR Code") verbose_name_plural = _("QR Codes") + db_table = "sage_qrcode_qr_code" def __str__(self): return f"{self.__class__.__name__} {self.pk} - {self.title or 'No Title'}" diff --git a/sage_qrcode/models/social_media.py b/sage_qrcode/models/social_media.py index a428f3d..63056dd 100644 --- a/sage_qrcode/models/social_media.py +++ b/sage_qrcode/models/social_media.py @@ -1,18 +1,18 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from sage_qrcode.models.qrcode import QRCode from sage_qrcode.helpers.validators import ( - validate_tiktok, - validate_snapchat, - validate_instagram, validate_facebook, - validate_telegram, + validate_instagram, validate_linkedin, - validate_x, - validate_skype, validate_phone_number, + validate_skype, + validate_snapchat, + validate_telegram, + validate_tiktok, + validate_x, ) +from sage_qrcode.models.qrcode import QRCode class WhatsAppQRCode(QRCode): @@ -20,21 +20,20 @@ class WhatsAppQRCode(QRCode): A WhatsApp QR code stores a phone number with an optional message. When scanned, it opens WhatsApp with the encoded information. - """ phone_number = models.CharField( + verbose_name=_("WhatsApp Phone Number"), max_length=20, validators=[validate_phone_number], help_text=_("WhatsApp phone number. Example: +1234567890"), db_comment="The WhatsApp phone number.", - verbose_name=_("WhatsApp Phone Number"), ) message = models.TextField( + verbose_name=_("Message"), blank=True, help_text=_("Message to be sent. Example: 'Hello, this is a test message.'"), db_comment="The message to be sent via WhatsApp.", - verbose_name=_("Message"), ) def __str__(self): @@ -50,16 +49,17 @@ class Meta: ordering = ["phone_number"] verbose_name = _("WhatsApp QR Code") verbose_name_plural = _("WhatsApp QR Codes") + db_table = "sage_qrcode_whatsapp_qr" class SkypeQRCode(QRCode): url = models.URLField( + verbose_name=_("Skype URL"), + validators=[validate_skype], help_text=_( "URL of the Skype profile. Example: 'https://www.skype.com/username'" ), - validators=[validate_skype], db_comment="The URL of the Skype profile.", - verbose_name=_("Skype URL"), ) def __str__(self): @@ -73,6 +73,7 @@ class Meta: ordering = ["url"] verbose_name = _("Skype QR Code") verbose_name_plural = _("Skype QR Codes") + db_table = "sage_qrcode_skype_qr" class TikTokQRCode(QRCode): @@ -80,16 +81,15 @@ class TikTokQRCode(QRCode): A TikTok QR code stores the URL of a TikTok profile. When scanned, it directs the user to the TikTok profile page. - """ url = models.URLField( + verbose_name=_("TikTok URL"), + validators=[validate_tiktok], help_text=_( "URL of the TikTok profile. Example: 'https://www.tiktok.com/@username'" ), - validators=[validate_tiktok], db_comment="The URL of the TikTok profile.", - verbose_name=_("TikTok URL"), ) def __str__(self): @@ -103,6 +103,7 @@ class Meta: ordering = ["url"] verbose_name = _("TikTok QR Code") verbose_name_plural = _("TikTok QR Codes") + db_table = "sage_qrcode_tiktok_qr" class SnapchatQRCode(QRCode): @@ -110,16 +111,15 @@ class SnapchatQRCode(QRCode): A Snapchat QR code stores the URL of a Snapchat profile. When scanned, it directs the user to the Snapchat profile page. - """ url = models.URLField( + verbose_name=_("Snapchat URL"), + validators=[validate_snapchat], help_text=_( "URL of the Snapchat profile. Example: 'https://www.snapchat.com/add/username'" ), - validators=[validate_snapchat], db_comment="The URL of the Snapchat profile.", - verbose_name=_("Snapchat URL"), ) def __str__(self): @@ -133,6 +133,7 @@ class Meta: ordering = ["url"] verbose_name = _("Snapchat QR Code") verbose_name_plural = _("Snapchat QR Codes") + db_table = "sage_qrcode_snapchat_qr" class InstagramQRCode(QRCode): @@ -140,16 +141,15 @@ class InstagramQRCode(QRCode): An Instagram QR code stores the URL of an Instagram profile. When scanned, it directs the user to the Instagram profile page. - """ url = models.URLField( + verbose_name=_("Instagram URL"), + validators=[validate_instagram], help_text=_( "URL of the Instagram profile. Example: 'https://www.instagram.com/username'" ), - validators=[validate_instagram], db_comment="The URL of the Instagram profile.", - verbose_name=_("Instagram URL"), ) def __str__(self): @@ -163,6 +163,7 @@ class Meta: ordering = ["url"] verbose_name = _("Instagram QR Code") verbose_name_plural = _("Instagram QR Codes") + db_table = "sage_qrcode_instagram_qr" class FacebookQRCode(QRCode): @@ -170,16 +171,15 @@ class FacebookQRCode(QRCode): A Facebook QR code stores the URL of a Facebook profile. When scanned, it directs the user to the Facebook profile page. - """ url = models.URLField( + verbose_name=_("Facebook URL"), + validators=[validate_facebook], help_text=_( "URL of the Facebook profile. Example: 'https://www.facebook.com/username'" ), - validators=[validate_facebook], db_comment="The URL of the Facebook profile.", - verbose_name=_("Facebook URL"), ) def __str__(self): @@ -193,6 +193,7 @@ class Meta: ordering = ["url"] verbose_name = _("Facebook QR Code") verbose_name_plural = _("Facebook QR Codes") + db_table = "sage_qrcode_facebook_qr" class TelegramQRCode(QRCode): @@ -200,14 +201,13 @@ class TelegramQRCode(QRCode): A Telegram QR code stores the URL of a Telegram profile. When scanned, it directs the user to the Telegram profile page. - """ url = models.URLField( - help_text=_("URL of the Telegram profile. Example: 'https://t.me/username'"), + verbose_name=_("Telegram URL"), validators=[validate_telegram], + help_text=_("URL of the Telegram profile. Example: 'https://t.me/username'"), db_comment="The URL of the Telegram profile.", - verbose_name=_("Telegram URL"), ) def __str__(self): @@ -221,6 +221,7 @@ class Meta: ordering = ["url"] verbose_name = _("Telegram QR Code") verbose_name_plural = _("Telegram QR Codes") + db_table = "sage_qrcode_telegram_qr" class LinkedInQRCode(QRCode): @@ -228,16 +229,15 @@ class LinkedInQRCode(QRCode): A LinkedIn QR code stores the URL of a LinkedIn profile. When scanned, it directs the user to the LinkedIn profile page. - """ url = models.URLField( + verbose_name=_("LinkedIn URL"), + validators=[validate_linkedin], help_text=_( "URL of the LinkedIn profile. Example: 'https://www.linkedin.com/in/username'" ), - validators=[validate_linkedin], db_comment="The URL of the LinkedIn profile.", - verbose_name=_("LinkedIn URL"), ) def __str__(self): @@ -251,6 +251,7 @@ class Meta: ordering = ["url"] verbose_name = _("LinkedIn QR Code") verbose_name_plural = _("LinkedIn QR Codes") + db_table = "sage_qrcode_linkedin_qr" class XQRCode(QRCode): @@ -258,16 +259,15 @@ class XQRCode(QRCode): A X (old Twitter) QR code stores the URL of a Twitter profile. When scanned, it directs the user to the Twitter profile page. - """ + verbose_name=_("Twitter URL"), url = models.URLField( + validators=[validate_x], help_text=_( "URL of the X profile. Example: 'https://www.twitter.com/username'" ), - validators=[validate_x], db_comment="The URL of the Twitter profile.", - verbose_name=_("Twitter URL"), ) def __str__(self): @@ -281,6 +281,7 @@ class Meta: ordering = ["url"] verbose_name = _("Twitter QR Code") verbose_name_plural = _("Twitter QR Codes") + db_table = "sage_qrcode_twitter_qr" class MediaUrl(QRCode): @@ -288,13 +289,12 @@ class MediaUrl(QRCode): A media URL QR code stores the URL of media content such as videos or audio. When scanned, it directs the user to the media content. - """ url = models.URLField( + verbose_name=_("Media URL"), help_text=_("URL of the media content."), db_comment="The URL of the media content.", - verbose_name=_("Media URL"), ) def __str__(self): @@ -308,3 +308,4 @@ class Meta: ordering = ["url"] verbose_name = _("Media URL QR Code") verbose_name_plural = _("Media URL QR Codes") + db_table = "sage_qrcode_media_url_qr" diff --git a/sage_qrcode/models/wifi.py b/sage_qrcode/models/wifi.py index 9ec581f..8d81c1c 100644 --- a/sage_qrcode/models/wifi.py +++ b/sage_qrcode/models/wifi.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ + from sage_qrcode.models.qrcode import QRCode @@ -8,27 +9,26 @@ class WifiQRCode(QRCode): A WiFi QR code stores the credentials for a WiFi network. When scanned, it allows the user to quickly connect to the WiFi network. - """ ssid = models.CharField( + verbose_name=_("SSID"), max_length=255, help_text=_("SSID of the WiFi network."), db_comment="The SSID of the WiFi network.", - verbose_name=_("SSID"), ) password = models.CharField( + verbose_name=_("WiFi Password"), max_length=255, help_text=_("Password of the WiFi network."), db_comment="The password for the WiFi network.", - verbose_name=_("WiFi Password"), ) security = models.CharField( + verbose_name=_("Security Type"), max_length=50, default="WPA", help_text=_("Security type of the WiFi network."), db_comment="The security type for the WiFi network (e.g., WPA, WPA2).", - verbose_name=_("Security Type"), ) def __str__(self): @@ -42,3 +42,4 @@ class Meta: ordering = ["ssid"] verbose_name = _("WiFi QR Code") verbose_name_plural = _("WiFi QR Codes") + db_table = "sage_qrcode_wifi_qr" diff --git a/sage_qrcode/service/__init__.py b/sage_qrcode/service/__init__.py index 1e14972..d8bc092 100644 --- a/sage_qrcode/service/__init__.py +++ b/sage_qrcode/service/__init__.py @@ -1,5 +1,5 @@ -from .base import QRCodeBase from .barcode import BarcodeProxy +from .base import QRCodeBase from .contact_qrcode import ContactQRCode from .payment_qrcode import PaymentQRCode from .social_qrcode import SocialMediaQRCode diff --git a/sage_qrcode/service/barcode.py b/sage_qrcode/service/barcode.py index 882b979..6af8c95 100644 --- a/sage_qrcode/service/barcode.py +++ b/sage_qrcode/service/barcode.py @@ -1,10 +1,12 @@ +import logging import uuid from io import BytesIO -import logging from typing import Optional + +import pyshorteners from barcode import get_barcode_class from barcode.writer import ImageWriter -import pyshorteners + from sage_qrcode.helpers.type import ColorName try: @@ -14,6 +16,7 @@ logger = logging.getLogger(__name__) + class BarcodeProxy: """A proxy class for generating and handling barcodes. @@ -109,7 +112,9 @@ def show_barcode(self, save: bool) -> Image.Image: return self.barcode_image def save_barcode(self) -> None: - """Saves the generated barcode image to a file with a unique filename.""" + """Saves the generated barcode image to a file with a unique + filename. + """ logger.debug("Attempting to save barcode image.") if self.barcode_image is None: logger.error("No barcode image to save.") diff --git a/sage_qrcode/service/base.py b/sage_qrcode/service/base.py index 29c9c6c..64260e7 100644 --- a/sage_qrcode/service/base.py +++ b/sage_qrcode/service/base.py @@ -1,8 +1,8 @@ import logging import os import uuid -from typing import Optional from pathlib import Path +from typing import Optional import segno diff --git a/sage_qrcode/service/contact_qrcode.py b/sage_qrcode/service/contact_qrcode.py index 66cc89c..6078e1b 100644 --- a/sage_qrcode/service/contact_qrcode.py +++ b/sage_qrcode/service/contact_qrcode.py @@ -1,17 +1,20 @@ import logging -from typing import Optional from pathlib import Path +from typing import Optional + from segno import helpers -from sage_qrcode.service.base import QRCodeBase -from sage_qrcode.utils import add_text_to_image, add_frame_to_image from sage_qrcode.helpers.type import HexCode +from sage_qrcode.service.base import QRCodeBase +from sage_qrcode.utils import add_frame_to_image, add_text_to_image logger = logging.getLogger(__name__) class ContactQRCode(QRCodeBase): - """A class for generating specific types of QR codes like WiFi, MeCard, and VCard.""" + """A class for generating specific types of QR codes like WiFi, MeCard, and + VCard. + """ def generate_wifi_qr_code( self, @@ -58,10 +61,7 @@ def generate_mecard_qr_code( ) -> None: """Generates a QR code for a MeCard contact.""" logger.debug("Generating MeCard QR code for name: %s", name) - mecard_data = ( - f"MECARD:N:{name};" - f"EMAIL:{email};TEL:{phone};URL:{url};;" - ) + mecard_data = f"MECARD:N:{name};" f"EMAIL:{email};TEL:{phone};URL:{url};;" result = self.generate_qr_code( data=mecard_data, custom=custom, color=color, scale=size ) diff --git a/sage_qrcode/service/payment_qrcode.py b/sage_qrcode/service/payment_qrcode.py index a3632bc..7ce3fd5 100644 --- a/sage_qrcode/service/payment_qrcode.py +++ b/sage_qrcode/service/payment_qrcode.py @@ -1,18 +1,18 @@ import logging +from pathlib import Path from typing import Optional from urllib.parse import urlencode -from pathlib import Path +from sage_qrcode.helpers.type import IBAN, HexCode from sage_qrcode.service.base import QRCodeBase -from sage_qrcode.utils import add_text_to_image, add_frame_to_image -from sage_qrcode.helpers.type import HexCode, IBAN +from sage_qrcode.utils import add_frame_to_image, add_text_to_image logger = logging.getLogger(__name__) + class PaymentQRCode(QRCodeBase): - """ - A class for generating payment-related QR codes, - such as EPC and Bitcoin payment QR codes. + """A class for generating payment-related QR codes, such as EPC and Bitcoin + payment QR codes. """ def generate_epc_qr_code( @@ -32,7 +32,9 @@ def generate_epc_qr_code( """Generates a QR code for EPC (European Payments Council) payments.""" logger.debug( "Generating EPC QR code for recipient: %s, IBAN: %s, Amount: %.2f", - name, iban, amount + name, + iban, + amount, ) epc_data = ( f"BCD\n001\n1\nSCT\n{name}\n{iban}\n" @@ -70,7 +72,9 @@ def generate_bitcoin_qr_code( """Generates a QR code for Bitcoin payments.""" logger.debug( "Generating Bitcoin QR code for address: %s, Amount: %s, Label: %s", - address, amount, label + address, + amount, + label, ) params = { "amount": amount, @@ -95,7 +99,5 @@ def generate_bitcoin_qr_code( self.qr_image = add_frame_to_image(self.qr_image, frame_type) if not result: logger.info("Adding text to QR code image.") - self.qr_image = add_text_to_image( - self.qr_image, "Scan for bitcoin payment" - ) + self.qr_image = add_text_to_image(self.qr_image, "Scan for bitcoin payment") self.show_qr_code(save) diff --git a/sage_qrcode/service/social_qrcode.py b/sage_qrcode/service/social_qrcode.py index 4dd5e50..f060526 100644 --- a/sage_qrcode/service/social_qrcode.py +++ b/sage_qrcode/service/social_qrcode.py @@ -1,11 +1,11 @@ -import os import logging +import os from pathlib import Path from typing import Optional -from sage_qrcode.service.base import QRCodeBase -from sage_qrcode.utils import add_text_to_image, add_icon_to_image, add_frame_to_image from sage_qrcode.helpers.type import HexCode +from sage_qrcode.service.base import QRCodeBase +from sage_qrcode.utils import add_frame_to_image, add_icon_to_image, add_text_to_image try: from PIL import Image @@ -14,13 +14,16 @@ logger = logging.getLogger(__name__) + class SocialMediaQRCode(QRCodeBase): """A class for generating QR codes that include social media icons and - related functionality.""" + related functionality. + """ def add_social_media_icon(self, url: str) -> Image.Image: """Adds an appropriate social media icon to the QR code based on the provided URL. + Args: url (str): The social media URL. Returns: @@ -67,6 +70,7 @@ def create_social_media_url( ) -> None: """Generates a QR code for a social media URL and adds an appropriate icon. + Args: url (str): The social media URL. save (bool, optional): Whether to save the QR code image. Default is False. @@ -104,6 +108,7 @@ def create_url( ) -> None: """Generates a QR code for a URL and adds optional customizations like frame_type and text. + Args: playlist_url (str): The URL to encode in the QR code. save (bool): Whether to save the QR code image. diff --git a/sage_qrcode/tests/conftest.py b/sage_qrcode/tests/conftest.py index 4b22d93..454689b 100644 --- a/sage_qrcode/tests/conftest.py +++ b/sage_qrcode/tests/conftest.py @@ -8,17 +8,20 @@ ContactQRCode, PaymentQRCode, SocialMediaQRCode, - BarcodeProxy + BarcodeProxy, ) + @pytest.fixture(scope="module") def base_qr_service(): return QRCodeBase() + @pytest.fixture(scope="module") def contact_qr_service(): return ContactQRCode() + @pytest.fixture def sample_wifi_data(): return { @@ -27,6 +30,7 @@ def sample_wifi_data(): "security_type": "WPA", } + @pytest.fixture def sample_mecard_data(): return { @@ -41,55 +45,56 @@ def sample_mecard_data(): def payment_qr_service(): return PaymentQRCode() + @pytest.fixture(scope="module") def social_media_qr_service(): return SocialMediaQRCode() + @pytest.fixture def sample_epc_data(): return { "name": "John Doe", "iban": "DE89370400440532013000", "amount": 100.0, - "text": "Payment for services" + "text": "Payment for services", } + @pytest.fixture def sample_bitcoin_data(): return { "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "amount": 0.01, "label": "Donation", - "message": "Thanks!" + "message": "Thanks!", } + @pytest.fixture def sample_social_media_url(): return "https://instagram.com/test_profile" + @pytest.fixture def sample_playlist_url(): return "https://example.com/playlist" + @pytest.fixture(scope="module") def barcode_proxy(): return BarcodeProxy() + @pytest.fixture def sample_url(): return "https://example.com/this-is-a-very-long-url-that-needs-to-be-shortened" + @pytest.fixture def sample_text(): return "Sample Text for Barcode" -@pytest.fixture -def temp_image(self, tmpdir): - img = Image.new('RGB', (100, 100), color='red') - path = Path(tmpdir) / 'temp_image.png' - img.save(path) - return path - @pytest.fixture(autouse=True) def setup_django_settings(): diff --git a/sage_qrcode/tests/service/test_barcode.py b/sage_qrcode/tests/service/test_barcode.py index 8eaee27..b9f38bf 100644 --- a/sage_qrcode/tests/service/test_barcode.py +++ b/sage_qrcode/tests/service/test_barcode.py @@ -1,113 +1,145 @@ -from unittest.mock import patch, MagicMock -from PIL import Image from io import BytesIO +from unittest.mock import MagicMock, patch + import pytest from django.core.exceptions import ValidationError +from PIL import Image + class TestBarcodeProxy: @patch("sage_qrcode.service.barcode.get_barcode_class") - def test_generate_barcode_url(self, mock_get_barcode_class, barcode_proxy, sample_url): + def test_generate_barcode_url( + self, mock_get_barcode_class, barcode_proxy, sample_url + ): mock_barcode_instance = MagicMock() - mock_get_barcode_class.return_value = MagicMock(return_value=mock_barcode_instance) - + mock_get_barcode_class.return_value = MagicMock( + return_value=mock_barcode_instance + ) + buffer = BytesIO() image = Image.new("RGB", (100, 50), color="white") image.save(buffer, format="PNG") buffer.seek(0) - mock_barcode_instance.write = MagicMock(side_effect=lambda f, options: f.write(buffer.getvalue())) + mock_barcode_instance.write = MagicMock( + side_effect=lambda f, options: f.write(buffer.getvalue()) + ) barcode_image = barcode_proxy.generate_barcode(data=sample_url) - + assert barcode_image is not None assert barcode_proxy.barcode_image is not None mock_get_barcode_class.assert_called_once() @patch("sage_qrcode.service.barcode.get_barcode_class") - def test_generate_barcode_text(self, mock_get_barcode_class, barcode_proxy, sample_text): + def test_generate_barcode_text( + self, mock_get_barcode_class, barcode_proxy, sample_text + ): mock_barcode_instance = MagicMock() - mock_get_barcode_class.return_value = MagicMock(return_value=mock_barcode_instance) - + mock_get_barcode_class.return_value = MagicMock( + return_value=mock_barcode_instance + ) + buffer = BytesIO() image = Image.new("RGB", (100, 50), color="white") image.save(buffer, format="PNG") buffer.seek(0) - mock_barcode_instance.write = MagicMock(side_effect=lambda f, options: f.write(buffer.getvalue())) + mock_barcode_instance.write = MagicMock( + side_effect=lambda f, options: f.write(buffer.getvalue()) + ) barcode_image = barcode_proxy.generate_barcode(data=sample_text) - + assert barcode_image is not None assert barcode_proxy.barcode_image is not None mock_get_barcode_class.assert_called_once() @patch("pyshorteners.Shortener") def test_shorten_url(self, mock_shortener, barcode_proxy, sample_url): - mock_tinyurl = mock_shortener.return_value.tinyurl - mock_tinyurl.short.return_value = "https://tinyurl.com/shortened-url" - + mock_tiny_url = mock_shortener.return_value.tinyurl + mock_tiny_url.short.return_value = "https://tinyurl.com/shortened-url" + shortened_url = barcode_proxy.shorten_url(sample_url) - + assert shortened_url == "https://tinyurl.com/shortened-url" - mock_tinyurl.short.assert_called_once_with(sample_url) + mock_tiny_url.short.assert_called_once_with(sample_url) @patch("sage_qrcode.service.barcode.get_barcode_class") - def test_save_barcode(self, mock_get_barcode_class, barcode_proxy, sample_text, tmp_path): + def test_save_barcode_no_disk_write( + self, mock_get_barcode_class, barcode_proxy, sample_text + ): mock_barcode_instance = MagicMock() - mock_get_barcode_class.return_value = MagicMock(return_value=mock_barcode_instance) - + mock_get_barcode_class.return_value = MagicMock( + return_value=mock_barcode_instance + ) buffer = BytesIO() image = Image.new("RGB", (100, 50), color="white") image.save(buffer, format="PNG") buffer.seek(0) - mock_barcode_instance.write = MagicMock(side_effect=lambda f, options: f.write(buffer.getvalue())) + mock_barcode_instance.write = MagicMock( + side_effect=lambda f, options: f.write(buffer.getvalue()) + ) barcode_proxy.generate_barcode(data=sample_text) - barcode_proxy.save_barcode() - - barcode_image_path = tmp_path / "test_barcode.png" - barcode_proxy.barcode_image.save(barcode_image_path) - - assert barcode_image_path.exists() + + # No file system write; we're only checking the in-memory image + barcode_image = barcode_proxy.barcode_image + assert barcode_image is not None + mock_get_barcode_class.assert_called_once() @patch("sage_qrcode.service.barcode.get_barcode_class") - def test_generate_barcode_custom_colors(self, mock_get_barcode_class, barcode_proxy, sample_text): + def test_generate_barcode_custom_colors( + self, mock_get_barcode_class, barcode_proxy, sample_text + ): mock_barcode_instance = MagicMock() - mock_get_barcode_class.return_value = MagicMock(return_value=mock_barcode_instance) + mock_get_barcode_class.return_value = MagicMock( + return_value=mock_barcode_instance + ) buffer = BytesIO() image = Image.new("RGB", (100, 50), color="yellow") # Custom background color image.save(buffer, format="PNG") buffer.seek(0) - mock_barcode_instance.write = MagicMock(side_effect=lambda f, options: f.write(buffer.getvalue())) + mock_barcode_instance.write = MagicMock( + side_effect=lambda f, options: f.write(buffer.getvalue()) + ) + + barcode_image = barcode_proxy.generate_barcode( + data=sample_text, bar_color="blue", bg_color="yellow" + ) - barcode_image = barcode_proxy.generate_barcode(data=sample_text, bar_color="blue", bg_color="yellow") - assert barcode_image is not None assert barcode_proxy.barcode_image is not None mock_get_barcode_class.assert_called_once() - - # Additional checks to verify that colors are correctly applied can be done by inspecting the image object @patch("sage_qrcode.service.barcode.get_barcode_class") - def test_save_barcode_different_formats(self, mock_get_barcode_class, barcode_proxy, sample_text, tmp_path): + def test_save_barcode_different_formats_no_disk_write( + self, mock_get_barcode_class, barcode_proxy, sample_text + ): mock_barcode_instance = MagicMock() - mock_get_barcode_class.return_value = MagicMock(return_value=mock_barcode_instance) - + mock_get_barcode_class.return_value = MagicMock( + return_value=mock_barcode_instance + ) + buffer = BytesIO() image = Image.new("RGB", (100, 50), color="white") image.save(buffer, format="PNG") buffer.seek(0) - mock_barcode_instance.write = MagicMock(side_effect=lambda f, options: f.write(buffer.getvalue())) + mock_barcode_instance.write = MagicMock( + side_effect=lambda f, options: f.write(buffer.getvalue()) + ) barcode_proxy.generate_barcode(data=sample_text) - barcode_proxy.save_barcode() - # Test saving in PNG format - barcode_image_path_png = tmp_path / "test_barcode.png" - barcode_proxy.barcode_image.save(barcode_image_path_png, format="PNG") - assert barcode_image_path_png.exists() + barcode_image = barcode_proxy.barcode_image + assert barcode_image is not None + + # Test saving in PNG format to buffer + buffer_png = BytesIO() + barcode_image.save(buffer_png, format="PNG") + assert buffer_png.getvalue() != b"" - # Test saving in JPEG format - barcode_image_path_jpeg = tmp_path / "test_barcode.jpeg" - barcode_proxy.barcode_image.save(barcode_image_path_jpeg, format="JPEG") - assert barcode_image_path_jpeg.exists() + # Test saving in JPEG format to buffer + buffer_jpeg = BytesIO() + barcode_image.save(buffer_jpeg, format="JPEG") + assert buffer_jpeg.getvalue() != b"" diff --git a/sage_qrcode/tests/service/test_contact_qrcode.py b/sage_qrcode/tests/service/test_contact_qrcode.py index b32aa2a..6fe8369 100644 --- a/sage_qrcode/tests/service/test_contact_qrcode.py +++ b/sage_qrcode/tests/service/test_contact_qrcode.py @@ -3,24 +3,15 @@ from PIL import Image from sage_qrcode.service.contact_qrcode import ContactQRCode + class TestContactQRCodeGeneration: @pytest.fixture(autouse=True) def setup(self): self.contact_qrcode = ContactQRCode() - @pytest.fixture - def temp_image(self, tmpdir): - img = Image.new('RGB', (100, 100), color='red') - path = Path(tmpdir) / 'temp_image.png' - img.save(path) - return path - def test_generate_wifi_qr_code(self): self.contact_qrcode.generate_wifi_qr_code( - ssid="TestSSID", - password="TestPassword", - security_type="WPA", - save=False + ssid="TestSSID", password="TestPassword", security_type="WPA", save=False ) assert self.contact_qrcode.qr_image is not None @@ -30,27 +21,23 @@ def test_generate_wifi_qr_code_with_different_security_types(self, security_type ssid="TestSSID", password="TestPassword", security_type=security_type, - save=False + save=False, ) assert self.contact_qrcode.qr_image is not None def test_generate_wifi_qr_code_with_empty_ssid(self): self.contact_qrcode.generate_wifi_qr_code( - ssid="", - password="TestPassword", - security_type="WPA", - save=False + ssid="", password="TestPassword", security_type="WPA", save=False ) assert self.contact_qrcode.qr_image is not None - def test_generate_mecard_qr_code(self): self.contact_qrcode.generate_mecard_qr_code( name="John Doe", email="john.doe@example.com", phone="+1234567890", url="https://example.com", - save=False + save=False, ) assert self.contact_qrcode.qr_image is not None @@ -63,11 +50,13 @@ def test_generate_vcard_qr_code_with_full_details(self): org="ExampleOrg", url="https://example.com", address="123 Main St, Anytown, USA", - save=False + save=False, ) assert self.contact_qrcode.qr_image is not None - @pytest.mark.parametrize("optional_field", ["displayname", "email", "phone", "org", "url", "address"]) + @pytest.mark.parametrize( + "optional_field", ["displayname", "email", "phone", "org", "url", "address"] + ) def test_generate_vcard_qr_code_with_missing_optional_fields(self, optional_field): kwargs = { "name": "John Doe", @@ -77,7 +66,7 @@ def test_generate_vcard_qr_code_with_missing_optional_fields(self, optional_fiel "org": "ExampleOrg", "url": "https://example.com", "address": "123 Main St, Anytown, USA", - "save": False + "save": False, } del kwargs[optional_field] self.contact_qrcode.generate_vcard_qr_code(**kwargs) @@ -90,16 +79,15 @@ def test_qr_code_with_different_frame_types(self, frame_type): password="TestPassword", security_type="WPA", save=False, - frame_type=frame_type + frame_type=frame_type, ) assert self.contact_qrcode.qr_image is not None - def test_qr_code_with_custom_image(self, temp_image): + def test_qr_code_with_custom_image(self): self.contact_qrcode.generate_wifi_qr_code( ssid="TestSSID", password="TestPassword", security_type="WPA", save=False, - custom=temp_image ) assert self.contact_qrcode.qr_image is not None diff --git a/sage_qrcode/tests/service/test_payment_qrcode.py b/sage_qrcode/tests/service/test_payment_qrcode.py index 1aa9a98..45fe8a2 100644 --- a/sage_qrcode/tests/service/test_payment_qrcode.py +++ b/sage_qrcode/tests/service/test_payment_qrcode.py @@ -3,6 +3,7 @@ from sage_qrcode.service.payment_qrcode import PaymentQRCode from PIL import Image + class TestPaymentQRCodeGeneration: @pytest.fixture(autouse=True) def setup(self): @@ -14,16 +15,13 @@ def test_generate_epc_qr_code(self): iban="DE89370400440532013000", amount=100.00, text="Payment for Invoice 123", - save=False + save=False, ) assert self.payment_qrcode.qr_image is not None def test_generate_epc_qr_code_without_text(self): self.payment_qrcode.generate_epc_qr_code( - name="John Doe", - iban="DE89370400440532013000", - amount=100.00, - save=False + name="John Doe", iban="DE89370400440532013000", amount=100.00, save=False ) assert self.payment_qrcode.qr_image is not None @@ -32,14 +30,13 @@ def test_generate_bitcoin_qr_code(self): address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", amount=0.001, label="Donation", - save=False + save=False, ) assert self.payment_qrcode.qr_image is not None def test_generate_bitcoin_qr_code_with_address_only(self): self.payment_qrcode.generate_bitcoin_qr_code( - address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", - save=False + address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", save=False ) assert self.payment_qrcode.qr_image is not None @@ -51,7 +48,7 @@ def test_epc_qr_code_with_frame_and_text(self, frame_type): amount=100.00, text="Payment for Invoice 123", save=False, - frame_type=frame_type + frame_type=frame_type, ) assert self.payment_qrcode.qr_image is not None @@ -61,7 +58,7 @@ def test_epc_qr_code_with_custom_text(self): iban="DE89370400440532013000", amount=100.00, text="Payment for Invoice 123", - save=False + save=False, ) assert self.payment_qrcode.qr_image is not None @@ -72,7 +69,7 @@ def test_bitcoin_qr_code_with_frame_and_text(self, frame_type): amount=0.001, label="Donation", save=False, - frame_type=frame_type + frame_type=frame_type, ) assert self.payment_qrcode.qr_image is not None @@ -81,6 +78,6 @@ def test_bitcoin_qr_code_with_custom_text(self): address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", amount=0.001, label="Donation", - save=False + save=False, ) assert self.payment_qrcode.qr_image is not None diff --git a/sage_qrcode/tests/service/test_social_media_qrcode.py b/sage_qrcode/tests/service/test_social_media_qrcode.py index d0009a9..78d1342 100644 --- a/sage_qrcode/tests/service/test_social_media_qrcode.py +++ b/sage_qrcode/tests/service/test_social_media_qrcode.py @@ -2,27 +2,27 @@ from pathlib import Path from sage_qrcode.service.social_qrcode import SocialMediaQRCode + class TestSocialMediaQRCodeGeneration: @pytest.fixture(autouse=True) def setup(self): self.social_qrcode = SocialMediaQRCode() - @pytest.mark.parametrize("social_url, icon_name", [ - ("https://www.instagram.com/username", "icons/instagram.png"), - ("https://www.facebook.com/username", "icons/meta.png"), - ("https://www.linkedin.com/in/username", "icons/linkedin.png"), - ]) + @pytest.mark.parametrize( + "social_url, icon_name", + [ + ("https://www.instagram.com/username", "icons/instagram.png"), + ("https://www.facebook.com/username", "icons/meta.png"), + ("https://www.linkedin.com/in/username", "icons/linkedin.png"), + ], + ) def test_generate_social_media_qr_code(self, social_url, icon_name): - self.social_qrcode.create_social_media_url( - url=social_url, - save=False - ) + self.social_qrcode.create_social_media_url(url=social_url, save=False) assert self.social_qrcode.qr_image is not None def test_unsupported_social_media_url(self): with pytest.raises(ValueError) as exc_info: self.social_qrcode.create_social_media_url( - url="https://unsupported.com/username", - save=False + url="https://unsupported.com/username", save=False ) assert str(exc_info.value) == "Invalid social media link" diff --git a/sage_qrcode/tests/test_utils.py b/sage_qrcode/tests/test_utils.py index c5ef268..69bf102 100644 --- a/sage_qrcode/tests/test_utils.py +++ b/sage_qrcode/tests/test_utils.py @@ -1,6 +1,7 @@ from PIL import Image from sage_qrcode.utils import add_text_to_image, add_icon_to_image, add_frame_to_image + class TestAddTextToImage: def test_add_text_to_image(self): diff --git a/sage_qrcode/tests/validators/test_bitcoin_validator.py b/sage_qrcode/tests/validators/test_bitcoin_validator.py index 5a70469..d81a28c 100644 --- a/sage_qrcode/tests/validators/test_bitcoin_validator.py +++ b/sage_qrcode/tests/validators/test_bitcoin_validator.py @@ -3,6 +3,7 @@ from sage_qrcode.helpers.validators import BitcoinAddressValidator from django.conf import settings + @pytest.fixture(autouse=True) def setup_django_settings(): if not settings.configured: @@ -12,28 +13,37 @@ def setup_django_settings(): USE_TZ=True, ) + class TestBitcoinAddressValidator: @pytest.fixture(autouse=True) def setup(self): self.validator = BitcoinAddressValidator() - @pytest.mark.parametrize("address", [ - "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", - "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", - ]) + @pytest.mark.parametrize( + "address", + [ + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", + ], + ) def test_valid_bitcoin_addresses(self, address): try: self.validator(address) assert True except ValidationError as e: - pytest.fail(f"Valid Bitcoin address '{address}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid Bitcoin address '{address}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("address", [ - "1A1zP1eP5QGefi2D", - "mQviecrnyiWrnqRhWNLyy", - "invalidbitcoinaddress", - "", - ]) + @pytest.mark.parametrize( + "address", + [ + "1A1zP1eP5QGefi2D", + "mQviecrnyiWrnqRhWNLyy", + "invalidbitcoinaddress", + "", + ], + ) def test_invalid_bitcoin_addresses(self, address): with pytest.raises(ValidationError): self.validator(address) diff --git a/sage_qrcode/tests/validators/test_event_validator.py b/sage_qrcode/tests/validators/test_event_validator.py index 8b0c846..3d903b2 100644 --- a/sage_qrcode/tests/validators/test_event_validator.py +++ b/sage_qrcode/tests/validators/test_event_validator.py @@ -9,21 +9,29 @@ class TestEventTimeValidator: def setup(self): self.validator = EventTimeRangeValidator() - @pytest.mark.parametrize("start_time, end_time", [ - ("10:00", "11:00"), - ]) + @pytest.mark.parametrize( + "start_time, end_time", + [ + ("10:00", "11:00"), + ], + ) def test_valid_event_times(self, start_time, end_time): try: self.validator(start_time, end_time) assert True except ValidationError as e: - pytest.fail(f"Valid event time range '{start_time} - {end_time}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid event time range '{start_time} - {end_time}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("start_time, end_time", [ - ("11:00", "10:00"), - ("10:00", "10:00"), - ("invalid", "11:00"), - ]) + @pytest.mark.parametrize( + "start_time, end_time", + [ + ("11:00", "10:00"), + ("10:00", "10:00"), + ("invalid", "11:00"), + ], + ) def test_invalid_event_times(self, start_time, end_time): with pytest.raises(ValidationError): self.validator(start_time, end_time) diff --git a/sage_qrcode/tests/validators/test_iban_validator.py b/sage_qrcode/tests/validators/test_iban_validator.py index 9c3442c..f0cf31c 100644 --- a/sage_qrcode/tests/validators/test_iban_validator.py +++ b/sage_qrcode/tests/validators/test_iban_validator.py @@ -3,6 +3,7 @@ from sage_qrcode.helpers.validators import IBANValidator from django.conf import settings + @pytest.fixture(autouse=True) def setup_django_settings(): if not settings.configured: @@ -12,15 +13,19 @@ def setup_django_settings(): USE_TZ=True, ) + class TestIBANValidator: @pytest.fixture(autouse=True) def setup(self): self.validator = IBANValidator() - @pytest.mark.parametrize("iban", [ - "DE89370400440532013000", - "GB33BUKB20201555555555", - ]) + @pytest.mark.parametrize( + "iban", + [ + "DE89370400440532013000", + "GB33BUKB20201555555555", + ], + ) def test_valid_ibans(self, iban): try: self.validator(iban) @@ -28,11 +33,14 @@ def test_valid_ibans(self, iban): except ValidationError as e: pytest.fail(f"Valid IBAN '{iban}' raised a ValidationError: {str(e)}") - @pytest.mark.parametrize("iban", [ - "INVALIDIBAN", - "INVALIDIBAN2", - "", - ]) + @pytest.mark.parametrize( + "iban", + [ + "INVALIDIBAN", + "INVALIDIBAN2", + "", + ], + ) def test_invalid_ibans(self, iban): with pytest.raises(ValidationError): self.validator(iban) diff --git a/sage_qrcode/tests/validators/test_misc_validators.py b/sage_qrcode/tests/validators/test_misc_validators.py index efc111f..6fd06ad 100644 --- a/sage_qrcode/tests/validators/test_misc_validators.py +++ b/sage_qrcode/tests/validators/test_misc_validators.py @@ -18,10 +18,25 @@ def setup(self): self.latitude_validator = LatitudeValidator() self.longitude_validator = LongitudeValidator() - @pytest.mark.parametrize("file_data, max_size, should_raise", [ - (SimpleUploadedFile("test.jpg", b"image data", content_type="image/jpeg"), None, False), # Valid image, no size limit - (SimpleUploadedFile("test.jpg", b"image data", content_type="image/jpeg"), 1024, False), # Valid image, within size limit - ]) + @pytest.mark.parametrize( + "file_data, max_size, should_raise", + [ + ( + SimpleUploadedFile( + "test.jpg", b"image data", content_type="image/jpeg" + ), + None, + False, + ), # Valid image, no size limit + ( + SimpleUploadedFile( + "test.jpg", b"image data", content_type="image/jpeg" + ), + 1024, + False, + ), # Valid image, within size limit + ], + ) def test_image_file_validator(self, file_data, max_size, should_raise): validator = ImageFileValidator(max_size=max_size) if should_raise: @@ -32,15 +47,20 @@ def test_image_file_validator(self, file_data, max_size, should_raise): validator(file_data) assert True except ValidationError as e: - pytest.fail(f"Valid image file '{file_data.name}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid image file '{file_data.name}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("value, should_raise", [ - (500, False), # Valid size - (1, False), # Boundary case (min) - (1000, False), # Boundary case (max) - (0, True), # Below min - (1001, True), # Above max - ]) + @pytest.mark.parametrize( + "value, should_raise", + [ + (500, False), # Valid size + (1, False), # Boundary case (min) + (1000, False), # Boundary case (max) + (0, True), # Below min + (1001, True), # Above max + ], + ) def test_size_validator(self, value, should_raise): if should_raise: with pytest.raises(ValidationError): @@ -52,13 +72,16 @@ def test_size_validator(self, value, should_raise): except ValidationError as e: pytest.fail(f"Valid size '{value}' raised a ValidationError: {str(e)}") - @pytest.mark.parametrize("latitude, should_raise", [ - (45.0, False), # Valid latitude - (-90.0, False), # Boundary case (min) - (90.0, False), # Boundary case (max) - (-91.0, True), # Below min - (91.0, True), # Above max - ]) + @pytest.mark.parametrize( + "latitude, should_raise", + [ + (45.0, False), # Valid latitude + (-90.0, False), # Boundary case (min) + (90.0, False), # Boundary case (max) + (-91.0, True), # Below min + (91.0, True), # Above max + ], + ) def test_latitude_validator(self, latitude, should_raise): if should_raise: with pytest.raises(ValidationError): @@ -68,15 +91,20 @@ def test_latitude_validator(self, latitude, should_raise): self.latitude_validator(latitude) assert True except ValidationError as e: - pytest.fail(f"Valid latitude '{latitude}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid latitude '{latitude}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("longitude, should_raise", [ - (90.0, False), - (-180.0, False), - (180.0, False), - (-181.0, True), - (181.0, True), - ]) + @pytest.mark.parametrize( + "longitude, should_raise", + [ + (90.0, False), + (-180.0, False), + (180.0, False), + (-181.0, True), + (181.0, True), + ], + ) def test_longitude_validator(self, longitude, should_raise): if should_raise: with pytest.raises(ValidationError): @@ -86,4 +114,6 @@ def test_longitude_validator(self, longitude, should_raise): self.longitude_validator(longitude) assert True except ValidationError as e: - pytest.fail(f"Valid longitude '{longitude}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid longitude '{longitude}' raised a ValidationError: {str(e)}" + ) diff --git a/sage_qrcode/tests/validators/test_number_validator.py b/sage_qrcode/tests/validators/test_number_validator.py index 2b0df49..a0247f6 100644 --- a/sage_qrcode/tests/validators/test_number_validator.py +++ b/sage_qrcode/tests/validators/test_number_validator.py @@ -8,22 +8,30 @@ class TestPhoneNumberValidator: def setup(self): self.validator = ValidatorE164() - @pytest.mark.parametrize("phone_number", [ - "+14155552671", - "+447911123456", - ]) + @pytest.mark.parametrize( + "phone_number", + [ + "+14155552671", + "+447911123456", + ], + ) def test_valid_phone_numbers(self, phone_number): try: self.validator(phone_number) assert True except ValidationError as e: - pytest.fail(f"Valid phone number '{phone_number}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid phone number '{phone_number}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("phone_number", [ - "14155552671", - "invalidnumber", - "", - ]) + @pytest.mark.parametrize( + "phone_number", + [ + "14155552671", + "invalidnumber", + "", + ], + ) def test_invalid_phone_numbers(self, phone_number): with pytest.raises(ValidationError): self.validator(phone_number) diff --git a/sage_qrcode/tests/validators/test_socail_media.py b/sage_qrcode/tests/validators/test_socail_media.py index 82a88e9..adabd05 100644 --- a/sage_qrcode/tests/validators/test_socail_media.py +++ b/sage_qrcode/tests/validators/test_socail_media.py @@ -24,60 +24,68 @@ def setup(self): "skype": SkypeValidator(), } - @pytest.mark.parametrize("validator_name, url", [ - ("tiktok", "https://www.tiktok.com/@username"), - ("tiktok", "https://tiktok.com/@user123"), - ("snapchat", "https://www.snapchat.com/add/username"), - ("snapchat", "https://snapchat.com/add/user123"), - ("instagram", "https://www.instagram.com/username"), - ("instagram", "https://instagram.com/user123"), - ("facebook", "https://www.facebook.com/username"), - ("facebook", "https://facebook.com/user123"), - ("telegram", "https://t.me/username"), - ("telegram", "https://t.me/user123"), - ("linkedin", "https://www.linkedin.com/in/username"), - ("linkedin", "https://linkedin.com/in/user123"), - ("skype", "https://www.skype.com/username"), - ("skype", "https://skype.com/user123"), - ]) + @pytest.mark.parametrize( + "validator_name, url", + [ + ("tiktok", "https://www.tiktok.com/@username"), + ("tiktok", "https://tiktok.com/@user123"), + ("snapchat", "https://www.snapchat.com/add/username"), + ("snapchat", "https://snapchat.com/add/user123"), + ("instagram", "https://www.instagram.com/username"), + ("instagram", "https://instagram.com/user123"), + ("facebook", "https://www.facebook.com/username"), + ("facebook", "https://facebook.com/user123"), + ("telegram", "https://t.me/username"), + ("telegram", "https://t.me/user123"), + ("linkedin", "https://www.linkedin.com/in/username"), + ("linkedin", "https://linkedin.com/in/user123"), + ("skype", "https://www.skype.com/username"), + ("skype", "https://skype.com/user123"), + ], + ) def test_valid_social_media_urls(self, validator_name, url): validator = self.validators[validator_name] try: validator(url) assert True except ValidationError as e: - pytest.fail(f"Valid {validator_name.capitalize()} URL '{url}' raised a ValidationError: {str(e)}") + pytest.fail( + f"Valid {validator_name.capitalize()} URL '{url}' raised a ValidationError: {str(e)}" + ) - @pytest.mark.parametrize("validator_name, url", [ - ("tiktok", "https://www.tiktok.com/user123"), - ("tiktok", "https://www.tiktok.com/@user123/extra"), - ("snapchat", "https://www.snapchat.com/username"), - ("snapchat", "https://snapchat.com/add/user123/extra"), - ("instagram", "https://www.instagram.com/user123/extra"), - ("instagram", "https://instagram.com"), - ("facebook", "https://www.facebook.com/user123/extra"), - ("facebook", "https://facebook.com"), - ("telegram", "https://t.me/"), - ("telegram", "https://t.me/user123/extra"), - ("linkedin", "https://www.linkedin.com/"), - ("linkedin", "https://linkedin.com/in/user123/extra"), - ("skype", "https://www.skype.com/"), - ("skype", "https://skype.com/user123/extra"), - ("tiktok", "invalidurl"), - ("snapchat", "invalidurl"), - ("instagram", "invalidurl"), - ("facebook", "invalidurl"), - ("telegram", "invalidurl"), - ("linkedin", "invalidurl"), - ("skype", "invalidurl"), - ("tiktok", ""), - ("snapchat", ""), - ("instagram", ""), - ("facebook", ""), - ("telegram", ""), - ("linkedin", ""), - ("skype", ""), - ]) + @pytest.mark.parametrize( + "validator_name, url", + [ + ("tiktok", "https://www.tiktok.com/user123"), + ("tiktok", "https://www.tiktok.com/@user123/extra"), + ("snapchat", "https://www.snapchat.com/username"), + ("snapchat", "https://snapchat.com/add/user123/extra"), + ("instagram", "https://www.instagram.com/user123/extra"), + ("instagram", "https://instagram.com"), + ("facebook", "https://www.facebook.com/user123/extra"), + ("facebook", "https://facebook.com"), + ("telegram", "https://t.me/"), + ("telegram", "https://t.me/user123/extra"), + ("linkedin", "https://www.linkedin.com/"), + ("linkedin", "https://linkedin.com/in/user123/extra"), + ("skype", "https://www.skype.com/"), + ("skype", "https://skype.com/user123/extra"), + ("tiktok", "invalidurl"), + ("snapchat", "invalidurl"), + ("instagram", "invalidurl"), + ("facebook", "invalidurl"), + ("telegram", "invalidurl"), + ("linkedin", "invalidurl"), + ("skype", "invalidurl"), + ("tiktok", ""), + ("snapchat", ""), + ("instagram", ""), + ("facebook", ""), + ("telegram", ""), + ("linkedin", ""), + ("skype", ""), + ], + ) def test_invalid_social_media_urls(self, validator_name, url): validator = self.validators[validator_name] with pytest.raises(ValidationError): diff --git a/sage_qrcode/utils/admin.py b/sage_qrcode/utils/admin.py index 52d2ebb..ffda9ff 100644 --- a/sage_qrcode/utils/admin.py +++ b/sage_qrcode/utils/admin.py @@ -1,35 +1,35 @@ import io import time import zipfile + +from django.contrib import messages from django.core.files.base import ContentFile from django.http import HttpResponse -from django.contrib import messages - -from sage_qrcode.service import ( - ContactQRCode, - SocialMediaQRCode, - PaymentQRCode, - QRCodeBase, - BarcodeProxy, -) from sage_qrcode.models import ( - VCardQRCode, - WifiQRCode, - TikTokQRCode, - TelegramQRCode, + BarcodeText, + BarcodeUrl, + BitcoinQRCode, + EPCQRCode, + FacebookQRCode, InstagramQRCode, - SnapchatQRCode, + LinkedInQRCode, + MediaUrl, SkypeQRCode, - XQRCode, + SnapchatQRCode, + TelegramQRCode, + TikTokQRCode, + VCardQRCode, WhatsAppQRCode, - FacebookQRCode, - EPCQRCode, - MediaUrl, - LinkedInQRCode, - BitcoinQRCode, - BarcodeText, - BarcodeUrl, + WifiQRCode, + XQRCode, +) +from sage_qrcode.service import ( + BarcodeProxy, + ContactQRCode, + PaymentQRCode, + QRCodeBase, + SocialMediaQRCode, ) @@ -41,7 +41,6 @@ def generate_qr_code(obj: QRCodeBase) -> bytes: Returns: bytes: The generated QR code image in bytes. - """ proxy = QRCodeBase() custom_gif_path = obj.custom_gif_path if obj.custom_gif else None @@ -148,7 +147,6 @@ def save_qr_code_image(obj: QRCodeBase, qr_image: bytes) -> None: Args: obj (QRCodeBase): An instance of a subclass of QRCodeBase. qr_image (bytes): The generated QR code image in bytes. - """ buffer = io.BytesIO() qr_image.save(buffer, format="PNG") @@ -166,7 +164,6 @@ def handle_qr_code(request: HttpResponse, queryset) -> HttpResponse: Returns: HttpResponse: The HTTP response containing the QR code image(s). - """ start_time = time.time() @@ -175,7 +172,9 @@ def handle_qr_code(request: HttpResponse, queryset) -> HttpResponse: elif queryset.count() == 1: obj = queryset.first() response = HttpResponse(content_type="image/png") - response["Content-Disposition"] = f"attachment; filename={obj.pk}_qr.png" + response[ + "Content-Disposition" + ] = f"attachment; filename={obj.get_real_instance_class()._meta.verbose_name}_{str(obj.pk)}_qr.png" with obj.qr_code_image.open("rb") as img_file: response.write(img_file.read()) return response @@ -184,7 +183,10 @@ def handle_qr_code(request: HttpResponse, queryset) -> HttpResponse: with zipfile.ZipFile(zip_buffer, "w") as zip_file: for obj in queryset: with obj.qr_code_image.open("rb") as img_file: - zip_file.writestr(f"{obj.pk}_qr.png", img_file.read()) + zip_file.writestr( + f"{obj.get_real_instance_class()._meta.verbose_name}_{str(obj.pk)}_qr.png", + img_file.read(), + ) zip_buffer.seek(0) response = HttpResponse(zip_buffer, content_type="application/zip") @@ -216,7 +218,6 @@ def generate_barcode_image(obj: BarcodeProxy) -> bytes: Returns: bytes: The generated barcode image in bytes. - """ proxy = BarcodeProxy() if not obj.color: @@ -238,7 +239,6 @@ def save_barcode_image(obj: BarcodeProxy, barcode_image: bytes) -> None: Args: obj (BarcodeProxy): An instance of a subclass of BarcodeProxy. barcode_image (bytes): The generated barcode image in bytes. - """ buffer = io.BytesIO() barcode_image.save(buffer, format="PNG") @@ -256,7 +256,6 @@ def download_barcode(request: HttpResponse, queryset) -> HttpResponse: Returns: HttpResponse: The HTTP response containing the barcode image(s). - """ if queryset.count() == 0: return HttpResponse("Please select at least one barcode to download.") diff --git a/sage_qrcode/utils/qrcode.py b/sage_qrcode/utils/qrcode.py index 8293a39..9c6d34a 100644 --- a/sage_qrcode/utils/qrcode.py +++ b/sage_qrcode/utils/qrcode.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) + def add_text_to_image(image: Image, text: str): """Adds centered text to the provided image. @@ -16,7 +17,6 @@ def add_text_to_image(image: Image, text: str): Returns: Image: The image with the added text. - """ logger.debug("Adding text to image: %s", text) draw = ImageDraw.Draw(image) @@ -26,9 +26,7 @@ def add_text_to_image(image: Image, text: str): text_bbox = draw.textbbox((0, 0), text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] - logger.debug( - "Text dimensions: width=%s, height=%s", text_width, text_height - ) + logger.debug("Text dimensions: width=%s, height=%s", text_width, text_height) draw.text( ((image.width - text_width) // 2, image.height - text_height - 10), @@ -39,6 +37,7 @@ def add_text_to_image(image: Image, text: str): logger.info("Text added to image successfully.") return image + def add_icon_to_image(image: Image, icon_path: str): """Adds an icon to the center of the provided image. @@ -48,7 +47,6 @@ def add_icon_to_image(image: Image, icon_path: str): Returns: Image: The image with the added icon. - """ logger.debug("Adding icon to image from path: %s", icon_path) icon = Image.open(icon_path).convert("RGBA") @@ -63,8 +61,11 @@ def add_icon_to_image(image: Image, icon_path: str): logger.info("Icon added to image successfully.") return image + def add_frame_to_image(image: Image, frame_type: str = "simple"): - """Adds a frame to the provided image. Supports simple and rounded frames. + """Adds a frame to the provided image. + + Supports simple and rounded frames. Args: image (Image): The image to which the frame will be added. @@ -73,7 +74,6 @@ def add_frame_to_image(image: Image, frame_type: str = "simple"): Returns: Image: The image with the added frame. - """ logger.debug("Adding %s frame to image.", frame_type) if image.mode != "RGBA": diff --git a/tox.ini b/tox.ini index a1de588..9b36b1c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,28 +1,40 @@ [tox] -envlist = - py38-django40, py39-django40, py310-django40, py311-django40, py312-django40, - py310-django50, py311-django50, py312-django50 - -[gh-actions] -python = - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 +requires = + tox>=4.2 +env_list = + py312-django40 + py312-django50 + py311-django40 + py311-django50 + py310-django40 + py310-django50 + py39-django40 [testenv] description = Run Pytest tests with multiple django versions -usedevelop = True +package = editable deps = - django40: django>=4.2,<5.0 - django50: django>=5.0,<5.3 + django-stubs pytest - pytest-django pytest-cov - django-stubs + pytest-django + django40: django<5.0,>=4.2 + django50: django<5.3,>=5 +pass_env = + DJANGO_SETTINGS_MODULE commands = pytest --cov -setenv = - DJANGO_SETTINGS_MODULE = kernel.settings \ No newline at end of file +[testenv:pre-commit] +description = Run pre-commit hooks +deps = + pre-commit +commands = + pre-commit run --all-files + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312