Merge branch 'develop' into feature/frontend-v2
This commit is contained in:
+6
-2
@@ -2,6 +2,10 @@ FROM python:3.10-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip install fastapi uvicorn
|
COPY requirements.txt .
|
||||||
|
|
||||||
CMD ["uvicorn", "api.routes:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
MONGO_URL = "mongodb://mongo:27017"
|
||||||
|
DB_NAME = "deckofcards"
|
||||||
|
|
||||||
|
client = AsyncIOMotorClient(MONGO_URL)
|
||||||
|
db = client[DB_NAME]
|
||||||
|
|
||||||
|
users_collection = db["users"]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from api.database.mongodb import db
|
||||||
|
|
||||||
|
# Routers
|
||||||
|
from api.routers.test_mongo import router as test_mongo_router
|
||||||
|
from api.routers.value_function import router as value_router
|
||||||
|
from api.routers.docmf_build import router as docmf_build_router
|
||||||
|
from api.routers.docmf_evaluate import router as docmf_eval_router
|
||||||
|
from api.routers.docmf_simple_validation import router as simple_validation_router
|
||||||
|
from api.routers.docmf_validation import router as validation_router
|
||||||
|
from api.routers.auth import router as auth_router
|
||||||
|
from api.routers.history import router as history_router
|
||||||
|
from api.routers.test_mongo import router as test_mongo_router
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Aquí podrías hacer comprobaciones si quieres
|
||||||
|
yield
|
||||||
|
# No hace falta cerrar nada con Motor
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(test_mongo_router, prefix="/api")
|
||||||
|
app.include_router(value_router, prefix="/api/criteria/doc")
|
||||||
|
app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf")
|
||||||
|
app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf")
|
||||||
|
app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf")
|
||||||
|
app.include_router(validation_router, prefix="/api/criteria/doc-mf")
|
||||||
|
app.include_router(test_mongo_router, prefix="/api")
|
||||||
|
app.include_router(auth_router, prefix="/api")
|
||||||
|
app.include_router(history_router, prefix="/api")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
class DoCMFRequest(BaseModel):
|
||||||
|
term: str
|
||||||
|
core: Tuple[float, float]
|
||||||
|
support: Tuple[float, float]
|
||||||
|
left_nodes_x: List[float]
|
||||||
|
left_blank_cards: List[int]
|
||||||
|
right_nodes_x: List[float]
|
||||||
|
right_blank_cards: List[int]
|
||||||
|
|
||||||
|
@field_validator("term")
|
||||||
|
def term_not_empty(cls, v):
|
||||||
|
if not v.strip():
|
||||||
|
raise ValueError("El término no puede estar vacío.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("core")
|
||||||
|
def core_valid(cls, v):
|
||||||
|
a, b = v
|
||||||
|
if a > b:
|
||||||
|
raise ValueError("El núcleo debe cumplir a <= b.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("support")
|
||||||
|
def support_valid(cls, v, info):
|
||||||
|
c, d = v
|
||||||
|
if c >= d:
|
||||||
|
raise ValueError("El soporte debe cumplir c < d.")
|
||||||
|
|
||||||
|
core = info.data.get("core")
|
||||||
|
if core:
|
||||||
|
a, b = core
|
||||||
|
if not (c <= a < b <= d):
|
||||||
|
raise ValueError("El núcleo debe estar dentro del soporte.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("left_blank_cards", "right_blank_cards")
|
||||||
|
def cards_valid(cls, v):
|
||||||
|
if any(c < 0 for c in v):
|
||||||
|
raise ValueError("Las cartas no pueden ser negativas.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class DoCMFMultiRequest(BaseModel):
|
||||||
|
levels: List[DoCMFRequest]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
class SimpleLevelDefinition(BaseModel):
|
||||||
|
core: Tuple[float, float]
|
||||||
|
support: Tuple[float, float]
|
||||||
|
|
||||||
|
@field_validator("core")
|
||||||
|
def validate_core(cls, v):
|
||||||
|
a, b = v
|
||||||
|
if a >= b:
|
||||||
|
raise ValueError("El núcleo debe cumplir a < b.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("support")
|
||||||
|
def validate_support(cls, v):
|
||||||
|
c, d = v
|
||||||
|
if c >= d:
|
||||||
|
raise ValueError("El soporte debe cumplir c < d.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleValidationRequest(BaseModel):
|
||||||
|
levels: List[SimpleLevelDefinition]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
class LevelDefinition(BaseModel):
|
||||||
|
core: Tuple[float, float]
|
||||||
|
support: Tuple[float, float]
|
||||||
|
left_nodes: List[Tuple[float, float]]
|
||||||
|
right_nodes: List[Tuple[float, float]]
|
||||||
|
|
||||||
|
@field_validator("core")
|
||||||
|
def validate_core(cls, v):
|
||||||
|
a, b = v
|
||||||
|
if a >= b:
|
||||||
|
raise ValueError("El núcleo debe cumplir a < b.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("support")
|
||||||
|
def validate_support(cls, v):
|
||||||
|
c, d = v
|
||||||
|
if c >= d:
|
||||||
|
raise ValueError("El soporte debe cumplir c < d.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("left_nodes", "right_nodes")
|
||||||
|
def validate_nodes(cls, v):
|
||||||
|
if len(v) < 2:
|
||||||
|
raise ValueError("Debe haber al menos 2 nodos.")
|
||||||
|
xs = [p[0] for p in v]
|
||||||
|
if xs != sorted(xs):
|
||||||
|
raise ValueError("Los nodos deben estar ordenados por x.")
|
||||||
|
if len(xs) != len(set(xs)):
|
||||||
|
raise ValueError("Los nodos no pueden tener valores x duplicados.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationRequest(BaseModel):
|
||||||
|
levels: List[LevelDefinition]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
class EvaluationRequest(BaseModel):
|
||||||
|
x: float
|
||||||
|
core: Tuple[float, float]
|
||||||
|
support: Tuple[float, float]
|
||||||
|
left_nodes: List[Tuple[float, float]]
|
||||||
|
right_nodes: List[Tuple[float, float]]
|
||||||
|
|
||||||
|
@field_validator("core")
|
||||||
|
def core_valid(cls, v):
|
||||||
|
a, b = v
|
||||||
|
if a > b:
|
||||||
|
raise ValueError("El núcleo debe cumplir a <= b.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("support")
|
||||||
|
def support_valid(cls, v, info):
|
||||||
|
c, d = v
|
||||||
|
if c >= d:
|
||||||
|
raise ValueError("El soporte debe cumplir c < d.")
|
||||||
|
|
||||||
|
core = info.data.get("core")
|
||||||
|
if core:
|
||||||
|
a, b = core
|
||||||
|
if not (c <= a < b <= d):
|
||||||
|
raise ValueError("El núcleo debe estar dentro del soporte.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("left_nodes", "right_nodes")
|
||||||
|
def nodes_valid(cls, v):
|
||||||
|
if len(v) < 2:
|
||||||
|
raise ValueError("Debe haber al menos 2 nodos.")
|
||||||
|
return v
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzyTerm(BaseModel):
|
||||||
|
term: str
|
||||||
|
core: List[float]
|
||||||
|
support: List[float]
|
||||||
|
left_nodes: List[List[float]]
|
||||||
|
right_nodes: List[List[float]]
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryItem(BaseModel):
|
||||||
|
id: Optional[str] = Field(default=None, alias="_id")
|
||||||
|
name: str
|
||||||
|
created_at: datetime
|
||||||
|
results: List[FuzzyTerm]
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryCreateRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
results: List[FuzzyTerm]
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDB(BaseModel):
|
||||||
|
id: Optional[str] = Field(default=None, alias="_id")
|
||||||
|
username: str
|
||||||
|
email: EmailStr
|
||||||
|
password_hash: str
|
||||||
|
token: Optional[str] = None
|
||||||
|
history: List[HistoryItem] = []
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
class ValueFunctionRequest(BaseModel):
|
||||||
|
criterion_name: str
|
||||||
|
levels: List[str]
|
||||||
|
blank_cards: List[int]
|
||||||
|
references: Dict[str, float]
|
||||||
|
|
||||||
|
@field_validator("criterion_name")
|
||||||
|
def name_not_empty(cls, v):
|
||||||
|
if not v.strip():
|
||||||
|
raise ValueError("El nombre no puede estar vacío.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("levels")
|
||||||
|
def levels_not_empty(cls, v):
|
||||||
|
if len(v) < 2:
|
||||||
|
raise ValueError("Debe haber al menos 2 niveles.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("blank_cards")
|
||||||
|
def cards_valid(cls, v, info):
|
||||||
|
if any(c < 0 for c in v):
|
||||||
|
raise ValueError("Las cartas no pueden ser negativas.")
|
||||||
|
|
||||||
|
levels = info.data.get("levels")
|
||||||
|
if levels and len(v) != len(levels) - 1:
|
||||||
|
raise ValueError("Debe haber uno menos de número de cartas blancas que de niveles.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("references")
|
||||||
|
def refs_valid(cls, v):
|
||||||
|
if len(v) != 2:
|
||||||
|
raise ValueError("Debe haber 2 referencias.")
|
||||||
|
return v
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
from api.database.mongodb import users_collection
|
||||||
|
from api.models.user_models import UserCreate, UserLogin
|
||||||
|
from api.utils.security import hash_password, verify_password, generate_token
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register")
|
||||||
|
async def register_user(user: UserCreate):
|
||||||
|
existing_username = await users_collection.find_one({"username": user.username})
|
||||||
|
if existing_username:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="El nombre de usuario ya está en uso",
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_email = await users_collection.find_one({"email": user.email})
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="El email ya está registrado",
|
||||||
|
)
|
||||||
|
|
||||||
|
token = generate_token()
|
||||||
|
|
||||||
|
user_doc = {
|
||||||
|
"username": user.username,
|
||||||
|
"email": user.email,
|
||||||
|
"password_hash": hash_password(user.password),
|
||||||
|
"token": token,
|
||||||
|
"history": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await users_collection.insert_one(user_doc)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Usuario registrado correctamente",
|
||||||
|
"user_id": str(result.inserted_id),
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
async def login_user(credentials: UserLogin):
|
||||||
|
user = await users_collection.find_one({"email": credentials.email})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Credenciales inválidas",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not verify_password(credentials.password, user["password_hash"]):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Credenciales inválidas",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_token = generate_token()
|
||||||
|
|
||||||
|
await users_collection.update_one(
|
||||||
|
{"_id": user["_id"]},
|
||||||
|
{"$set": {"token": new_token}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Login correcto",
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"username": user["username"],
|
||||||
|
"token": new_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout/{user_id}")
|
||||||
|
async def logout_user(user_id: str):
|
||||||
|
user = await users_collection.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Usuario no encontrado",
|
||||||
|
)
|
||||||
|
|
||||||
|
await users_collection.update_one(
|
||||||
|
{"_id": ObjectId(user_id)},
|
||||||
|
{"$set": {"token": None}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Sesión cerrada correctamente"}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from api.models.docmf_models import DoCMFMultiRequest
|
||||||
|
from api.services.docmf_build_service import build_docmf_multi
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/build")
|
||||||
|
def build(request: DoCMFMultiRequest):
|
||||||
|
try:
|
||||||
|
return build_docmf_multi(request)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from api.models.evaluation_models import EvaluationRequest
|
||||||
|
from api.services.docmf_evaluate_service import evaluate_docmf
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/evaluate")
|
||||||
|
def evaluate(request: EvaluationRequest):
|
||||||
|
try:
|
||||||
|
return evaluate_docmf(request)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from api.models.docmf_simple_validation_models import SimpleValidationRequest
|
||||||
|
from api.services.docmf_simple_validation_service import validate_simple_levels
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/validate-simple")
|
||||||
|
def validate_simple_docmf(request: SimpleValidationRequest):
|
||||||
|
results = validate_simple_levels(request.levels)
|
||||||
|
invalid = [r for r in results if not r["valid"]]
|
||||||
|
|
||||||
|
# Caso: un solo nivel
|
||||||
|
if len(request.levels) == 1:
|
||||||
|
if invalid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"message": "El nivel es incorrecto.",
|
||||||
|
"errors": invalid[0]["errors"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "El nivel es correcto.",
|
||||||
|
"details": results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caso: varios niveles
|
||||||
|
if invalid:
|
||||||
|
return {
|
||||||
|
"message": "Validación completada.",
|
||||||
|
"valid_levels": [r for r in results if r["valid"]],
|
||||||
|
"invalid_levels": invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Todos los niveles son correctos.",
|
||||||
|
"results": results
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from api.models.docmf_validation_models import ValidationRequest
|
||||||
|
from api.services.docmf_validation_service import validate_levels
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/validate")
|
||||||
|
def validate_docmf_levels(request: ValidationRequest):
|
||||||
|
results = validate_levels(request.levels)
|
||||||
|
|
||||||
|
invalid = [r for r in results if not r["valid"]]
|
||||||
|
|
||||||
|
if len(request.levels) == 1:
|
||||||
|
# Caso de un solo nivel
|
||||||
|
if invalid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"message": "El nivel es incorrecto.",
|
||||||
|
"errors": invalid[0]["errors"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "El nivel es correcto.",
|
||||||
|
"details": results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caso de varios niveles
|
||||||
|
if invalid:
|
||||||
|
return {
|
||||||
|
"message": "Validación completada.",
|
||||||
|
"valid_levels": [r for r in results if r["valid"]],
|
||||||
|
"invalid_levels": invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Todos los niveles son correctos.",
|
||||||
|
"results": results
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
from typing import List
|
||||||
|
from datetime import datetime
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
from api.database.mongodb import users_collection
|
||||||
|
from api.models.user_models import FuzzyTerm, HistoryCreateRequest
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/history", tags=["history"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{user_id}/add")
|
||||||
|
async def add_history_item(user_id: str, data: HistoryCreateRequest):
|
||||||
|
user = await users_collection.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Usuario no encontrado",
|
||||||
|
)
|
||||||
|
|
||||||
|
history_item_id = ObjectId()
|
||||||
|
|
||||||
|
history_item = {
|
||||||
|
"_id": history_item_id,
|
||||||
|
"name": data.name, # ← nuevo campo
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"results": [r.dict() for r in data.results],
|
||||||
|
}
|
||||||
|
|
||||||
|
await users_collection.update_one(
|
||||||
|
{"_id": ObjectId(user_id)},
|
||||||
|
{"$push": {"history": history_item}},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Elemento añadido al historial",
|
||||||
|
"history_item_id": str(history_item_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}/delete/{history_item_id}")
|
||||||
|
async def delete_history_item(user_id: str, history_item_id: str):
|
||||||
|
user = await users_collection.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Usuario no encontrado",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await users_collection.update_one(
|
||||||
|
{"_id": ObjectId(user_id)},
|
||||||
|
{"$pull": {"history": {"_id": ObjectId(history_item_id)}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Elemento de historial no encontrado",
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Elemento eliminado del historial"}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from api.database.mongodb import db
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/test-mongo")
|
||||||
|
async def test_mongo():
|
||||||
|
try:
|
||||||
|
await db.command("ping")
|
||||||
|
return {"status": "ok", "message": "Conexión a MongoDB correcta"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from api.models.value_function_models import ValueFunctionRequest
|
||||||
|
from api.services.value_function_service import compute_value_function, compute_points
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/value-function")
|
||||||
|
def value_function(request: ValueFunctionRequest):
|
||||||
|
try:
|
||||||
|
return compute_value_function(request)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/value-function/points")
|
||||||
|
def value_function_points(request: ValueFunctionRequest):
|
||||||
|
try:
|
||||||
|
return compute_points(request)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
from fastapi import FastAPI
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import List, Dict, Tuple
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
# Configuración CORS para permitir peticiones desde React
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # Permite cualquier origen (perfecto para desarrollo local)
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"], # Permite POST, GET, OPTIONS, etc.
|
|
||||||
allow_headers=["*"], # Permite cualquier cabecera
|
|
||||||
)
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# MODELOS
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
class ValueFunctionRequest(BaseModel):
|
|
||||||
criterion_name: str
|
|
||||||
levels: List[str]
|
|
||||||
blank_cards: List[int]
|
|
||||||
references: Dict[str, float]
|
|
||||||
|
|
||||||
class DoCMFRequest(BaseModel):
|
|
||||||
term: str
|
|
||||||
core: Tuple[float, float] # [a, b]
|
|
||||||
support: Tuple[float, float] # [c, d]
|
|
||||||
left_nodes_x: List[float]
|
|
||||||
left_blank_cards: List[int]
|
|
||||||
right_nodes_x: List[float]
|
|
||||||
right_blank_cards: List[int]
|
|
||||||
|
|
||||||
class EvaluationRequest(BaseModel):
|
|
||||||
x: float
|
|
||||||
left_nodes: List[Tuple[float, float]]
|
|
||||||
right_nodes: List[Tuple[float, float]]
|
|
||||||
core: Tuple[float, float]
|
|
||||||
support: Tuple[float, float]
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENDPOINT 1: FUNCIÓN DE VALOR DoC
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
@app.post("/api/criteria/doc/value-function")
|
|
||||||
def calcular_value_function(request: ValueFunctionRequest):
|
|
||||||
levels = request.levels
|
|
||||||
cards = request.blank_cards
|
|
||||||
refs = request.references
|
|
||||||
|
|
||||||
ref_indices = sorted(int(k) for k in refs)
|
|
||||||
p, q = ref_indices
|
|
||||||
up, uq = refs[str(p)], refs[str(q)]
|
|
||||||
|
|
||||||
total_units = sum(cards[i] + 1 for i in range(p, q))
|
|
||||||
alpha = (uq - up) / total_units if total_units else 0
|
|
||||||
|
|
||||||
values = [0] * len(levels)
|
|
||||||
values[p] = up
|
|
||||||
|
|
||||||
for i in range(p + 1, len(levels)):
|
|
||||||
units = sum(cards[r] + 1 for r in range(p, i))
|
|
||||||
values[i] = up + alpha * units
|
|
||||||
|
|
||||||
for i in range(p - 1, -1, -1):
|
|
||||||
units = sum(cards[r] + 1 for r in range(i, p))
|
|
||||||
values[i] = up - alpha * units
|
|
||||||
|
|
||||||
return {
|
|
||||||
"criterion_name": request.criterion_name,
|
|
||||||
"values": {levels[i]: round(values[i], 4) for i in range(len(levels))}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENDPOINT 2: CONSTRUIR DoC-MF
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
@app.post("/api/criteria/doc-mf/build")
|
|
||||||
def build_doc_mf(request: DoCMFRequest):
|
|
||||||
|
|
||||||
a, b = request.core
|
|
||||||
c, d = request.support
|
|
||||||
|
|
||||||
# ---- LADO IZQUIERDO ----
|
|
||||||
left_x = request.left_nodes_x
|
|
||||||
left_e = request.left_blank_cards
|
|
||||||
|
|
||||||
TL = sum(e + 1 for e in left_e)
|
|
||||||
YL = 1 / TL if TL else 0
|
|
||||||
|
|
||||||
left_nodes = []
|
|
||||||
acc = 0
|
|
||||||
for i in range(len(left_x)):
|
|
||||||
if i == 0:
|
|
||||||
left_nodes.append((left_x[i], 0.0))
|
|
||||||
else:
|
|
||||||
acc += (left_e[i-1] + 1)
|
|
||||||
left_nodes.append((left_x[i], round(acc * YL, 4)))
|
|
||||||
|
|
||||||
# ---- LADO DERECHO ----
|
|
||||||
right_x = request.right_nodes_x
|
|
||||||
right_e = request.right_blank_cards
|
|
||||||
|
|
||||||
TR = sum(e + 1 for e in right_e)
|
|
||||||
YR = 1 / TR if TR else 0
|
|
||||||
|
|
||||||
right_nodes = []
|
|
||||||
acc = 0
|
|
||||||
for i in range(len(right_x)):
|
|
||||||
if i == 0:
|
|
||||||
right_nodes.append((right_x[i], 1.0))
|
|
||||||
else:
|
|
||||||
acc += (right_e[i-1] + 1)
|
|
||||||
right_nodes.append((right_x[i], round(1 - acc * YR, 4)))
|
|
||||||
|
|
||||||
return {
|
|
||||||
"term": request.term,
|
|
||||||
"core": request.core,
|
|
||||||
"support": request.support,
|
|
||||||
"left_nodes": left_nodes,
|
|
||||||
"right_nodes": right_nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENDPOINT 3: EVALUAR UN VALOR x
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
def linear_interpolation(x, nodes):
|
|
||||||
for i in range(len(nodes) - 1):
|
|
||||||
x0, y0 = nodes[i]
|
|
||||||
x1, y1 = nodes[i+1]
|
|
||||||
if x0 <= x <= x1:
|
|
||||||
t = (x - x0) / (x1 - x0)
|
|
||||||
return y0 + t * (y1 - y0)
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
@app.post("/api/criteria/doc-mf/evaluate")
|
|
||||||
def evaluate_doc_mf(request: EvaluationRequest):
|
|
||||||
x = request.x
|
|
||||||
a, b = request.core
|
|
||||||
c, d = request.support
|
|
||||||
|
|
||||||
if x < c or x > d:
|
|
||||||
return {
|
|
||||||
"membership": 0.0,
|
|
||||||
"explanation": "El valor está fuera del soporte, por eso la pertenencia es 0."
|
|
||||||
}
|
|
||||||
|
|
||||||
if a <= x <= b:
|
|
||||||
return {
|
|
||||||
"membership": 1.0,
|
|
||||||
"explanation": "El valor está dentro del núcleo, por eso la pertenencia es 1."
|
|
||||||
}
|
|
||||||
|
|
||||||
if c <= x < a:
|
|
||||||
mu = linear_interpolation(x, request.left_nodes)
|
|
||||||
return {
|
|
||||||
"membership": mu,
|
|
||||||
"explanation": "El valor está en el lado izquierdo y se interpola entre nodos."
|
|
||||||
}
|
|
||||||
|
|
||||||
if b < x <= d:
|
|
||||||
mu = linear_interpolation(x, request.right_nodes)
|
|
||||||
return {
|
|
||||||
"membership": mu,
|
|
||||||
"explanation": "El valor está en el lado derecho y se interpola entre nodos."
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"membership": 0.0,
|
|
||||||
"explanation": "No se pudo determinar la pertenencia."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENDPOINT 4: PUNTOS (x,y) DE LA FUNCIÓN DE VALOR
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
@app.post("/api/criteria/doc/value-function/points")
|
|
||||||
def value_function_points(request: ValueFunctionRequest):
|
|
||||||
levels = request.levels
|
|
||||||
cards = request.blank_cards
|
|
||||||
refs = request.references
|
|
||||||
|
|
||||||
ref_indices = sorted(int(k) for k in refs)
|
|
||||||
p, q = ref_indices
|
|
||||||
up, uq = refs[str(p)], refs[str(q)]
|
|
||||||
|
|
||||||
total_units = sum(cards[i] + 1 for i in range(p, q))
|
|
||||||
alpha = (uq - up) / total_units if total_units else 0
|
|
||||||
|
|
||||||
values = [0] * len(levels)
|
|
||||||
values[p] = up
|
|
||||||
|
|
||||||
for i in range(p + 1, len(levels)):
|
|
||||||
units = sum(cards[r] + 1 for r in range(p, i))
|
|
||||||
values[i] = up + alpha * units
|
|
||||||
|
|
||||||
for i in range(p - 1, -1, -1):
|
|
||||||
units = sum(cards[r] + 1 for r in range(i, p))
|
|
||||||
values[i] = up - alpha * units
|
|
||||||
|
|
||||||
points = [{"x": i, "y": round(values[i], 4)} for i in range(len(levels))]
|
|
||||||
|
|
||||||
return {"points": points}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
def build_single_docmf(request):
|
||||||
|
a, b = request.core
|
||||||
|
c, d = request.support
|
||||||
|
|
||||||
|
# LEFT
|
||||||
|
TL = sum(e + 1 for e in request.left_blank_cards)
|
||||||
|
YL = 1 / TL
|
||||||
|
left_nodes = []
|
||||||
|
acc = 0
|
||||||
|
|
||||||
|
for i, x in enumerate(request.left_nodes_x):
|
||||||
|
if i == 0:
|
||||||
|
left_nodes.append((x, 0.0))
|
||||||
|
else:
|
||||||
|
acc += request.left_blank_cards[i-1] + 1
|
||||||
|
left_nodes.append((x, round(acc * YL, 4)))
|
||||||
|
|
||||||
|
# RIGHT
|
||||||
|
TR = sum(e + 1 for e in request.right_blank_cards)
|
||||||
|
YR = 1 / TR
|
||||||
|
right_nodes = []
|
||||||
|
acc = 0
|
||||||
|
|
||||||
|
for i, x in enumerate(request.right_nodes_x):
|
||||||
|
if i == 0:
|
||||||
|
right_nodes.append((x, 1.0))
|
||||||
|
else:
|
||||||
|
acc += request.right_blank_cards[i-1] + 1
|
||||||
|
right_nodes.append((x, round(1 - acc * YR, 4)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"term": request.term,
|
||||||
|
"core": request.core,
|
||||||
|
"support": request.support,
|
||||||
|
"left_nodes": left_nodes,
|
||||||
|
"right_nodes": right_nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_docmf_multi(request):
|
||||||
|
results = []
|
||||||
|
for level in request.levels:
|
||||||
|
result = build_single_docmf(level)
|
||||||
|
results.append(result)
|
||||||
|
return {"results": results}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from api.utils.interpolation import linear_interpolation
|
||||||
|
|
||||||
|
def evaluate_docmf(request):
|
||||||
|
x = request.x
|
||||||
|
a, b = request.core
|
||||||
|
c, d = request.support
|
||||||
|
|
||||||
|
if x < c or x > d:
|
||||||
|
return {"membership": 0.0, "explanation": "Fuera del soporte."}
|
||||||
|
|
||||||
|
if a <= x <= b:
|
||||||
|
return {"membership": 1.0, "explanation": "Dentro del núcleo."}
|
||||||
|
|
||||||
|
if c <= x < a:
|
||||||
|
mu = linear_interpolation(x, request.left_nodes)
|
||||||
|
return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.left_nodes} del lado izquierdo."}
|
||||||
|
|
||||||
|
if b < x <= d:
|
||||||
|
mu = linear_interpolation(x, request.right_nodes)
|
||||||
|
return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.right_nodes} del lado derecho."}
|
||||||
|
|
||||||
|
raise ValueError("No se pudo evaluar el valor.")
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
def validate_simple_level(level: dict):
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
a, b = level["core"]
|
||||||
|
c, d = level["support"]
|
||||||
|
|
||||||
|
# Validación: núcleo dentro del soporte
|
||||||
|
if not (c <= a < b <= d):
|
||||||
|
errors.append("El núcleo debe estar completamente dentro del soporte.")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_simple_levels(levels):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for idx, level in enumerate(levels):
|
||||||
|
errors = validate_simple_level(level.dict())
|
||||||
|
results.append({
|
||||||
|
"level_index": idx,
|
||||||
|
"valid": len(errors) == 0,
|
||||||
|
"errors": errors
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
def validate_single_level(level: dict):
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
a, b = level["core"]
|
||||||
|
c, d = level["support"]
|
||||||
|
|
||||||
|
# Core dentro del soporte
|
||||||
|
if not (c <= a < b <= d):
|
||||||
|
errors.append("El núcleo debe estar completamente dentro del soporte.")
|
||||||
|
|
||||||
|
# Nodos cubren correctamente el soporte
|
||||||
|
left = level["left_nodes"]
|
||||||
|
right = level["right_nodes"]
|
||||||
|
|
||||||
|
if left[0][0] != c:
|
||||||
|
errors.append("El primer nodo izquierdo debe coincidir con el inicio del soporte.")
|
||||||
|
if left[-1][0] != a:
|
||||||
|
errors.append("El último nodo izquierdo debe coincidir con el inicio del núcleo.")
|
||||||
|
|
||||||
|
if right[0][0] != b:
|
||||||
|
errors.append("El primer nodo derecho debe coincidir con el final del núcleo.")
|
||||||
|
if right[-1][0] != d:
|
||||||
|
errors.append("El último nodo derecho debe coincidir con el final del soporte.")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_levels(levels):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for idx, level in enumerate(levels):
|
||||||
|
errors = validate_single_level(level.dict())
|
||||||
|
results.append({
|
||||||
|
"level_index": idx,
|
||||||
|
"valid": len(errors) == 0,
|
||||||
|
"errors": errors
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
def compute_value_function(request):
|
||||||
|
levels = request.levels
|
||||||
|
cards = request.blank_cards
|
||||||
|
refs = request.references
|
||||||
|
|
||||||
|
p, q = sorted(int(k) for k in refs)
|
||||||
|
up, uq = refs[str(p)], refs[str(q)]
|
||||||
|
|
||||||
|
if len(levels) < 3:
|
||||||
|
raise ValueError("Mínimo debe haber 3 niveles para esta funcionalidad.")
|
||||||
|
|
||||||
|
total_units = sum(cards[i] + 1 for i in range(p, q))
|
||||||
|
if total_units == 0:
|
||||||
|
raise ValueError("Las cartas no pueden generar 0 unidades.")
|
||||||
|
|
||||||
|
alpha = (uq - up) / total_units
|
||||||
|
|
||||||
|
values = [0] * len(levels)
|
||||||
|
values[p] = up
|
||||||
|
|
||||||
|
for i in range(p + 1, len(levels)):
|
||||||
|
units = sum(cards[r] + 1 for r in range(p, i))
|
||||||
|
values[i] = up + alpha * units
|
||||||
|
|
||||||
|
for i in range(p - 1, -1, -1):
|
||||||
|
units = sum(cards[r] + 1 for r in range(i, p))
|
||||||
|
values[i] = up - alpha * units
|
||||||
|
|
||||||
|
return {
|
||||||
|
"criterion_name": request.criterion_name,
|
||||||
|
"values": {levels[i]: round(values[i], 4) for i in range(len(levels))}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_points(request):
|
||||||
|
result = compute_value_function(request)
|
||||||
|
values = list(result["values"].values())
|
||||||
|
return {"points": [{"x": i, "y": values[i]} for i in range(len(values))]}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
def linear_interpolation(x, nodes):
|
||||||
|
for i in range(len(nodes) - 1):
|
||||||
|
x0, y0 = nodes[i]
|
||||||
|
x1, y1 = nodes[i+1]
|
||||||
|
if x0 <= x <= x1:
|
||||||
|
t = (x - x0) / (x1 - x0)
|
||||||
|
return y0 + t * (y1 - y0)
|
||||||
|
return 0.0
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import secrets
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token() -> str:
|
||||||
|
return secrets.token_hex(32) # 64 caracteres seguros
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
motor
|
||||||
|
pydantic
|
||||||
|
passlib
|
||||||
|
bcrypt==4.0.1
|
||||||
|
email-validator
|
||||||
@@ -1,17 +1,35 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
|
container_name: backend
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
|
container_name: frontend
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mongo:6
|
||||||
|
container_name: mongo
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "27018:27017"
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user