gemification_admin bekötve

This commit is contained in:
Roo
2026-06-30 08:56:55 +00:00
parent df4a0f07ff
commit cbfb955580
313 changed files with 18490 additions and 2895 deletions

View File

@@ -1,4 +1,5 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
import logging
from fastapi import APIRouter, Depends, HTTPException, status, Body, BackgroundTasks, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete, or_
@@ -6,6 +7,8 @@ from sqlalchemy.orm import selectinload
from typing import List, Any, Dict, Optional
from datetime import datetime, timedelta, timezone
logger = logging.getLogger("admin-endpoints")
from app.api import deps
from app.models.identity import User, UserRole, Person # JAVÍTVA: Központi import
from app.models.identity.address import Address
@@ -79,7 +82,7 @@ async def approve_action(
_ = Depends(deps.RequirePermission("settings:edit")),
):
try:
await security_service.approve_action(db, admin.id, action_id)
await security_service.approve_action(db, current_user.id, action_id)
return {"status": "success", "message": "Művelet végrehajtva."}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -420,8 +423,12 @@ async def apply_gamification_penalty(
"""
Gamification büntetés kiosztása egy felhasználónak.
Negatív szintek alkalmazása a frissen létrehozott Gamification rendszerben.
REFAKTOR: Most a GamificationService process_activity() metódusát használja
is_penalty=True paraméterrel, a direkt level módosítás helyett.
A penalty_level (negatív szám) átalakításra kerül penalty_points értékké.
"""
from app.services.gamification_service import gamification_service
# Ellenőrizzük, hogy létezik-e a felhasználó
user_stmt = select(User).where(User.id == user_id)
user_result = await db.execute(user_stmt)
@@ -433,50 +440,54 @@ async def apply_gamification_penalty(
detail=f"User not found with ID: {user_id}"
)
# Megkeressük a felhasználó gamification profilját (vagy létrehozzuk)
gamification_stmt = select(UserStats).where(UserStats.user_id == user_id)
gamification_result = await db.execute(gamification_stmt)
gamification = gamification_result.scalar_one_or_none()
# A penalty_level (pl. -3) átalakítása penalty_points értékké
# A régi rendszerben a penalty_level negatív szint volt (-1, -2, -3)
# Az új rendszerben penalty_points-ban mérjük (abs(penalty_level) * 100)
penalty_amount = abs(penalty.penalty_level) * 100
if not gamification:
# Ha nincs profil, létrehozzuk alapértelmezett értékekkel
gamification = UserStats(
try:
# GamificationService használata a büntetéshez
stats = await gamification_service.process_activity(
db=db,
user_id=user_id,
level=0,
xp=0,
reputation_score=100,
created_at=datetime.now(),
updated_at=datetime.now()
xp_amount=penalty_amount,
social_amount=0,
reason=f"ADMIN_PENALTY: {penalty.reason}",
is_penalty=True,
commit=False, # Ne commitoljon, mert még audit logot is hozzáadunk
action_key=None,
source_type="admin_penalty",
source_id=current_user.id,
)
# Audit log - SecurityAuditLog has actor_id, target_id, payload_before/payload_after
audit_log = SecurityAuditLog(
actor_id=current_user.id,
action="apply_gamification_penalty",
target_id=user_id,
payload_before={},
payload_after={"penalty_points_added": penalty_amount, "reason": penalty.reason},
is_critical=False,
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Gamification penalty applied to user {user_id}",
"user_id": user_id,
"penalty_points_added": penalty_amount,
"total_penalty_points": stats.penalty_points,
"restriction_level": stats.restriction_level,
"reason": penalty.reason
}
except Exception as e:
await db.rollback()
logger.error(f"Failed to apply penalty to user {user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to apply penalty: {str(e)}",
)
db.add(gamification)
await db.flush()
# Alkalmazzuk a büntetést (negatív szint módosítása)
# A level mező lehet negatív is a büntetések miatt
new_level = gamification.level + penalty.penalty_level
gamification.level = new_level
gamification.updated_at = datetime.now()
# Audit log
audit_log = SecurityAuditLog(
user_id=current_user.id,
action="apply_gamification_penalty",
target_user_id=user_id,
details=f"Gamification penalty applied: level change {penalty.penalty_level}, reason: {penalty.reason}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Gamification penalty applied to user {user_id}",
"user_id": user_id,
"penalty_level": penalty.penalty_level,
"new_level": new_level,
"reason": penalty.reason
}
# ==================== USER MANAGEMENT (EPIC 10: Admin Frontend) ====================

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
import logging
from fastapi import APIRouter, Depends, HTTPException, Body, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func, and_
from typing import List, Optional
from datetime import datetime, timedelta
logger = logging.getLogger("gamification-endpoints")
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.identity import User
@@ -12,6 +15,7 @@ from app.models import UserStats, PointsLedger, LevelConfig, UserContribution, B
from app.models.system import SystemParameter, ParameterScope
from app.models.marketplace.service import ServiceStaging
from app.schemas.gamification import SeasonResponse, UserStatResponse, LeaderboardEntry
from app.core.i18n import t
router = APIRouter()
@@ -140,7 +144,7 @@ async def get_season_standings(
season = (await db.execute(season_stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="Season not found")
raise HTTPException(status_code=404, detail=t("GAMIFICATION.SEASON.NOT_FOUND"))
# Top hozzájárulók lekérdezése
stmt = (
@@ -267,36 +271,31 @@ async def get_self_defense_status(
# --- AZ ÚJ, DINAMIKUS BEKÜLDŐ VÉGPONT (Gamification 2.0 kompatibilis) ---
@router.post("/submit-service")
async def submit_new_service(
name: str = Body(...),
city: str = Body(...),
name: str = Body(...),
city: str = Body(...),
address: str = Body(...),
contact_phone: Optional[str] = Body(None),
website: Optional[str] = Body(None),
description: Optional[str] = Body(None),
db: AsyncSession = Depends(get_db),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
from app.services.gamification_service import gamification_service
# 1. Önvédelmi státusz ellenőrzése
defense_status = await get_self_defense_status(db, current_user)
if not defense_status["can_submit_services"]:
raise HTTPException(
status_code=403,
detail="Önvédelmi korlátozás miatt nem küldhetsz be új szerviz adatokat."
status_code=403,
detail=t("GAMIFICATION.SUBMIT_SERVICE.SELF_DEFENSE_BLOCKED")
)
# 2. Beállítások lekérése az Admin által vezérelt táblából
submission_rewards = await get_system_param(
db, "service_submission_rewards",
db, "service_submission_rewards",
{"points": 50, "xp": 100, "social_credits": 10}
)
contribution_config = await get_system_param(
db, "contribution_types_config",
{
"service_submission": {"points": 50, "xp": 100, "weight": 1.0}
}
)
# 3. Aktuális szezon lekérdezése
season_stmt = select(Season).where(
and_(
@@ -351,50 +350,41 @@ async def submit_new_service(
points_awarded=submission_rewards.get("points", 50),
xp_awarded=submission_rewards.get("xp", 100),
status="pending", # Robot 5 jóváhagyására vár
metadata={
provided_fields={
"service_name": name,
"city": city,
"staging_id": new_staging.id
},
created_at=datetime.utcnow()
action_type=1,
earned_xp=submission_rewards.get("xp", 100),
)
db.add(contribution)
# 8. PointsLedger bejegyzés
ledger = PointsLedger(
user_id=current_user.id,
points=submission_rewards.get("points", 50),
xp=submission_rewards.get("xp", 100),
source_type="service_submission",
source_id=new_staging.id,
description=f"Szerviz beküldés: {name}",
created_at=datetime.utcnow()
)
db.add(ledger)
# 9. UserStats frissítése
if stats:
stats.total_points += submission_rewards.get("points", 50)
stats.total_xp += submission_rewards.get("xp", 100)
stats.services_submitted += 1
stats.updated_at = datetime.utcnow()
else:
# Ha nincs még UserStats, létrehozzuk
stats = UserStats(
# 8. GamificationService hívás a pontok jóváírásához (a direkt UserStats módosítás helyett)
# A GamificationService kezeli a szorzókat, szintlépést, recovery-t és naplózást
try:
await gamification_service.process_activity(
db=db,
user_id=current_user.id,
total_points=submission_rewards.get("points", 50),
total_xp=submission_rewards.get("xp", 100),
services_submitted=1,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
xp_amount=submission_rewards.get("xp", 100),
social_amount=submission_rewards.get("social_credits", 10),
reason=f"SERVICE_SUBMISSION: {name}",
is_penalty=False,
commit=False, # Ne commitoljon, mert a contribution-t is hozzáadjuk
action_key="service_submission",
source_type="service_submission",
source_id=new_staging.id,
)
db.add(stats)
except Exception as e:
await db.rollback()
logger.error(f"GamificationService error during service submission for user {current_user.id}: {e}")
raise HTTPException(status_code=400, detail=t("GAMIFICATION.SUBMIT_SERVICE.POINTS_ERROR", error=str(e)))
try:
await db.commit()
return {
"status": "success",
"message": "Szerviz beküldve a rendszerbe elemzésre!",
"status": "success",
"message": t("GAMIFICATION.SUBMIT_SERVICE.SUCCESS"),
"xp_earned": submission_rewards.get("xp", 100),
"points_earned": submission_rewards.get("points", 50),
"staging_id": new_staging.id,
@@ -402,7 +392,7 @@ async def submit_new_service(
}
except Exception as e:
await db.rollback()
raise HTTPException(status_code=400, detail=f"Hiba a beküldés során: {str(e)}")
raise HTTPException(status_code=400, detail=t("GAMIFICATION.SUBMIT_SERVICE.SUBMIT_ERROR", error=str(e)))
# --- Gamification 2.0 API végpontok (Frontend/Mobil) ---
@@ -441,7 +431,7 @@ async def get_active_season(
stmt = select(Season).where(Season.is_active == True)
season = (await db.execute(stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="No active season found")
raise HTTPException(status_code=404, detail=t("GAMIFICATION.SEASON.NO_ACTIVE"))
return SeasonResponse.from_orm(season)
@@ -499,33 +489,13 @@ async def get_daily_quiz(
if already_played:
raise HTTPException(
status_code=400,
detail="You have already played the daily quiz today. Try again tomorrow."
detail=t("GAMIFICATION.QUIZ.ALREADY_PLAYED")
)
# Return quiz questions (for now, using mock questions - in production these would come from a database)
quiz_questions = [
{
"id": 1,
"question": "Melyik alkatrész felelős a motor levegőüzemanyag keverékének szabályozásáért?",
"options": ["Generátor", "Lambdaszonda", "Féktárcsa", "Olajszűrő"],
"correctAnswer": 1,
"explanation": "A lambdaszonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."
},
{
"id": 2,
"question": "Mennyi ideig érvényes egy gépjármű műszaki vizsgája Magyarországon?",
"options": ["1 év", "2 év", "4 év", "6 év"],
"correctAnswer": 1,
"explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."
},
{
"id": 3,
"question": "Melyik anyag NEM része a hibrid autók akkumulátorának?",
"options": ["Lítium", "Nikkel", "Ólom", "Kobalt"],
"correctAnswer": 2,
"explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólomsavas akkukban van."
}
]
# Load quiz questions from JSON locale files (i18n)
from app.core.i18n import get_quiz_data
quiz_data = get_quiz_data()
quiz_questions = quiz_data.get("questions", [])
return {
"questions": quiz_questions,
@@ -557,48 +527,50 @@ async def submit_quiz_answer(
if already_played:
raise HTTPException(
status_code=400,
detail="You have already played the daily quiz today. Try again tomorrow."
detail=t("GAMIFICATION.QUIZ.ALREADY_PLAYED")
)
# Mock quiz data - in production this would come from a database
quiz_data = {
1: {"correct_answer": 1, "points": 10, "explanation": "A lambdaszonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."},
2: {"correct_answer": 1, "points": 10, "explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."},
3: {"correct_answer": 2, "points": 10, "explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólomsavas akkukban van."}
}
# Load quiz data from JSON locale files (i18n)
from app.core.i18n import get_quiz_data
quiz_locale = get_quiz_data()
quiz_questions = quiz_locale.get("questions", [])
if question_id not in quiz_data:
raise HTTPException(status_code=404, detail="Question not found")
# Build lookup dict from questions list
quiz_lookup = {}
for q in quiz_questions:
quiz_lookup[q["id"]] = {
"correct_answer": q["correctAnswer"],
"points": 10,
"explanation": q["explanation"]
}
question_info = quiz_data[question_id]
if question_id not in quiz_lookup:
raise HTTPException(status_code=404, detail=t("GAMIFICATION.QUIZ.QUESTION_NOT_FOUND"))
question_info = quiz_lookup[question_id]
is_correct = selected_option == question_info["correct_answer"]
# Award points if correct
# Award points if correct (GamificationService-en keresztül)
if is_correct:
# Update user stats
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats_result = await db.execute(stats_stmt)
user_stats = stats_result.scalar_one_or_none()
from app.services.gamification_service import gamification_service
if not user_stats:
# Create user stats if they don't exist
user_stats = UserStats(
try:
await gamification_service.process_activity(
db=db,
user_id=current_user.id,
total_xp=question_info["points"],
current_level=1
xp_amount=question_info["points"],
social_amount=0,
reason=f"DAILY_QUIZ: Question {question_id}",
is_penalty=False,
commit=False,
action_key="daily_quiz_correct",
source_type="daily_quiz",
source_id=question_id,
)
db.add(user_stats)
else:
user_stats.total_xp += question_info["points"]
# Add points ledger entry
points_ledger = PointsLedger(
user_id=current_user.id,
points=question_info["points"],
reason=f"Daily quiz correct answer - Question {question_id}",
created_at=datetime.now()
)
db.add(points_ledger)
except Exception as e:
await db.rollback()
logger.error(f"GamificationService error during quiz answer for user {current_user.id}: {e}")
raise HTTPException(status_code=500, detail=t("GAMIFICATION.QUIZ.POINTS_FAILED", error=str(e)))
await db.commit()
@@ -633,7 +605,7 @@ async def complete_daily_quiz(
if already_completed:
raise HTTPException(
status_code=400,
detail="Daily quiz already marked as completed today."
detail=t("GAMIFICATION.QUIZ.ALREADY_COMPLETED")
)
# Add completion entry
@@ -646,7 +618,7 @@ async def complete_daily_quiz(
db.add(completion_ledger)
await db.commit()
return {"message": "Daily quiz marked as completed for today."}
return {"message": t("GAMIFICATION.QUIZ.COMPLETED_SUCCESS")}
@router.get("/quiz/stats")
@@ -775,7 +747,7 @@ async def award_badge_to_user(
badge = badge_result.scalar_one_or_none()
if not badge:
raise HTTPException(status_code=404, detail="Badge not found")
raise HTTPException(status_code=404, detail=t("GAMIFICATION.BADGE.NOT_FOUND"))
# Determine target user (default to current user if not specified)
target_user_id = user_id if user_id else current_user.id
@@ -789,7 +761,7 @@ async def award_badge_to_user(
existing = existing_result.scalar_one_or_none()
if existing:
raise HTTPException(status_code=400, detail="User already has this badge")
raise HTTPException(status_code=400, detail=t("GAMIFICATION.BADGE.ALREADY_AWARDED"))
# Award the badge
user_badge = UserBadge(
@@ -799,34 +771,31 @@ async def award_badge_to_user(
)
db.add(user_badge)
# Also add points for earning a badge
points_ledger = PointsLedger(
user_id=target_user_id,
points=50, # Points for earning a badge
reason=f"Badge earned: {badge.name}",
created_at=datetime.now()
)
db.add(points_ledger)
# Award points via GamificationService (a direkt UserStats módosítás helyett)
from app.services.gamification_service import gamification_service
# Update user stats
stats_stmt = select(UserStats).where(UserStats.user_id == target_user_id)
stats_result = await db.execute(stats_stmt)
user_stats = stats_result.scalar_one_or_none()
if user_stats:
user_stats.total_xp += 50
else:
user_stats = UserStats(
try:
await gamification_service.process_activity(
db=db,
user_id=target_user_id,
total_xp=50,
current_level=1
xp_amount=50,
social_amount=0,
reason=f"BADGE_EARNED: {badge.name}",
is_penalty=False,
commit=False,
action_key="badge_earned",
source_type="badge",
source_id=badge_id,
)
db.add(user_stats)
except Exception as e:
await db.rollback()
logger.error(f"GamificationService error during badge award for user {target_user_id}: {e}")
raise HTTPException(status_code=500, detail=t("GAMIFICATION.BADGE.POINTS_FAILED", error=str(e)))
await db.commit()
return {
"message": f"Badge '{badge.name}' awarded to user",
"message": t("GAMIFICATION.BADGE.AWARDED_SUCCESS", badge_name=badge.name),
"badge_id": badge.id,
"badge_name": badge.name,
"points_awarded": 50
@@ -873,10 +842,10 @@ async def get_achievements_progress(
# XP-based achievements
xp_levels = [
{"title": "Novice", "xp_required": 100, "description": "Earn 100 XP"},
{"title": "Apprentice", "xp_required": 500, "description": "Earn 500 XP"},
{"title": "Expert", "xp_required": 2000, "description": "Earn 2000 XP"},
{"title": "Master", "xp_required": 5000, "description": "Earn 5000 XP"},
{"title": t("GAMIFICATION.ACHIEVEMENTS.XP_NOVICE"), "xp_required": 100, "description": t("GAMIFICATION.ACHIEVEMENTS.XP_NOVICE_DESC")},
{"title": t("GAMIFICATION.ACHIEVEMENTS.XP_APPRENTICE"), "xp_required": 500, "description": t("GAMIFICATION.ACHIEVEMENTS.XP_APPRENTICE_DESC")},
{"title": t("GAMIFICATION.ACHIEVEMENTS.XP_EXPERT"), "xp_required": 2000, "description": t("GAMIFICATION.ACHIEVEMENTS.XP_EXPERT_DESC")},
{"title": t("GAMIFICATION.ACHIEVEMENTS.XP_MASTER"), "xp_required": 5000, "description": t("GAMIFICATION.ACHIEVEMENTS.XP_MASTER_DESC")},
]
current_xp = user_stats.total_xp if user_stats else 0
@@ -901,9 +870,9 @@ async def get_achievements_progress(
quiz_points = quiz_points_result.scalar() or 0
quiz_achievements = [
{"title": "Quiz Beginner", "points_required": 50, "description": "Earn 50 quiz points"},
{"title": "Quiz Enthusiast", "points_required": 200, "description": "Earn 200 quiz points"},
{"title": "Quiz Master", "points_required": 500, "description": "Earn 500 quiz points"},
{"title": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_BEGINNER"), "points_required": 50, "description": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_BEGINNER_DESC")},
{"title": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_ENTHUSIAST"), "points_required": 200, "description": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_ENTHUSIAST_DESC")},
{"title": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_MASTER"), "points_required": 500, "description": t("GAMIFICATION.ACHIEVEMENTS.QUIZ_MASTER_DESC")},
]
for achievement in quiz_achievements: