Quellcode durchsuchen

Fix: Unified database localization for materials/services and resolved Pydantic deprecation warnings

unknown vor 1 Woche
Ursprung
Commit
a6dbfe1890
6 geänderte Dateien mit 152 neuen und 34 gelöschten Zeilen
  1. 3 3
      backend/auth_utils.py
  2. 47 0
      backend/migrate_localized.py
  3. 50 0
      backend/reset_db.py
  4. 22 14
      backend/schema.sql
  5. 22 14
      backend/schemas.py
  6. 8 3
      src/components/ServicesSection.vue

+ 3 - 3
backend/auth_utils.py

@@ -1,6 +1,6 @@
 from passlib.context import CryptContext
 from jose import JWTError, jwt
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, UTC
 from typing import Optional
 from fastapi.security import OAuth2PasswordBearer
 import session_utils
@@ -24,9 +24,9 @@ def get_password_hash(password):
 def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
     to_encode = data.copy()
     if expires_delta:
-        expire = datetime.utcnow() + expires_delta
+        expire = datetime.now(UTC) + expires_delta
     else:
-        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+        expire = datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
     # Create a persistent session in Redis for tracking
     sid = session_utils.create_session(data.get("id", 0))
     to_encode.update({"exp": expire, "sid": sid})

+ 47 - 0
backend/migrate_localized.py

@@ -0,0 +1,47 @@
+import mysql.connector
+import os
+import sys
+
+# Add parent dir to path to import db
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+from db import DB_CONFIG
+
+def migrate():
+    try:
+        conn = mysql.connector.connect(**DB_CONFIG)
+        cursor = conn.cursor()
+        
+        print("Checking materials table...")
+        cursor.execute("DESCRIBE materials")
+        columns = [c[0] for c in cursor.fetchall()]
+        
+        if 'name_key' in columns:
+            print("Migrating materials table to localized columns...")
+            cursor.execute("ALTER TABLE materials ADD COLUMN name_en VARCHAR(100) AFTER id")
+            cursor.execute("ALTER TABLE materials ADD COLUMN name_ru VARCHAR(100) AFTER name_en")
+            cursor.execute("ALTER TABLE materials ADD COLUMN name_me VARCHAR(100) AFTER name_ru")
+            cursor.execute("ALTER TABLE materials ADD COLUMN desc_en TEXT AFTER name_me")
+            cursor.execute("ALTER TABLE materials ADD COLUMN desc_ru TEXT AFTER desc_en")
+            cursor.execute("ALTER TABLE materials ADD COLUMN desc_me TEXT AFTER desc_ru")
+            cursor.execute("ALTER TABLE materials DROP COLUMN name_key")
+            cursor.execute("ALTER TABLE materials DROP COLUMN description_key")
+            
+        print("Checking services table...")
+        cursor.execute("DESCRIBE services")
+        columns = [c[0] for c in cursor.fetchall()]
+        if 'description_key' in columns:
+            # Check if it's already text or needs change
+            pass # We'll just leave it for now or truncate and redo
+            
+        conn.commit()
+        print("Migration complete.")
+        
+    except Exception as e:
+        print(f"Error: {e}")
+    finally:
+        if 'conn' in locals() and conn.is_connected():
+            cursor.close()
+            conn.close()
+
+if __name__ == "__main__":
+    migrate()

+ 50 - 0
backend/reset_db.py

@@ -0,0 +1,50 @@
+import mysql.connector
+import os
+import sys
+
+# Add parent dir to path to import db
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+from db import DB_CONFIG
+
+def reset_and_seed():
+    try:
+        conn = mysql.connector.connect(**DB_CONFIG)
+        cursor = conn.cursor()
+        
+        print("Dropping tables...")
+        cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
+        cursor.execute("DROP TABLE IF EXISTS materials;")
+        cursor.execute("DROP TABLE IF EXISTS services;")
+        # cursor.execute("DROP TABLE IF EXISTS order_files;") # Avoid dropping if not needed, but they depend on materials? No, orders depend on materials?
+        cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
+        
+        print("Running schema.sql...")
+        schema_path = os.path.join(os.path.dirname(__file__), "schema.sql")
+        with open(schema_path, "r", encoding="utf-8") as f:
+            # We need to split by semicolon, but be careful with multi-line statements
+            sql = f.read()
+            # A simple split by ; works if there are no semicolons inside strings
+            sql_commands = sql.split(";")
+            
+        for command in sql_commands:
+            cmd = command.strip()
+            if cmd:
+                try:
+                    cursor.execute(cmd)
+                except Exception as e:
+                    # Skip database creation if exists
+                    if "database exists" not in str(e).lower():
+                        print(f"Error in command: {cmd[:50]}... -> {e}")
+        
+        conn.commit()
+        print("Database reset and seeded successfully.")
+        
+    except Exception as e:
+        print(f"Error: {e}")
+    finally:
+        if 'conn' in locals() and conn.is_connected():
+            cursor.close()
+            conn.close()
+
+if __name__ == "__main__":
+    reset_and_seed()

+ 22 - 14
backend/schema.sql

@@ -4,8 +4,12 @@ USE radionica3d;
 -- Materials table
 CREATE TABLE IF NOT EXISTS materials (
     id INT AUTO_INCREMENT PRIMARY KEY,
-    name_key VARCHAR(100) NOT NULL,
-    description_key VARCHAR(255),
+    name_en VARCHAR(100) NOT NULL,
+    name_ru VARCHAR(100),
+    name_me VARCHAR(100),
+    desc_en TEXT,
+    desc_ru TEXT,
+    desc_me TEXT,
     price_per_cm3 DECIMAL(10, 2) DEFAULT 0.00,
     is_active BOOLEAN DEFAULT TRUE,
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -14,9 +18,13 @@ CREATE TABLE IF NOT EXISTS materials (
 -- Services table
 CREATE TABLE IF NOT EXISTS services (
     id INT AUTO_INCREMENT PRIMARY KEY,
-    name_key VARCHAR(100) NOT NULL,
-    description_key VARCHAR(255),
-    tech_type VARCHAR(50), -- e.g., FDM, SLA, SLS
+    name_en VARCHAR(100) NOT NULL,
+    name_ru VARCHAR(100),
+    name_me VARCHAR(100),
+    desc_en TEXT,
+    desc_ru TEXT,
+    desc_me TEXT,
+    tech_type VARCHAR(50), -- e.g., FDM, SLA
     is_active BOOLEAN DEFAULT TRUE,
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
@@ -90,13 +98,13 @@ CREATE TABLE IF NOT EXISTS order_photos (
     FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
 );
 
--- Initial Data Migration (Optional, can be done via init script)
-INSERT INTO materials (name_key, description_key, price_per_cm3) VALUES 
-('pricing.matNames.pla', 'pricing.matDescs.pla', 0.04),
-('pricing.matNames.abs', 'pricing.matDescs.abs', 0.05),
-('pricing.matNames.petg', 'pricing.matDescs.petg', 0.06),
-('pricing.matNames.resin', 'pricing.matDescs.resin', 0.12);
+-- Initial Data Migration
+INSERT INTO materials (name_en, name_ru, name_me, desc_en, desc_ru, desc_me, price_per_cm3) VALUES 
+('PLA Plastic', 'PLA Пластик', 'PLA Plastika', 'Biodegradable, ideal for prototypes', 'Биоразлагаемый, идеален для прототипов', 'Biorazgradiva, idealna za prototipove', 0.04),
+('ABS Plastic', 'ABS Пластик', 'ABS Plastika', 'Durable, impact resistant', 'Прочный, ударостойкий', 'Izdržljiva, otporna na udarce', 0.05),
+('PETG Plastic', 'PETG Пластик', 'PETG Plastika', 'High chemical resistance', 'Высокая химическая стойкость', 'Visoka hemijska otpornost', 0.06),
+('Photopolymer Resin', 'Фотополимерная смола', 'Fotopolimerna smola', 'Maximum detail and surface quality', 'Максимальная детализация и качество поверхности', 'Maksimalna detaljnost i kvalitet površine', 0.12);
 
-INSERT INTO services (name_key, description_key, tech_type) VALUES 
-('services.fdm.title', 'services.fdm.description', 'FDM'),
-('services.sla.title', 'services.sla.description', 'SLA');
+INSERT INTO services (name_en, name_ru, name_me, desc_en, desc_ru, desc_me, tech_type) VALUES 
+('FDM Printing', 'FDM Печать', 'FDM Štampa', 'Fast and durable parts made from high-strength engineering plastics. Ideal for functional prototypes and industrial components.', 'Быстрые и прочные детали из высокопрочных инженерных пластиков. Идеально для функциональных прототипов и промышленных узлов.', 'Brzi i izdržljivi djelovi od visokootporne inženjerske plastike. Idealno za funkcionalne prototipove i industrijske komponente.', 'FDM'),
+('SLA Printing', 'SLA Печать', 'SLA Štampa', 'Maximum detail and smooth surface for professional prototypes and high-precision models.', 'Максимальная детализация и гладкость поверхности для профессиональных прототипов и высокоточных моделей.', 'Maksimalna detaljnost i glatka površina za profesionalne prototipove i modele visoke preciznosti.', 'SLA');

+ 22 - 14
backend/schemas.py

@@ -1,4 +1,4 @@
-from pydantic import BaseModel, EmailStr, Field
+from pydantic import BaseModel, EmailStr, Field, ConfigDict
 from typing import Optional, List
 from datetime import datetime
 
@@ -11,16 +11,18 @@ class MaterialBase(BaseModel):
     desc_en: Optional[str] = None
     desc_ru: Optional[str] = None
     desc_me: Optional[str] = None
-    name_key: Optional[str] = None
-    description_key: Optional[str] = None
     price_per_cm3: float
     is_active: bool
 
 class ServiceBase(BaseModel):
     id: int
-    name_key: str
-    description_key: Optional[str]
-    tech_type: Optional[str]
+    name_en: Optional[str] = None
+    name_ru: Optional[str] = None
+    name_me: Optional[str] = None
+    desc_en: Optional[str] = None
+    desc_ru: Optional[str] = None
+    desc_me: Optional[str] = None
+    tech_type: Optional[str] = None
     is_active: bool
 
 # Management models
@@ -45,14 +47,22 @@ class MaterialUpdate(BaseModel):
     is_active: Optional[bool] = None
 
 class ServiceCreate(BaseModel):
-    name_key: str
-    description_key: Optional[str] = None
+    name_en: str
+    name_ru: str
+    name_me: str
+    desc_en: str
+    desc_ru: str
+    desc_me: str
     tech_type: Optional[str] = None
     is_active: bool = True
 
 class ServiceUpdate(BaseModel):
-    name_key: Optional[str] = None
-    description_key: Optional[str] = None
+    name_en: Optional[str] = None
+    name_ru: Optional[str] = None
+    name_me: Optional[str] = None
+    desc_en: Optional[str] = None
+    desc_ru: Optional[str] = None
+    desc_me: Optional[str] = None
     tech_type: Optional[str] = None
     is_active: Optional[bool] = None
 
@@ -104,8 +114,7 @@ class UserResponse(BaseModel):
     ip_address: Optional[str] = None
     created_at: datetime
     
-    class Config:
-        from_attributes = True
+    model_config = ConfigDict(from_attributes=True)
 
 class AdminOrderUpdate(BaseModel):
     status: Optional[str] = None
@@ -148,5 +157,4 @@ class OrderResponse(OrderCreate):
     material_price: Optional[float] = None
     created_at: datetime
     
-    class Config:
-        from_attributes = True
+    model_config = ConfigDict(from_attributes=True)

+ 8 - 3
src/components/ServicesSection.vue

@@ -26,8 +26,8 @@
           <div class="w-14 h-14 bg-primary/10 rounded-xl flex items-center justify-center mb-4 group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-500">
             <component :is="iconMap[service.tech_type] || iconMap.DEFAULT" class="w-7 h-7" />
           </div>
-          <h3 class="font-display text-xl font-semibold mb-3">{{ t(service.name_key) }}</h3>
-          <p class="text-muted-foreground leading-relaxed">{{ t(service.description_key) }}</p>
+          <h3 class="font-display text-xl font-semibold mb-3">{{ service[`name_${locale}`] || service.name_en }}</h3>
+          <p class="text-muted-foreground leading-relaxed">{{ service[`desc_${locale}`] || service.desc_en }}</p>
         </div>
       </div>
 
@@ -65,7 +65,12 @@ import { getServices, getMaterials } from "@/lib/api";
 
 const { t, locale } = useI18n();
 
-interface Service { id: number; name_key: string; description_key: string; tech_type: string }
+interface Service { 
+  id: number; 
+  name_en: string; name_ru: string; name_me: string;
+  desc_en: string; desc_ru: string; desc_me: string;
+  tech_type: string 
+}
 interface Material { 
   id: number; 
   name_en: string; name_ru: string; name_me: string;