final app for testing and deployment
This commit is contained in:
@@ -4,8 +4,9 @@ Utility modules for QR Code Manager
|
||||
|
||||
from .auth import init_admin, login_required, verify_password, get_admin_credentials
|
||||
from .qr_generator import QRCodeGenerator
|
||||
from .link_manager import LinkPageManager, link_pages_db
|
||||
from .data_manager import QRDataManager, qr_codes_db
|
||||
from .link_manager import LinkPageManager
|
||||
from .data_manager import QRDataManager
|
||||
from .url_shortener import URLShortener
|
||||
|
||||
__all__ = [
|
||||
'init_admin',
|
||||
@@ -14,7 +15,6 @@ __all__ = [
|
||||
'get_admin_credentials',
|
||||
'QRCodeGenerator',
|
||||
'LinkPageManager',
|
||||
'link_pages_db',
|
||||
'QRDataManager',
|
||||
'qr_codes_db'
|
||||
'URLShortener'
|
||||
]
|
||||
|
||||
@@ -3,14 +3,39 @@ Data storage utilities for QR codes
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# In-memory storage for QR codes (in production, use a database)
|
||||
qr_codes_db = {}
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
QR_CODES_FILE = os.path.join(DATA_DIR, 'qr_codes.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class QRDataManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
|
||||
def _load_qr_codes(self):
|
||||
"""Load QR codes from JSON file"""
|
||||
try:
|
||||
if os.path.exists(QR_CODES_FILE):
|
||||
with open(QR_CODES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading QR codes: {e}")
|
||||
return {}
|
||||
|
||||
def _save_qr_codes(self):
|
||||
"""Save QR codes to JSON file"""
|
||||
try:
|
||||
with open(QR_CODES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.qr_codes_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving QR codes: {e}")
|
||||
|
||||
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
|
||||
"""Save QR code record to database"""
|
||||
@@ -27,32 +52,45 @@ class QRDataManager:
|
||||
if page_id:
|
||||
qr_record['page_id'] = page_id
|
||||
|
||||
qr_codes_db[qr_id] = qr_record
|
||||
self.qr_codes_db[qr_id] = qr_record
|
||||
self._save_qr_codes() # Persist to file
|
||||
return qr_id
|
||||
|
||||
def get_qr_record(self, qr_id):
|
||||
"""Get QR code record"""
|
||||
return qr_codes_db.get(qr_id)
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
return self.qr_codes_db.get(qr_id)
|
||||
|
||||
def get_qr_code(self, qr_id):
|
||||
"""Get QR code record (alias for compatibility)"""
|
||||
return self.get_qr_record(qr_id)
|
||||
|
||||
def delete_qr_record(self, qr_id):
|
||||
"""Delete QR code record"""
|
||||
if qr_id in qr_codes_db:
|
||||
del qr_codes_db[qr_id]
|
||||
if qr_id in self.qr_codes_db:
|
||||
del self.qr_codes_db[qr_id]
|
||||
self._save_qr_codes() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_qr_codes(self):
|
||||
"""List all QR codes"""
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
qr_list = []
|
||||
for qr_id, qr_data in qr_codes_db.items():
|
||||
for qr_id, qr_data in self.qr_codes_db.items():
|
||||
qr_list.append({
|
||||
'id': qr_id,
|
||||
'type': qr_data['type'],
|
||||
'created_at': qr_data['created_at'],
|
||||
'preview': f'data:image/png;base64,{qr_data["image_data"]}'
|
||||
'preview': f'data:image/png;base64,{qr_data["image_data"]}',
|
||||
'page_id': qr_data.get('page_id') # Include page_id if it exists
|
||||
})
|
||||
return qr_list
|
||||
|
||||
def qr_exists(self, qr_id):
|
||||
"""Check if QR code exists"""
|
||||
return qr_id in qr_codes_db
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
return qr_id in self.qr_codes_db
|
||||
|
||||
@@ -3,14 +3,41 @@ Dynamic Link Page Manager utilities
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from .url_shortener import URLShortener
|
||||
|
||||
# In-memory storage for dynamic link pages (in production, use a database)
|
||||
link_pages_db = {}
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
LINK_PAGES_FILE = os.path.join(DATA_DIR, 'link_pages.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class LinkPageManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.url_shortener = URLShortener()
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
|
||||
def _load_link_pages(self):
|
||||
"""Load link pages from JSON file"""
|
||||
try:
|
||||
if os.path.exists(LINK_PAGES_FILE):
|
||||
with open(LINK_PAGES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading link pages: {e}")
|
||||
return {}
|
||||
|
||||
def _save_link_pages(self):
|
||||
"""Save link pages to JSON file"""
|
||||
try:
|
||||
with open(LINK_PAGES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.link_pages_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving link pages: {e}")
|
||||
|
||||
def create_link_page(self, title="My Links", description="Collection of useful links"):
|
||||
"""Create a new dynamic link page"""
|
||||
@@ -22,34 +49,70 @@ class LinkPageManager:
|
||||
'links': [],
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'updated_at': datetime.now().isoformat(),
|
||||
'view_count': 0
|
||||
'view_count': 0,
|
||||
'short_url': None, # Will be set when short URL is created
|
||||
'short_code': None
|
||||
}
|
||||
link_pages_db[page_id] = page_data
|
||||
self.link_pages_db[page_id] = page_data
|
||||
self._save_link_pages() # Persist to file
|
||||
return page_id
|
||||
|
||||
def add_link(self, page_id, title, url, description=""):
|
||||
"""Add a link to a page"""
|
||||
if page_id not in link_pages_db:
|
||||
def set_page_short_url(self, page_id, short_url, short_code):
|
||||
"""Set the short URL for a link page"""
|
||||
if page_id in self.link_pages_db:
|
||||
self.link_pages_db[page_id]['short_url'] = short_url
|
||||
self.link_pages_db[page_id]['short_code'] = short_code
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_link(self, page_id, title, url, description="", enable_shortener=False, custom_short_code=None):
|
||||
"""Add a link to a page with optional URL shortening"""
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
# Ensure URL has protocol
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = f'https://{url}'
|
||||
|
||||
# Create the link data
|
||||
link_data = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'title': title,
|
||||
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
|
||||
'url': url,
|
||||
'description': description,
|
||||
'created_at': datetime.now().isoformat()
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'short_url': None,
|
||||
'short_code': None,
|
||||
'clicks': 0
|
||||
}
|
||||
|
||||
link_pages_db[page_id]['links'].append(link_data)
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
# Generate short URL if enabled
|
||||
if enable_shortener:
|
||||
try:
|
||||
short_result = self.url_shortener.create_short_url(
|
||||
url,
|
||||
custom_code=custom_short_code,
|
||||
title=title
|
||||
)
|
||||
link_data['short_url'] = short_result['short_url']
|
||||
link_data['short_code'] = short_result['short_code']
|
||||
except Exception as e:
|
||||
# If shortening fails, continue without it
|
||||
print(f"URL shortening failed: {e}")
|
||||
|
||||
self.link_pages_db[page_id]['links'].append(link_data)
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
|
||||
def update_link(self, page_id, link_id, title=None, url=None, description=None):
|
||||
"""Update a specific link"""
|
||||
if page_id not in link_pages_db:
|
||||
def update_link(self, page_id, link_id, title=None, url=None, description=None, enable_shortener=None, custom_short_code=None):
|
||||
"""Update a specific link with optional URL shortening"""
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
for link in link_pages_db[page_id]['links']:
|
||||
for link in self.link_pages_db[page_id]['links']:
|
||||
if link['id'] == link_id:
|
||||
if title is not None:
|
||||
link['title'] = title
|
||||
@@ -58,29 +121,79 @@ class LinkPageManager:
|
||||
if description is not None:
|
||||
link['description'] = description
|
||||
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
# Handle URL shortening update
|
||||
if enable_shortener is not None:
|
||||
if enable_shortener and not link.get('short_url'):
|
||||
# Create new short URL
|
||||
try:
|
||||
short_result = self.url_shortener.create_short_url(
|
||||
link['url'],
|
||||
custom_code=custom_short_code,
|
||||
title=link['title']
|
||||
)
|
||||
link['short_url'] = short_result['short_url']
|
||||
link['short_code'] = short_result['short_code']
|
||||
except Exception as e:
|
||||
print(f"URL shortening failed: {e}")
|
||||
elif not enable_shortener and link.get('short_code'):
|
||||
# Remove short URL
|
||||
if link.get('short_code'):
|
||||
self.url_shortener.delete_url(link['short_code'])
|
||||
link['short_url'] = None
|
||||
link['short_code'] = None
|
||||
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def delete_link(self, page_id, link_id):
|
||||
"""Delete a specific link"""
|
||||
if page_id not in link_pages_db:
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
links = link_pages_db[page_id]['links']
|
||||
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
links = self.link_pages_db[page_id]['links']
|
||||
for link in links:
|
||||
if link['id'] == link_id and link.get('short_code'):
|
||||
# Delete the short URL if it exists
|
||||
self.url_shortener.delete_url(link['short_code'])
|
||||
|
||||
self.link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
|
||||
def increment_view_count(self, page_id):
|
||||
"""Increment view count for a page"""
|
||||
if page_id in link_pages_db:
|
||||
link_pages_db[page_id]['view_count'] += 1
|
||||
if page_id in self.link_pages_db:
|
||||
self.link_pages_db[page_id]['view_count'] += 1
|
||||
self._save_link_pages() # Persist to file
|
||||
|
||||
def get_page(self, page_id):
|
||||
"""Get page data"""
|
||||
return link_pages_db.get(page_id)
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
return self.link_pages_db.get(page_id)
|
||||
|
||||
def page_exists(self, page_id):
|
||||
"""Check if page exists"""
|
||||
return page_id in link_pages_db
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
return page_id in self.link_pages_db
|
||||
|
||||
# URL Shortener management methods
|
||||
def create_standalone_short_url(self, url, title="", custom_code=None):
|
||||
"""Create a standalone short URL (not tied to a link page)"""
|
||||
return self.url_shortener.create_short_url(url, custom_code, title)
|
||||
|
||||
def get_short_url_stats(self, short_code):
|
||||
"""Get statistics for a short URL"""
|
||||
return self.url_shortener.get_url_stats(short_code)
|
||||
|
||||
def list_all_short_urls(self):
|
||||
"""List all short URLs in the system"""
|
||||
return self.url_shortener.list_urls()
|
||||
|
||||
def resolve_short_url(self, short_code):
|
||||
"""Resolve a short URL to its original URL"""
|
||||
return self.url_shortener.get_original_url(short_code)
|
||||
|
||||
@@ -6,7 +6,9 @@ import os
|
||||
import qrcode
|
||||
from qrcode.image.styledpil import StyledPilImage
|
||||
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
|
||||
from qrcode.image.svg import SvgPathImage, SvgFragmentImage, SvgFillImage
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
class QRCodeGenerator:
|
||||
def __init__(self):
|
||||
@@ -19,8 +21,8 @@ class QRCodeGenerator:
|
||||
'style': 'square'
|
||||
}
|
||||
|
||||
def generate_qr_code(self, data, settings=None):
|
||||
"""Generate QR code with custom settings"""
|
||||
def generate_qr_code(self, data, settings=None, format='PNG'):
|
||||
"""Generate QR code with custom settings in PNG or SVG format"""
|
||||
if settings is None:
|
||||
settings = self.default_settings.copy()
|
||||
else:
|
||||
@@ -28,6 +30,13 @@ class QRCodeGenerator:
|
||||
merged_settings.update(settings)
|
||||
settings = merged_settings
|
||||
|
||||
if format.upper() == 'SVG':
|
||||
return self._generate_svg_qr_code(data, settings)
|
||||
else:
|
||||
return self._generate_png_qr_code(data, settings)
|
||||
|
||||
def _generate_png_qr_code(self, data, settings):
|
||||
"""Generate PNG QR code (existing functionality)"""
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
@@ -64,6 +73,47 @@ class QRCodeGenerator:
|
||||
|
||||
return img
|
||||
|
||||
def _generate_svg_qr_code(self, data, settings):
|
||||
"""Generate SVG QR code"""
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=settings['error_correction'],
|
||||
box_size=settings['size'],
|
||||
border=settings['border'],
|
||||
)
|
||||
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Choose SVG image factory based on style
|
||||
if settings['style'] == 'circle':
|
||||
# Use SvgFillImage for better circle support
|
||||
factory = SvgFillImage
|
||||
else:
|
||||
# Use SvgPathImage for square and rounded styles
|
||||
factory = SvgPathImage
|
||||
|
||||
# Generate SVG image
|
||||
img = qr.make_image(
|
||||
image_factory=factory,
|
||||
fill_color=settings['foreground_color'],
|
||||
back_color=settings['background_color']
|
||||
)
|
||||
|
||||
return img
|
||||
|
||||
def generate_qr_code_svg_string(self, data, settings=None):
|
||||
"""Generate QR code as SVG string"""
|
||||
svg_img = self.generate_qr_code(data, settings, format='SVG')
|
||||
|
||||
# Convert SVG image to string
|
||||
svg_buffer = io.BytesIO()
|
||||
svg_img.save(svg_buffer)
|
||||
svg_buffer.seek(0)
|
||||
|
||||
return svg_buffer.getvalue().decode('utf-8')
|
||||
|
||||
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
|
||||
"""Add logo to QR code"""
|
||||
try:
|
||||
|
||||
122
app/utils/url_shortener.py
Normal file
122
app/utils/url_shortener.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
URL Shortener utilities for QR Code Manager
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import string
|
||||
import random
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
SHORT_URLS_FILE = os.path.join(DATA_DIR, 'short_urls.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class URLShortener:
|
||||
def __init__(self):
|
||||
self.base_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
|
||||
# Ensure we have the protocol
|
||||
if not self.base_domain.startswith(('http://', 'https://')):
|
||||
# Use HTTPS for production domains, HTTP for localhost
|
||||
protocol = 'https://' if 'localhost' not in self.base_domain else 'http://'
|
||||
self.base_domain = f"{protocol}{self.base_domain}"
|
||||
|
||||
self.short_urls_db = self._load_short_urls()
|
||||
|
||||
def _load_short_urls(self):
|
||||
"""Load short URLs from JSON file"""
|
||||
try:
|
||||
if os.path.exists(SHORT_URLS_FILE):
|
||||
with open(SHORT_URLS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading short URLs: {e}")
|
||||
return {}
|
||||
|
||||
def _save_short_urls(self):
|
||||
"""Save short URLs to JSON file"""
|
||||
try:
|
||||
with open(SHORT_URLS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.short_urls_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving short URLs: {e}")
|
||||
|
||||
def generate_short_code(self, length=6):
|
||||
"""Generate a random short code"""
|
||||
characters = string.ascii_letters + string.digits
|
||||
while True:
|
||||
short_code = ''.join(random.choice(characters) for _ in range(length))
|
||||
# Ensure uniqueness
|
||||
if short_code not in self.short_urls_db:
|
||||
return short_code
|
||||
|
||||
def create_short_url(self, original_url, custom_code=None, title=""):
|
||||
"""Create a shortened URL"""
|
||||
# Generate or use custom short code
|
||||
if custom_code and custom_code not in self.short_urls_db:
|
||||
short_code = custom_code
|
||||
else:
|
||||
short_code = self.generate_short_code()
|
||||
|
||||
# Ensure original URL has protocol
|
||||
if not original_url.startswith(('http://', 'https://')):
|
||||
original_url = f'https://{original_url}'
|
||||
|
||||
# Create URL record
|
||||
url_data = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'short_code': short_code,
|
||||
'original_url': original_url,
|
||||
'title': title,
|
||||
'clicks': 0,
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'last_accessed': None
|
||||
}
|
||||
|
||||
self.short_urls_db[short_code] = url_data
|
||||
self._save_short_urls() # Persist to file
|
||||
|
||||
# Return the complete short URL
|
||||
short_url = f"{self.base_domain}/s/{short_code}"
|
||||
return {
|
||||
'short_url': short_url,
|
||||
'short_code': short_code,
|
||||
'original_url': original_url,
|
||||
'id': url_data['id']
|
||||
}
|
||||
|
||||
def get_original_url(self, short_code):
|
||||
"""Get original URL from short code and track click"""
|
||||
if short_code in self.short_urls_db:
|
||||
url_data = self.short_urls_db[short_code]
|
||||
# Track click
|
||||
url_data['clicks'] += 1
|
||||
url_data['last_accessed'] = datetime.now().isoformat()
|
||||
self._save_short_urls() # Persist to file
|
||||
return url_data['original_url']
|
||||
return None
|
||||
|
||||
def get_url_stats(self, short_code):
|
||||
"""Get statistics for a short URL"""
|
||||
return self.short_urls_db.get(short_code)
|
||||
|
||||
def list_urls(self):
|
||||
"""List all short URLs"""
|
||||
return list(self.short_urls_db.values())
|
||||
|
||||
def delete_url(self, short_code):
|
||||
"""Delete a short URL"""
|
||||
if short_code in self.short_urls_db:
|
||||
del self.short_urls_db[short_code]
|
||||
self._save_short_urls() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def url_exists(self, short_code):
|
||||
"""Check if short URL exists"""
|
||||
return short_code in self.short_urls_db
|
||||
Reference in New Issue
Block a user