/
marsela13
/
QR_Code_Generator
Обзор
Документация
Войти
/
marsela13
/
QR_Code_Generator
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
421 строка
21 KB
marsela13
Техническая документация | Web версия
28 апр 2026, 21:30
Верифицирован
28 апр 2026, 21:30
631f09c
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>QR Generator — Документация</title> <style> :root { --orange: #e65100; --gray-100: #f5f5f5; --gray-200: #eee; --gray-300: #e0e0e0; --gray-700: #616161; --gray-900: #212121; --mono: 'Consolas', 'Monaco', monospace; --radius: 6px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 15px; line-height: 1.6; color: var(--gray-900); background: var(--gray-100); } header { background: #fff; border-bottom: 1px solid var(--gray-300); padding: 0 1.5rem; height: 56px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 10; } header h1 { font-size: 1.1rem; } header span { font-size: 0.8rem; color: var(--gray-700); } nav { display: flex; gap: 0.25rem; } nav a { padding: 0.35rem 0.9rem; border-radius: var(--radius); font-size: 0.85rem; color: var(--gray-700); text-decoration: none; cursor: pointer; } nav a:hover { background: var(--gray-200); } nav a.active { background: var(--orange); color: #fff; } .container { max-width: 900px; margin: 0 auto; padding: 1.5rem; } .card { background: #fff; border: 1px solid var(--gray-300); border-radius: var(--radius); padding: 1.5rem; margin-bottom: 1rem; } .card h2 { font-size: 1.15rem; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--orange); } .card h3 { font-size: 0.95rem; color: var(--orange); margin: 1rem 0 0.35rem; } .card p { color: var(--gray-700); margin-bottom: 0.5rem; } .card ul { padding-left: 1.25rem; color: var(--gray-700); } .card li { margin-bottom: 0.25rem; } table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin: 0.5rem 0; } th, td { border: 1px solid var(--gray-300); padding: 0.5rem 0.75rem; text-align: left; } th { background: var(--gray-100); font-weight: 600; } .file-tabs { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 1rem; } .file-tab { padding: 0.3rem 0.8rem; border: 1px solid var(--gray-300); border-radius: var(--radius); font-size: 0.82rem; font-family: var(--mono); color: var(--gray-700); cursor: pointer; background: #fff; } .file-tab:hover { border-color: var(--orange); color: var(--orange); } .file-tab.active { background: var(--orange); color: #fff; border-color: var(--orange); } .code-box { background: #1e1e1e; border-radius: var(--radius); overflow: hidden; margin-bottom: 1rem; } .code-box-header { background: #2d2d2d; padding: 0.5rem 1rem; display: flex; justify-content: space-between; align-items: center; } .code-box-header span { font-family: var(--mono); font-size: 0.82rem; color: #ffab40; } .copy-btn { background: #3d3d3d; color: #ccc; border: 1px solid #555; padding: 0.2rem 0.7rem; border-radius: 4px; cursor: pointer; font-size: 0.78rem; } .copy-btn:hover { background: var(--orange); border-color: var(--orange); color: #fff; } .code-box pre { padding: 1rem; color: #d4d4d4; font-family: var(--mono); font-size: 0.82rem; line-height: 1.5; overflow-x: auto; max-height: 500px; overflow-y: auto; white-space: pre; } code { font-family: var(--mono); font-size: 0.85em; background: var(--gray-200); padding: 0.1rem 0.35rem; border-radius: 3px; color: var(--orange); } .badge { display: inline-block; background: var(--gray-100); border: 1px solid var(--gray-300); border-radius: 20px; padding: 0.2rem 0.7rem; font-size: 0.82rem; font-family: var(--mono); margin: 0.2rem; } footer { text-align: center; padding: 1rem; font-size: 0.78rem; color: var(--gray-700); border-top: 1px solid var(--gray-300); margin-top: 1rem; } </style> </head> <body> <header> <h1>QR Generator <span>— Техническая документация</span></h1> <nav id="nav"> <a class="active" data-page="main">Главная</a> <a data-page="app">App</a> <a data-page="auth">Auth</a> <a data-page="history">History</a> <a data-page="tests">Tests</a> </nav> </header> <div class="container" id="root"></div> <footer>Flask 3.1 · SQLAlchemy 2.0 · Python 3.10+ · QR Generator</footer> <script> const pages = { main: () => ` <div class="card"> <h2>О проекте</h2> <p>Веб-приложение для генерации QR-кодов с историей для зарегистрированных пользователей. Изображения формируются в памяти (BytesIO) и передаются клиенту без записи на диск.</p> <h3>Возможности</h3> <ul> <li>Генерация QR-кода по тексту или URL</li> <li>Выбор размера (200–600 px) и уровня коррекции ошибок (L / M / Q / H)</li> <li>Встраивание логотипа в центр QR-кода (PNG / JPG)</li> <li>Скачивание результата в формате PNG</li> <li>История генераций для авторизованных пользователей</li> </ul> </div> <div class="card"> <h2>Технологический стек</h2> <div> <span class="badge">Flask 3.1</span> <span class="badge">Flask-Login 0.6</span> <span class="badge">SQLAlchemy 2.0</span> <span class="badge">SQLite</span> <span class="badge">bcrypt</span> <span class="badge">qrcode 8.2</span> <span class="badge">Pillow 12</span> <span class="badge">pytest</span> </div> </div> <div class="card"> <h2>URL-адреса</h2> <table> <tr><th>Метод</th><th>URL</th><th>Описание</th><th>Авторизация</th></tr> <tr><td>GET</td><td><code>/</code></td><td>Главная страница</td><td>Нет</td></tr> <tr><td>POST</td><td><code>/generate</code></td><td>Генерация QR (JSON)</td><td>Нет</td></tr> <tr><td>POST</td><td><code>/generate-with-logo</code></td><td>Генерация с логотипом (multipart)</td><td>Нет</td></tr> <tr><td>GET</td><td><code>/history/</code></td><td>Страница истории</td><td>Да</td></tr> <tr><td>GET</td><td><code>/history/api</code></td><td>JSON-список истории</td><td>Да</td></tr> <tr><td>GET</td><td><code>/history/download/<id></code></td><td>Скачать QR из истории</td><td>Да</td></tr> <tr><td>DELETE</td><td><code>/history/<id></code></td><td>Удалить запись</td><td>Да</td></tr> <tr><td>GET/POST</td><td><code>/register</code></td><td>Регистрация</td><td>Нет</td></tr> <tr><td>GET/POST</td><td><code>/login</code></td><td>Вход</td><td>Нет</td></tr> <tr><td>GET</td><td><code>/logout</code></td><td>Выход</td><td>Да</td></tr> </table> </div>`, app: () => { const files = { '__init__.py': { code: `def create_app(): app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key') app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get( 'DATABASE_URL', f'sqlite:///{basedir}/app.db' ) db.init_app(app) login_manager.init_app(app) app.register_blueprint(auth_bp) app.register_blueprint(main_bp) app.register_blueprint(history_bp) with app.app_context(): db.create_all() return app`, desc: 'Фабрика приложения. Инициализирует расширения (SQLAlchemy, Flask-Login), регистрирует три Blueprint и создаёт таблицы БД при старте.' }, 'models.py': { code: `class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) qr_codes = db.relationship('GeneratedQR', back_populates='owner', cascade='all, delete-orphan') class GeneratedQR(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) data = db.Column(db.Text, nullable=False) size = db.Column(db.Integer, default=300) error_correction = db.Column(db.String(1), default='M') has_logo = db.Column(db.Boolean, default=False) image_blob = db.Column(db.LargeBinary, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow)`, desc: 'Две ORM-модели. User — аккаунт пользователя с bcrypt-хешем пароля. GeneratedQR — запись о генерации: входные данные, параметры и PNG-изображение как BLOB.' }, 'qr_service.py': { code: `SIZE_MIN, SIZE_MAX = 200, 600 LOGO_MAX_RATIO = 0.30 def generate_qr_bytes(data, size=300, error_correction='M', logo_bytes=None): if not data or not data.strip(): raise ValueError("Введите данные для QR-кода") if len(data) > 500: raise ValueError("Текст не должен превышать 500 символов") size = max(SIZE_MIN, min(SIZE_MAX, size)) ec_level = ERROR_CORRECTION_MAP.get(error_correction.upper()) if ec_level is None: raise ValueError(f"Недопустимый уровень коррекции: {error_correction!r}") qr = qrcode.QRCode(version=None, error_correction=ec_level, box_size=10, border=4) qr.add_data(data) qr.make(fit=True) img = qr.make_image(fill_color='black', back_color='white').convert('RGB') img = img.resize((size, size), Image.LANCZOS) if logo_bytes: img = _embed_logo(img, logo_bytes) # вставка логотипа в центр buffer = io.BytesIO() img.save(buffer, format='PNG') return buffer.getvalue()`, desc: 'Сервис генерации. Строит QR-матрицу через qrcode, масштабирует через Pillow, опционально вставляет логотип (не более 30% размера). Всё в памяти — диск не используется.' }, 'database.py': { code: `from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy()`, desc: 'Единственный экземпляр SQLAlchemy, разделяемый между всеми модулями приложения.' }, 'auth_utils.py': { code: `import bcrypt def hash_password(plain: str) -> str: salt = bcrypt.gensalt() return bcrypt.hashpw(plain.encode('utf-8'), salt).decode('utf-8') def check_password(plain: str, hashed: str) -> bool: return bcrypt.checkpw(plain.encode('utf-8'), hashed.encode('utf-8'))`, desc: 'Утилиты для работы с паролями. hash_password() создаёт bcrypt-хеш, check_password() проверяет пароль при входе.' } }; const activeFile = window._appFile || '__init__.py'; window._appFile = activeFile; return \` <div class="card"> <h2>Модуль App — основной пакет приложения</h2> <p>Содержит фабрику приложения, ORM-модели, сервис генерации QR-кодов и вспомогательные утилиты.</p> <div class="file-tabs"> \${Object.keys(files).map(f => \`<span class="file-tab\${f === activeFile ? ' active' : ''}" data-file="\${f}" data-module="app">\${f}</span>\` ).join('')} </div> <div class="code-box"> <div class="code-box-header"> <span>📄 \${activeFile}</span> <button class="copy-btn">📋 Копировать</button> </div> <pre>\${escHtml(files[activeFile].code)}</pre> </div> <div class="card" style="background:#f8f9fa;margin:0"> <h3>Пояснение</h3> <p>\${files[activeFile].desc}</p> </div> </div>\`; }, auth: () => { const files = { 'auth.py': { code: `@auth_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') user = User.query.filter_by(username=username).first() if user and check_password(password, user.password_hash): login_user(user) return redirect(request.args.get('next') or url_for('main.index')) flash('Неверное имя пользователя или пароль', 'error') return render_template('login.html') @auth_bp.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': # Валидация: длина username 3-50, пароль >= 6, совпадение паролей, # уникальность username user = User(username=username, password_hash=hash_password(password)) db.session.add(user) db.session.commit() login_user(user) return redirect(url_for('main.index')) return render_template('register.html')`, desc: 'Blueprint аутентификации. Обрабатывает вход (/login), регистрацию (/register) и выход (/logout). Валидирует данные формы, использует Flask-Login для управления сессией.' } }; const activeFile = 'auth.py'; return \` <div class="card"> <h2>Модуль Auth — аутентификация</h2> <p>Blueprint для регистрации, входа и выхода пользователей. Использует Flask-Login и bcrypt.</p> <div class="code-box"> <div class="code-box-header"> <span>📄 auth.py</span> <button class="copy-btn">📋 Копировать</button> </div> <pre>\${escHtml(files[activeFile].code)}</pre> </div> <div class="card" style="background:#f8f9fa;margin:0"> <h3>Пояснение</h3> <p>\${files[activeFile].desc}</p> <h3>Правила валидации</h3> <ul> <li>Имя пользователя: от 3 до 50 символов, уникальное</li> <li>Пароль: минимум 6 символов</li> <li>Подтверждение пароля должно совпадать</li> </ul> </div> </div>\`; }, history: () => { const code = \`history_bp = Blueprint('history', __name__, url_prefix='/history') @history_bp.route('/') @login_required def view_history(): page = request.args.get('page', 1, type=int) pagination = GeneratedQR.query\\ .filter_by(user_id=current_user.id)\\ .order_by(GeneratedQR.created_at.desc())\\ .paginate(page=page, per_page=20, error_out=False) return render_template('history.html', qr_codes=pagination.items, pagination=pagination) @history_bp.route('/download/<int:qr_id>') @login_required def download_qr(qr_id): record = GeneratedQR.query.filter_by( id=qr_id, user_id=current_user.id).first_or_404() return send_file(BytesIO(record.image_blob), mimetype='image/png', as_attachment=True, download_name=f'qr_code_{qr_id}.png') @history_bp.route('/delete/<int:qr_id>', methods=['POST']) @login_required def delete_qr(qr_id): record = GeneratedQR.query.filter_by( id=qr_id, user_id=current_user.id).first_or_404() db.session.delete(record) db.session.commit() return redirect(url_for('history.view_history'))\`; return \` <div class="card"> <h2>Модуль History — история QR-кодов</h2> <p>Blueprint с префиксом <code>/history</code>. Все маршруты защищены декоратором <code>@login_required</code>.</p> <div class="code-box"> <div class="code-box-header"> <span>📄 history.py</span> <button class="copy-btn">📋 Копировать</button> </div> <pre>\${escHtml(code)}</pre> </div> <div class="card" style="background:#f8f9fa;margin:0"> <h3>Маршруты</h3> <table> <tr><th>URL</th><th>Описание</th></tr> <tr><td><code>/history/</code></td><td>Список QR-кодов пользователя (пагинация по 20)</td></tr> <tr><td><code>/history/download/<id></code></td><td>Скачать PNG из BLOB в БД</td></tr> <tr><td><code>/history/delete/<id></code></td><td>Удалить запись (только своя)</td></tr> </table> <h3>Безопасность</h3> <p>Каждый запрос фильтруется по <code>user_id=current_user.id</code> — пользователь не может получить чужие QR-коды. При отсутствии записи возвращается 404.</p> </div> </div>\`; }, tests: () => \` <div class="card"> <h2>Тесты</h2> <p>Проект покрыт юнит- и интеграционными тестами на pytest. Каждый тест изолирован через транзакцию с откатом.</p> <h3>Структура</h3> <table> <tr><th>Файл</th><th>Тип</th><th>Что тестирует</th></tr> <tr><td><code>test_qr_service.py</code></td><td>Юнит</td><td>Сервис генерации: валидные данные, граничные значения, логотип, ошибки</td></tr> <tr><td><code>test_routes.py</code></td><td>Интеграционный</td><td>Все маршруты: генерация, аутентификация, история</td></tr> <tr><td><code>conftest.py</code></td><td>Фикстуры</td><td>app, client, registered_user, auth_client, db_session</td></tr> </table> <h3>Запуск</h3> <div class="code-box"> <div class="code-box-header"><span>bash</span></div> <pre>pytest project/tests/ -v</pre> </div> <h3>Ключевые сценарии test_qr_service.py</h3> <ul> <li>Генерация по URL, тексту, кириллице</li> <li>Все уровни коррекции ошибок (L, M, Q, H)</li> <li>Все допустимые размеры (200, 300, 400, 500, 600 px)</li> <li>Clamping размера за пределами диапазона</li> <li>Встраивание логотипа — результат отличается от QR без логотипа</li> <li>Ошибки: пустая строка, пробелы, >500 символов, неверный уровень коррекции</li> </ul> <h3>Ключевые сценарии test_routes.py</h3> <ul> <li>POST /generate возвращает валидный PNG</li> <li>Авторизованный пользователь — запись сохраняется в БД</li> <li>Регистрация: дубль username, короткий пароль, несовпадение паролей</li> <li>История: доступ только авторизованным, скачивание и удаление своих записей</li> <li>404 при обращении к чужим или несуществующим записям</li> </ul> </div>\` }; function escHtml(t) { return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } function render(page) { document.getElementById('root').innerHTML = pages[page](); document.querySelectorAll('#nav a').forEach(a => { a.classList.toggle('active', a.dataset.page === page); }); // file tabs document.querySelectorAll('.file-tab').forEach(tab => { tab.addEventListener('click', () => { window._appFile = tab.dataset.file; render('app'); }); }); // copy buttons document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => { const code = btn.closest('.code-box').querySelector('pre').innerText; navigator.clipboard.writeText(code).then(() => { btn.textContent = '✅ Скопировано'; setTimeout(() => btn.textContent = '📋 Копировать', 2000); }); }); }); window.scrollTo({ top: 0, behavior: 'smooth' }); } document.querySelectorAll('#nav a').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); render(a.dataset.page); }); }); render('main'); </script> </body> </html>