diff --git a/.dockerignore b/.dockerignore index d874ad6..8d9b7c8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,11 @@ *.tar +.venv/ +__pycache__/ +*.pyc +.idea/ +*.log +.git/ +.ruff_cache/ +.ty_cache/ +.pytest_cache/ +tests/result/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9f6f944 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.14" + - run: uv sync + - run: uv run ruff check . + - run: uv run ruff format --check . + - run: uv run ty check + - run: uv run codespell + - run: uv run bandit -r . -c pyproject.toml -q + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.14" + - run: uv sync + - run: uv run pytest -m "not slow" diff --git a/.gitignore b/.gitignore index b5ac65f..bdc846b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ image.tar *.png + +app.log +__pycache__/ +*.pyc +.venv/ +.pytest_cache/ +.ruff_cache/ +.ty_cache/ +tests/result/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..06e2b26 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,54 @@ +default_install_hook_types: [pre-commit, post-merge] +exclude: 'static/' + +repos: +- repo: local + hooks: + - id: uv-lock-check + name: uv lock --check + entry: uv lock --check + language: system + pass_filenames: false + files: ^(pyproject\.toml|uv\.lock)$ + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: local + hooks: + - id: ruff-check + name: ruff check + entry: uv run ruff check + language: system + types_or: [python, pyi] + pass_filenames: false + always_run: true + - id: ruff-format + name: ruff format + entry: uv run ruff format --check + language: system + types_or: [python, pyi] + pass_filenames: false + always_run: true + - id: codespell + name: codespell + entry: uv run codespell + language: system + types_or: [python, markdown, yaml] + pass_filenames: false + always_run: true + - id: ty-check + name: ty check + entry: uv run ty check + language: system + pass_filenames: false + always_run: true + - id: bandit + name: bandit + entry: uv run bandit -r . -c pyproject.toml -q + language: system + pass_filenames: false + always_run: true diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..a6d9ada --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14.5 diff --git a/Dockerfile b/Dockerfile index c148367..f7d1d06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,8 @@ -FROM python:3.12 +FROM python:3.14-slim LABEL authors="AM" +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv WORKDIR /app -COPY requirements.txt ./ - -RUN python -m pip install --upgrade pip && \ - pip install -r requirements.txt - +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev COPY . . - -CMD ["python", "app.py"] - +CMD ["uv", "run", "--no-sync", "python", "app.py"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0a6aa80 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: up down restart logs build + +up: + docker compose -f docker-compose-traefik.yml up -d --build + +down: + docker compose -f docker-compose-traefik.yml down + +restart: down up + +logs: + docker logs -f qr_web + +build: + docker compose -f docker-compose-traefik.yml build diff --git a/README.md b/README.md index 9db94e9..82dfcb2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,19 @@ The purpose of this project is to demonstrate how to integrate and test the Segn ## Getting Started: +### Local development + +Requires [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/). + +```powershell +just install # install all dependencies (including dev) +just runserver # start Flask on http://127.0.0.1:5000/ +just test # run all tests (excluding slow) +just lint # ruff, ty, codespell, bandit +just git-precommit # run pre-commit hooks on all files +``` + +> **Note:** `docker-compose.yml` mounts the working directory into the container (`.:/app`) for development convenience. Remove or replace the `volumes` section before using it in production. ### Commands reminder: @@ -38,7 +51,7 @@ minikube tunnel #### Checks: ```powershell -kubectl get pods +kubectl get pods docker images ``` diff --git a/app.log b/app.log deleted file mode 100644 index 0680b63..0000000 --- a/app.log +++ /dev/null @@ -1,213 +0,0 @@ -2024-04-16 05:16:42,760 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2024-04-16 05:16:42,760 - INFO - Press CTRL+C to quit -2024-04-16 05:16:42,761 - INFO - * Restarting with stat -2024-04-16 05:16:42,947 - WARNING - * Debugger is active! -2024-04-16 05:16:42,953 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:17:13,010 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:17:13,013 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET / HTTP/1.1" 200 - -2024-04-16 05:17:13,146 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET /static/js/form_remove_image.js HTTP/1.1" 200 - -2024-04-16 05:17:13,147 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 - -2024-04-16 05:17:34,782 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "GET / HTTP/1.1" 200 - -2024-04-16 05:17:34,812 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:17:34,813 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:17:37,759 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "GET / HTTP/1.1" 200 - -2024-04-16 05:17:37,790 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:17:37,790 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:17:43,620 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:17:43,631 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:17:43,631 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "GET /result HTTP/1.1" 200 - -2024-04-16 05:17:43,676 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "GET /download HTTP/1.1" 500 - -2024-04-16 05:17:43,705 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:18:06,974 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:06] "GET / HTTP/1.1" 200 - -2024-04-16 05:18:09,564 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "GET / HTTP/1.1" 200 - -2024-04-16 05:18:09,603 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:18:09,606 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:20:36,453 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading -2024-04-16 05:20:36,485 - INFO - * Restarting with stat -2024-04-16 05:20:36,687 - WARNING - * Debugger is active! -2024-04-16 05:20:36,691 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:20:40,442 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "GET / HTTP/1.1" 200 - -2024-04-16 05:20:40,558 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:20:40,559 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:20:46,243 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "GET / HTTP/1.1" 200 - -2024-04-16 05:20:46,279 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:20:46,279 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:20:50,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "GET / HTTP/1.1" 200 - -2024-04-16 05:20:50,862 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:20:50,863 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:20:53,352 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "GET / HTTP/1.1" 200 - -2024-04-16 05:20:53,387 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:20:53,388 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:20:57,894 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:20:57,905 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /result HTTP/1.1" 200 - -2024-04-16 05:20:57,990 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /download HTTP/1.1" 200 - -2024-04-16 05:20:57,991 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:21:05,702 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:21:05,703 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:05] "GET / HTTP/1.1" 200 - -2024-04-16 05:21:08,476 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:21:08,486 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "GET /result HTTP/1.1" 200 - -2024-04-16 05:21:08,542 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "GET /download HTTP/1.1" 200 - -2024-04-16 05:21:08,548 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:21:10,445 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:21:10,495 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 - -2024-04-16 05:21:10,495 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 - -2024-04-16 05:21:10,530 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 - -2024-04-16 05:21:10,537 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 - -2024-04-16 05:21:53,142 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "GET /result HTTP/1.1" 200 - -2024-04-16 05:21:53,182 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:21:53,214 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "GET /download HTTP/1.1" 304 - -2024-04-16 05:21:56,845 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "GET /result HTTP/1.1" 200 - -2024-04-16 05:21:56,876 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:21:56,908 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "GET /download HTTP/1.1" 304 - -2024-04-16 05:22:05,360 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "GET /result HTTP/1.1" 200 - -2024-04-16 05:22:05,387 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:22:05,411 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "GET /download HTTP/1.1" 304 - -2024-04-16 05:22:14,547 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "GET /result HTTP/1.1" 200 - -2024-04-16 05:22:14,574 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:22:14,596 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "GET /download HTTP/1.1" 304 - -2024-04-16 05:22:41,914 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "GET /result HTTP/1.1" 200 - -2024-04-16 05:22:41,950 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:22:41,964 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "GET /download HTTP/1.1" 304 - -2024-04-16 05:22:54,548 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "GET /result HTTP/1.1" 200 - -2024-04-16 05:22:54,580 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "GET /download_with_image HTTP/1.1" 500 - -2024-04-16 05:22:54,605 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "GET /download HTTP/1.1" 304 - -2024-04-16 05:24:15,869 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:15] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:15,918 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:15] "GET /download HTTP/1.1" 304 - -2024-04-16 05:24:16,524 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:16] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:16,625 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:16] "GET /download HTTP/1.1" 304 - -2024-04-16 05:24:17,407 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:17] "GET / HTTP/1.1" 200 - -2024-04-16 05:24:18,420 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:24:18,432 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:18,494 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "GET /download HTTP/1.1" 200 - -2024-04-16 05:24:25,556 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:24:25,572 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:25,674 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "GET /download HTTP/1.1" 200 - -2024-04-16 05:24:44,722 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:44] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:44,775 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:44] "GET /download HTTP/1.1" 200 - -2024-04-16 05:24:45,443 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:45] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:45,502 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:45] "GET /download HTTP/1.1" 304 - -2024-04-16 05:24:46,730 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:46] "GET /result HTTP/1.1" 200 - -2024-04-16 05:24:46,821 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:46] "GET /download HTTP/1.1" 304 - -2024-04-16 05:26:13,901 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading -2024-04-16 05:26:13,934 - INFO - * Restarting with stat -2024-04-16 05:26:14,161 - WARNING - * Debugger is active! -2024-04-16 05:26:14,166 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:26:23,270 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading -2024-04-16 05:26:23,304 - INFO - * Restarting with stat -2024-04-16 05:26:23,505 - WARNING - * Debugger is active! -2024-04-16 05:26:23,510 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:26:24,505 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /result HTTP/1.1" 200 - -2024-04-16 05:26:24,698 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /download HTTP/1.1" 304 - -2024-04-16 05:26:24,700 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:26:24,737 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /favicon.ico HTTP/1.1" 404 - -2024-04-16 05:26:25,942 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:25] "GET /result HTTP/1.1" 200 - -2024-04-16 05:26:26,018 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:26] "GET /download HTTP/1.1" 304 - -2024-04-16 05:26:26,019 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:26] "GET /download_with_image HTTP/1.1" 304 - -2024-04-16 05:26:42,772 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "GET /result HTTP/1.1" 200 - -2024-04-16 05:26:42,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "GET /download HTTP/1.1" 304 - -2024-04-16 05:26:42,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "GET /download_with_image HTTP/1.1" 304 - -2024-04-16 05:26:47,846 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:26:47,848 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "GET / HTTP/1.1" 200 - -2024-04-16 05:26:47,878 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:26:47,878 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:26:52,480 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:26:52,491 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "GET /result HTTP/1.1" 200 - -2024-04-16 05:26:52,562 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "GET /download HTTP/1.1" 200 - -2024-04-16 05:26:54,115 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:54] "GET / HTTP/1.1" 200 - -2024-04-16 05:26:57,509 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "GET / HTTP/1.1" 200 - -2024-04-16 05:26:57,534 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:26:57,535 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:28:58,363 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "GET / HTTP/1.1" 200 - -2024-04-16 05:28:58,412 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:28:58,416 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:29:33,501 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "GET / HTTP/1.1" 200 - -2024-04-16 05:29:33,536 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:29:33,536 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:30:06,705 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "GET / HTTP/1.1" 200 - -2024-04-16 05:30:06,742 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:30:06,743 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:30:07,622 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "GET / HTTP/1.1" 200 - -2024-04-16 05:30:07,691 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:30:07,693 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:30:30,254 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET / HTTP/1.1" 200 - -2024-04-16 05:30:30,297 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 - -2024-04-16 05:30:30,300 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET /static/js/form_remove_image.js HTTP/1.1" 200 - -2024-04-16 05:30:41,634 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:30:41,650 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /result HTTP/1.1" 200 - -2024-04-16 05:30:41,688 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /download HTTP/1.1" 200 - -2024-04-16 05:30:41,689 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:30:44,216 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:30:44,217 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:44] "GET / HTTP/1.1" 200 - -2024-04-16 05:30:47,545 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:30:47,557 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /result HTTP/1.1" 200 - -2024-04-16 05:30:47,619 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /download HTTP/1.1" 200 - -2024-04-16 05:30:47,620 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:31:38,356 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\mixins.py', reloading -2024-04-16 05:31:38,383 - INFO - * Restarting with stat -2024-04-16 05:31:38,576 - WARNING - * Debugger is active! -2024-04-16 05:31:38,581 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:31:38,593 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:31:38,598 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET / HTTP/1.1" 200 - -2024-04-16 05:31:38,719 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 - -2024-04-16 05:31:38,720 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET /static/js/form_remove_image.js HTTP/1.1" 200 - -2024-04-16 05:31:39,357 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "GET / HTTP/1.1" 200 - -2024-04-16 05:31:39,405 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "GET /static/js/input_errors_modal.js HTTP/1.1" 304 - -2024-04-16 05:31:39,409 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "GET /static/js/form_remove_image.js HTTP/1.1" 304 - -2024-04-16 05:31:42,816 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:31:42,837 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /result HTTP/1.1" 200 - -2024-04-16 05:31:42,966 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /download HTTP/1.1" 200 - -2024-04-16 05:31:42,966 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:31:44,499 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:31:44,499 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:44] "GET / HTTP/1.1" 200 - -2024-04-16 05:31:47,171 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:31:47,192 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /result HTTP/1.1" 200 - -2024-04-16 05:31:47,264 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /download HTTP/1.1" 200 - -2024-04-16 05:31:47,264 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:31:50,414 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:31:50,424 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /result HTTP/1.1" 200 - -2024-04-16 05:31:50,488 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /download HTTP/1.1" 200 - -2024-04-16 05:31:50,489 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:31:54,267 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:31:54,277 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /result HTTP/1.1" 200 - -2024-04-16 05:31:54,334 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /download HTTP/1.1" 200 - -2024-04-16 05:31:54,335 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:33:28,318 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:33:28,337 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /result HTTP/1.1" 200 - -2024-04-16 05:33:28,444 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /download HTTP/1.1" 200 - -2024-04-16 05:33:28,447 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:37:36,052 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:37:36,065 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /result HTTP/1.1" 200 - -2024-04-16 05:37:36,132 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /download HTTP/1.1" 200 - -2024-04-16 05:37:36,133 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:38:00,078 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:38:00,091 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /result HTTP/1.1" 200 - -2024-04-16 05:38:00,152 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /download HTTP/1.1" 200 - -2024-04-16 05:38:00,152 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /download_with_image HTTP/1.1" 200 - -2024-04-16 05:38:07,088 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 05:38:07,088 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:07] "GET / HTTP/1.1" 200 - -2024-04-16 05:38:09,111 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "POST /generate HTTP/1.1" 200 - -2024-04-16 05:38:09,125 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "GET /result HTTP/1.1" 200 - -2024-04-16 05:38:09,193 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "GET /download HTTP/1.1" 200 - -2024-04-16 05:39:24,775 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\qr_generator.py', reloading -2024-04-16 05:39:24,812 - INFO - * Restarting with stat -2024-04-16 05:39:25,031 - WARNING - * Debugger is active! -2024-04-16 05:39:25,037 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:41:26,769 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\logger.py', reloading -2024-04-16 05:41:26,805 - INFO - * Restarting with stat -2024-04-16 05:41:27,005 - WARNING - * Debugger is active! -2024-04-16 05:41:27,010 - INFO - * Debugger PIN: 136-546-380 -2024-04-16 05:42:33,413 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading -2024-04-16 05:42:33,448 - INFO - * Restarting with stat -2024-04-16 05:55:20,539 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 06:11:04,905 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:11:19,712 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 06:13:39,656 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 06:13:55,412 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 06:19:41,117 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:26:39,508 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:26:45,748 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.png' -2024-04-16 06:27:20,691 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:34:42,506 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:35:34,416 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' -2024-04-16 06:37:00,955 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleŸć okreœlonego pliku: 'qr_with_image.gif' diff --git a/app.py b/app.py index b169e59..95fc305 100644 --- a/app.py +++ b/app.py @@ -1,69 +1,111 @@ +import io import os +import secrets + +from flask import Flask, abort, jsonify, render_template, request, send_file +from flask_wtf import CSRFProtect +from flask_wtf.csrf import CSRFError -from flask import Flask, render_template, request, send_file, jsonify from utils.logger import AppLogger from utils.qr_generator import QRCodeGenerator - +from utils.result_store import ResultStore AppLogger.configure_logger() app = Flask(__name__) +app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or secrets.token_hex(32) +app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024 # 5 MB image + multipart headroom +csrf = CSRFProtect(app) +store = ResultStore() + +@app.errorhandler(413) +def request_too_large(e): + return jsonify({'success': False, 'errors': ['File is too large. Maximum request size is 6 MB.']}), 413 -def before_request_index(): - file_names = ('qr', 'qr_with_image') - current_dir = os.listdir() - for filename in current_dir: - for name in file_names: - if filename.startswith(name): - try: - os.remove(filename) - except Exception as e: - AppLogger.logger.error(f"Error removing file {filename}: {e}") +@app.errorhandler(CSRFError) +def handle_csrf_error(e): + return jsonify({'success': False, 'errors': ['Security token missing or expired. Please reload the page.']}), 400 @app.route('/') def index(): - before_request_index() return render_template('index.html') @app.route('/generate', methods=['POST']) def generate(): - text = request.form['text'] - size = int(request.form['size']) - image = request.files['image'] if 'image' in request.files else None - color = request.form['color'] if 'color' in request.form else None + text = request.form.get('text', '') + try: + size = int(request.form.get('size', '')) + except TypeError, ValueError: + return jsonify({'success': False, 'errors': ['Size must be a valid integer.']}), 400 + raw_image = request.files.get('image') + image = raw_image if (raw_image and raw_image.filename) else None + color = request.form.get('color') or None + bg_color = request.form.get('bg_color') or '#ffffff' + micro = request.form.get('micro') == 'on' generator = QRCodeGenerator() - success, errors = generator.generate_qr(text, size, image, color) + success, errors = generator.generate_qr(text, size, image, color, bg_color, micro) - if success: - return jsonify({'success': True}) - else: + if not success: return jsonify({'success': False, 'errors': errors}) + token = store.put( + { + 'qr': generator.qr_png, + 'artistic': generator.artistic_png, + 'artistic_ext': generator.artistic_ext, + } + ) + return jsonify({'success': True, 'id': token}) + @app.route('/result') def result(): - qr_with_image_exists = any(filename.startswith('qr_with_image') for filename in os.listdir('.')) - return render_template('result.html', qr_with_image_exists=qr_with_image_exists) + token = request.args.get('id', '') + payload = store.get(token) + if payload is None: + abort(404) + return render_template( + 'result.html', + token=token, + qr_with_image_exists=payload['artistic'] is not None, + ) @app.route('/download') def download(): - return send_file('qr.png', as_attachment=True) + token = request.args.get('id', '') + payload = store.get(token) + if payload is None: + abort(404) + return send_file( + io.BytesIO(payload['qr']), + mimetype='image/png', + as_attachment=False, + download_name='qr.png', + ) @app.route('/download_with_image') def download_with_image(): - for filename in os.listdir('.'): - if filename.startswith('qr_with_image'): - return send_file(filename, as_attachment=True) - - return "File not found", 404 + token = request.args.get('id', '') + payload = store.get(token) + if payload is None or payload['artistic'] is None: + abort(404) + ext = payload['artistic_ext'] + mimetype = 'image/gif' if ext == 'gif' else 'image/png' + return send_file( + io.BytesIO(payload['artistic']), + mimetype=mimetype, + as_attachment=False, + download_name=f'qr_with_image.{ext}', + ) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) + debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true' + app.run(debug=debug, host='0.0.0.0', port=5000) diff --git a/docker-compose-traefik.yml b/docker-compose-traefik.yml new file mode 100644 index 0000000..b624059 --- /dev/null +++ b/docker-compose-traefik.yml @@ -0,0 +1,15 @@ +services: + web: + build: . + container_name: qr_web + networks: + - proxy + labels: + - traefik.enable=true + - traefik.docker.network=proxy + - traefik.http.routers.qr.rule=Host(`qrcode.home`) + - traefik.http.routers.qr.entrypoints=web + - traefik.http.services.qr.loadbalancer.server.port=5000 +networks: + proxy: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 84566ca..3b518ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,4 +5,4 @@ services: ports: - "5000:5000" volumes: - - .:/app + - .:/app # dev-mode live reload; remove for production diff --git a/ingress.yaml b/ingress.yaml index 449f2f5..9eb94b8 100644 --- a/ingress.yaml +++ b/ingress.yaml @@ -2,9 +2,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-service - annotations: - nginx.ingress.kubernetes.io/ingress-class: "nginx" spec: + ingressClassName: nginx rules: - http: paths: @@ -14,4 +13,4 @@ spec: service: name: qrgen port: - number: 5000 \ No newline at end of file + number: 5000 diff --git a/justfile b/justfile new file mode 100644 index 0000000..175561f --- /dev/null +++ b/justfile @@ -0,0 +1,81 @@ +set shell := ["pwsh", "-NoLogo", "-Command"] + +server := "tartuffe@192.168.0.150" +server_dir := "~/qr_generator" + +# List all available recipes +help: + @just --list + +# Install all dependencies including dev group +install: + uv sync + +# Run all linters (ruff format --check, ruff check, ty, codespell, bandit) +lint: + uv run ruff format --check . + uv run ruff check . + uv run ty check + uv run codespell + uv run bandit -r . -c pyproject.toml -q + +# Format code and auto-fix lint issues +format: + uv run ruff format . + uv run ruff check --fix . + +# Start Flask development server +runserver: + uv run python app.py + +# Run all tests excluding slow +test: + uv run pytest -m "not slow" + +# Run only unit tests +test-unit: + uv run pytest -m unit -v + +# Run only integration tests +test-integration: + uv run pytest -m integration -v + +# Start Docker services — dev (plain docker-compose) +docker-up: + docker compose up -d --build + +# Stop Docker services — dev +docker-down: + docker compose down + +# Start Docker services with Traefik — local test +docker-up-traefik: + docker compose -f docker-compose-traefik.yml up -d --build + +# Stop Docker services with Traefik — local test +docker-down-traefik: + docker compose -f docker-compose-traefik.yml down + +# Deploy to home server: git pull + make up +deploy: + ssh {{server}} "cd {{server_dir}} && git pull && make up" + +# Stop deployment on home server +deploy-down: + ssh {{server}} "cd {{server_dir}} && make down" + +# Stream live logs from home server +deploy-logs: + ssh {{server}} "docker logs -f qr_web" + +# Run pre-commit hooks on all files +git-precommit: + uv run pre-commit run --all-files + +# Commit with pre-commit checks and commitizen +commit: + uv run pre-commit run && uv run cz commit + +# Bump version using commitizen +git-bump: + uv run cz bump diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b9cbcf5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[project] +name = "qr-generator" +version = "0.1.0" +description = "Flask QR code generator with artistic image overlay" +requires-python = ">=3.14" +dependencies = [ + "Flask>=3.1.3", + "Flask-WTF>=1.2.1", + "pillow>=11.0", + "qrcode-artistic>=3.0.2", + "segno>=1.6.1", +] + +[dependency-groups] +dev = [ + "pre-commit>=4.2.0", + "ruff>=0.11.6", + "ty>=0.0.20", + "codespell>=2.4.1", + "bandit[toml]", + "pytest>=9.0.2", + "pytest-flask", + "pytest-cov>=6.1.1", + "commitizen>=4.13.9", +] + +[tool.uv] +package = false + +[tool.ruff] +line-length = 124 + +[tool.ruff.lint] +select = ["E", "F", "UP", "B", "SIM", "I", "RUF"] +ignore = ["RUF012"] + +[tool.ruff.lint.isort] +known-first-party = ["utils", "app"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["F401", "F841"] +"test_*.py" = ["F401", "F841"] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +docstring-code-format = true +docstring-code-line-length = 124 + +[tool.pytest.ini_options] +pythonpath = ["."] +python_files = ["test_*.py"] +addopts = "--strict-markers" +markers = [ + "unit: fast isolated tests with no IO", + "integration: tests that touch routes or the store", + "slow: tests that take more than a few seconds", +] + +[tool.ty.environment] +python-version = "3.14" +extra-paths = ["."] + +[tool.codespell] +skip = "uv.lock,./static,./.venv" +builtin = "clear" +quiet-level = 3 + +[tool.bandit] +exclude_dirs = [".venv", "tests"] +skips = ["B104"] + +[tool.coverage.run] +source = ["utils", "app"] + +[[tool.ty.overrides]] +include = ["utils/qr_generator.py"] +rules = { call-non-callable = "ignore" } + +[tool.commitizen] +name = "cz_conventional_commits" +version = "0.1.0" +tag_format = "v$version" +version_files = ["pyproject.toml:version"] diff --git a/qrgen-deploy.yaml b/qrgen-deploy.yaml index d553048..fc3d668 100644 --- a/qrgen-deploy.yaml +++ b/qrgen-deploy.yaml @@ -18,7 +18,7 @@ spec: containers: - name: qrgen image: qr_generator-web:latest - imagePullPolicy: Never + imagePullPolicy: IfNotPresent ports: - containerPort: 5000 protocol: TCP diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f3a09e0..0000000 Binary files a/requirements.txt and /dev/null differ diff --git a/static/js/color_contrast_warning.js b/static/js/color_contrast_warning.js new file mode 100644 index 0000000..6276c78 --- /dev/null +++ b/static/js/color_contrast_warning.js @@ -0,0 +1,40 @@ +function hexToRgb(hex) { + const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return m ? { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) } : null; +} + +function relativeLuminance(r, g, b) { + return [r, g, b].reduce((acc, c, i) => { + c /= 255; + const lin = c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + return acc + lin * [0.2126, 0.7152, 0.0722][i]; + }, 0); +} + +function contrastRatio(hex1, hex2) { + const c1 = hexToRgb(hex1); + const c2 = hexToRgb(hex2); + if (!c1 || !c2) return 21; + const l1 = relativeLuminance(c1.r, c1.g, c1.b); + const l2 = relativeLuminance(c2.r, c2.g, c2.b); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +const darkInput = document.getElementById('color'); +const lightInput = document.getElementById('bg_color'); +const contrastWarning = document.getElementById('contrast-warning'); +const imageInput = document.getElementById('image'); +const imageWarning = document.getElementById('image-warning'); + +function updateContrastWarning() { + contrastWarning.classList.toggle('d-none', contrastRatio(darkInput.value, lightInput.value) >= 3); +} + +darkInput.addEventListener('input', updateContrastWarning); +lightInput.addEventListener('input', updateContrastWarning); + +imageInput.addEventListener('change', () => { + imageWarning.classList.toggle('d-none', imageInput.files.length === 0); +}); diff --git a/static/js/form_remove_image.js b/static/js/form_remove_image.js index 930e342..ec5da56 100644 --- a/static/js/form_remove_image.js +++ b/static/js/form_remove_image.js @@ -1,5 +1,5 @@ function clearFileInput() { var fileInput = document.getElementById('image'); - fileInput.value = ''; + fileInput.dispatchEvent(new Event('change')); } diff --git a/static/js/input_errors_modal.js b/static/js/input_errors_modal.js index 1e3e690..42e015e 100644 --- a/static/js/input_errors_modal.js +++ b/static/js/input_errors_modal.js @@ -1,29 +1,34 @@ -$(document).ready(function(){ - $('#generate-form').submit(function(event){ - event.preventDefault(); +document.addEventListener('DOMContentLoaded', function () { + var errorModal = new bootstrap.Modal(document.getElementById('errorModal')); + + function showErrors(errors) { + var body = document.getElementById('errorModalBody'); + body.innerHTML = ''; + (errors && errors.length ? errors : ['An unexpected error occurred.']) + .forEach(function (error) { + var p = document.createElement('p'); + p.textContent = error; + body.appendChild(p); + }); + errorModal.show(); + } - var formData = new FormData($(this)[0]); + document.getElementById('generate-form').addEventListener('submit', function (event) { + event.preventDefault(); - $.ajax({ - type: 'POST', - url: '/generate', - data: formData, - processData: false, - contentType: false, - success: function(response) { - if (response.success) { - window.location.href = '/result'; - } else { - $('#errorModalBody').empty(); - response.errors.forEach(function(error) { - $('#errorModalBody').append('

' + error + '

'); - }); - $('#errorModal').modal('show'); - } - }, - error: function(xhr, status, error) { - console.error(xhr.responseText); - } - }); + fetch('/generate', { method: 'POST', body: new FormData(this) }) + .then(function (response) { + return response.json().then(function (data) { + if (data.success) { + window.location.href = '/result?id=' + encodeURIComponent(data.id); + } else { + showErrors(data.errors); + } + }); + }) + .catch(function (err) { + console.error(err); + showErrors(null); + }); }); }); diff --git a/templates/index.html b/templates/index.html index 1f4d82d..e8a7a00 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,43 +5,76 @@ QR Code Generator - + +
-

QR Code Generator

+

QR Code Generator

-
-
+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ QR readability depends on image complexity — make sure the three corner squares remain clearly visible. +
+
-
-
+
+
+ + +
+
+ + +
+
-
-
-
+
+ Low contrast between foreground and background colors may make the QR code unreadable. +
-
-
- +
+ + +
+ +
-