diff --git a/.gitignore b/.gitignore index aa9d9a9..2e2fed2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ env __pycache__ instance/ uploads/ -.DS_Store \ No newline at end of file +.DS_Store +db/* diff --git a/_tools/db/migrate.txt b/_tools/db/migrate.txt index 1a93b27..eea8aa2 100644 --- a/_tools/db/migrate.txt +++ b/_tools/db/migrate.txt @@ -1,26 +1,38 @@ -Scenariusz migracji +python3 -m venv venv_migrate +source venv_migrate/bin/activate +pip install sqlalchemy psycopg2-binary dotenv +docker compose --profile pgsql up -d --build +PYTHONPATH=. python3 _tools/db/migrate_sqlite_to_pgsql.py +rm -rf venv_migrate -Zatrzymaj obecny kontener: +# reset wszystkich sekwencji w pgsql +docker exec -it pgsql-db psql -U lista -d lista -docker-compose down -Ustaw w .env: - -.env - -DB_ENGINE=pgsql -DB_NAME=myapp -DB_USER=user -DB_PASSWORD=pass -Uruchom bazę i aplikację: - -docker-compose --profile pgsql up -d -Wykonaj migrację danych: - - -bash _tools/db/migrate_to_new_db.sh pgsql - - - -Sprawdź działanie aplikacji. - +DO $$ +DECLARE + r RECORD; +BEGIN + FOR r IN + SELECT + c.relname AS seq_name, + t.relname AS table_name, + a.attname AS column_name + FROM + pg_class c + JOIN + pg_depend d ON d.objid = c.oid + JOIN + pg_class t ON d.refobjid = t.oid + JOIN + pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid + WHERE + c.relkind = 'S' + AND d.deptype = 'a' + LOOP + EXECUTE format( + 'SELECT setval(%L, COALESCE((SELECT MAX(%I) FROM %I), 1), true)', + r.seq_name, r.column_name, r.table_name + ); + END LOOP; +END$$; diff --git a/_tools/db/migrate_sqlite_to_pgsql.py b/_tools/db/migrate_sqlite_to_pgsql.py index 49f94ab..946a507 100644 --- a/_tools/db/migrate_sqlite_to_pgsql.py +++ b/_tools/db/migrate_sqlite_to_pgsql.py @@ -1,17 +1,22 @@ +import sys import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import sessionmaker from config import Config +from dotenv import load_dotenv +load_dotenv() # Źródło: SQLite sqlite_engine = create_engine("sqlite:///instance/shopping.db") -sqlite_meta = MetaData(bind=sqlite_engine) -sqlite_meta.reflect() +sqlite_meta = MetaData() +sqlite_meta.reflect(bind=sqlite_engine) -# Cel: PostgreSQL (czytany z .env przez Config) +# Cel: PostgreSQL pg_engine = create_engine(Config.SQLALCHEMY_DATABASE_URI) -pg_meta = MetaData(bind=pg_engine) -pg_meta.reflect() +pg_meta = MetaData() +pg_meta.reflect(bind=pg_engine) # Sesje SQLiteSession = sessionmaker(bind=sqlite_engine) @@ -21,29 +26,36 @@ sqlite_session = SQLiteSession() pg_session = PGSession() def migrate_table(table_name): - print(f"Migruję tabelę: {table_name}") + print("➡️ Używana baza docelowa:", Config.SQLALCHEMY_DATABASE_URI) + print(f"\n➡️ Migruję tabelę: {table_name}") source_table = sqlite_meta.tables.get(table_name) target_table = pg_meta.tables.get(table_name) - if not source_table or not target_table: - print(f"Pominięto: {table_name} (brak w jednej z baz)") + if source_table is None or target_table is None: + print(f"⚠️ Pominięto: {table_name} (brak w jednej z baz)") return rows = sqlite_session.execute(source_table.select()).fetchall() if not rows: - print("Brak danych do migracji.") + print("ℹ️ Brak danych do migracji.") return insert_data = [dict(row._mapping) for row in rows] - with pg_engine.begin() as conn: - conn.execute(target_table.insert(), insert_data) - print(f"✅ Przeniesiono: {len(rows)} rekordów") + try: + with pg_engine.begin() as conn: + conn.execute(target_table.delete()) + conn.execute(target_table.insert(), insert_data) + print(f"✅ Przeniesiono: {len(rows)} rekordów") + except Exception as e: + print(f"❌ Błąd przy migracji {table_name}: {e}") + def main(): tables = ["user", "shopping_list", "item", "expense", "receipt", "suggested_product"] for table in tables: migrate_table(table) + print("\n🎉 Migracja zakończona pomyślnie.") if __name__ == "__main__": main() diff --git a/_tools/db/migrate_to_new_db.sh b/_tools/db/migrate_to_new_db.sh deleted file mode 100644 index 32c72fe..0000000 --- a/_tools/db/migrate_to_new_db.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -APP_CONTAINER=web -DB_ENGINE=${1:-pgsql} # domyślnie pgsql - -echo "Migracja z SQLite do $DB_ENGINE..." - -docker-compose run --rm $APP_CONTAINER bash -c " - export FLASK_APP=app.py - flask db upgrade -" - -echo "Migracja zakończona." diff --git a/app.py b/app.py index ee97ac9..bf98b69 100644 --- a/app.py +++ b/app.py @@ -103,7 +103,7 @@ def utcnow(): class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) - password_hash = db.Column(db.String(150), nullable=False) + password_hash = db.Column(db.String(512), nullable=False) is_admin = db.Column(db.Boolean, default=False) @@ -162,6 +162,7 @@ class Receipt(db.Model): file_hash = db.Column(db.String(64), nullable=True, unique=True) + with app.app_context(): db.create_all() @@ -1088,9 +1089,11 @@ def all_products(): top_products_query = top_products_query.filter( SuggestedProduct.name.ilike(f"%{query}%") ) + top_products = ( top_products_query.order_by( - SuggestedProduct.usage_count.desc(), SuggestedProduct.name.asc() + SuggestedProduct.name.asc(), # musi być pierwsze + SuggestedProduct.usage_count.desc() ) .distinct(SuggestedProduct.name) .limit(20) @@ -1125,8 +1128,9 @@ def all_products(): def upload_receipt(list_id): l = db.session.get(ShoppingList, list_id) - if l is None or l.owner_id != current_user.id: - return _receipt_error("Nie masz uprawnień do tej listy.") + + #if l is None or l.owner_id != current_user.id: + # return _receipt_error("Nie masz uprawnień do tej listy.") if "receipt" not in request.files: return _receipt_error("Brak pliku") diff --git a/config.py b/config.py index 1946569..5c74d03 100644 --- a/config.py +++ b/config.py @@ -1,12 +1,9 @@ import os - class Config: SECRET_KEY = os.environ.get("SECRET_KEY", "D8pceNZ8q%YR7^7F&9wAC2") - DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite").lower() - if DB_ENGINE == "sqlite": SQLALCHEMY_DATABASE_URI = "sqlite:///instance/shopping.db" elif DB_ENGINE == "pgsql": @@ -18,9 +15,6 @@ class Config: else: raise ValueError("Nieobsługiwany typ bazy danych.") - - - SQLALCHEMY_TRACK_MODIFICATIONS = False SYSTEM_PASSWORD = os.environ.get("SYSTEM_PASSWORD", "admin") DEFAULT_ADMIN_USERNAME = os.environ.get("DEFAULT_ADMIN_USERNAME", "admin") diff --git a/deploy_docker.sh b/deploy_docker.sh index 7304eb1..1e8ff59 100644 --- a/deploy_docker.sh +++ b/deploy_docker.sh @@ -1,15 +1,28 @@ #!/bin/bash set -e +PROFILE=$1 + +if [[ -z "$PROFILE" ]]; then + echo "Użycie: $0 {pgsql|mysql|firebird|sqlite}" + exit 1 +fi + echo "Zatrzymuję i usuwam stare kontenery..." -docker compose down --rmi all +if [[ "$PROFILE" == "sqlite" ]]; then + docker compose down --rmi all +else + docker compose --profile "$PROFILE" down --rmi all +fi echo "Pobieram najnowszy kod z repozytorium..." git pull echo "Buduję obrazy i uruchamiam kontenery..." -#docker compose up -d --build -DB_ENGINE=pgsql docker compose --profile pgsql up -d --build - +if [[ "$PROFILE" == "sqlite" ]]; then + docker compose up -d --build +else + DB_ENGINE="$PROFILE" docker compose --profile "$PROFILE" up -d --build +fi echo "Gotowe!" diff --git a/docker-compose.yml b/docker-compose.yml index 070eb9e..5c89a6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,6 @@ services: - ./uploads:/app/uploads - ./instance:/app/instance restart: unless-stopped - # depends_on: - # - ${DB_ENGINE:-sqlite} pgsql: image: postgres:17 @@ -32,6 +30,7 @@ services: ports: - "5432:5432" restart: unless-stopped + hostname: db profiles: ["pgsql"] mysql: @@ -47,6 +46,7 @@ services: ports: - "3306:3306" restart: unless-stopped + hostname: db profiles: ["mysql"] firebird: @@ -59,4 +59,5 @@ services: ports: - "3050:3050" restart: unless-stopped + hostname: db profiles: ["firebird"] diff --git a/requirements.txt b/requirements.txt index 147773d..43127ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ pillow-heif pytesseract opencv-python-headless +psycopg2-binary