diff --git a/backend/Dockerfile b/backend/Dockerfile index b862b2b..3639ea9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,10 @@ FROM python:3.10-slim WORKDIR /app -RUN pip install fastapi uvicorn +COPY requirements.txt . -CMD ["uvicorn", "api.routes:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/api/database/mongodb.py b/backend/api/database/mongodb.py new file mode 100644 index 0000000..fbe5848 --- /dev/null +++ b/backend/api/database/mongodb.py @@ -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"] diff --git a/backend/api/main.py b/backend/api/main.py new file mode 100644 index 0000000..1edd7e5 --- /dev/null +++ b/backend/api/main.py @@ -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") \ No newline at end of file diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py new file mode 100644 index 0000000..0cf9342 --- /dev/null +++ b/backend/api/models/docmf_models.py @@ -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] diff --git a/backend/api/models/docmf_simple_validation_models.py b/backend/api/models/docmf_simple_validation_models.py new file mode 100644 index 0000000..246e5d5 --- /dev/null +++ b/backend/api/models/docmf_simple_validation_models.py @@ -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] diff --git a/backend/api/models/docmf_validation_models.py b/backend/api/models/docmf_validation_models.py new file mode 100644 index 0000000..e62907d --- /dev/null +++ b/backend/api/models/docmf_validation_models.py @@ -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] diff --git a/backend/api/models/evaluation_models.py b/backend/api/models/evaluation_models.py new file mode 100644 index 0000000..ca18221 --- /dev/null +++ b/backend/api/models/evaluation_models.py @@ -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 diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py new file mode 100644 index 0000000..c611c6f --- /dev/null +++ b/backend/api/models/user_models.py @@ -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] = [] + diff --git a/backend/api/models/value_function_models.py b/backend/api/models/value_function_models.py new file mode 100644 index 0000000..66aaed5 --- /dev/null +++ b/backend/api/models/value_function_models.py @@ -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 diff --git a/backend/api/routers/auth.py b/backend/api/routers/auth.py new file mode 100644 index 0000000..2287379 --- /dev/null +++ b/backend/api/routers/auth.py @@ -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"} diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py new file mode 100644 index 0000000..24467c4 --- /dev/null +++ b/backend/api/routers/docmf_build.py @@ -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)) diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py new file mode 100644 index 0000000..3024fe8 --- /dev/null +++ b/backend/api/routers/docmf_evaluate.py @@ -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)) diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py new file mode 100644 index 0000000..425384a --- /dev/null +++ b/backend/api/routers/docmf_simple_validation.py @@ -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 + } diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py new file mode 100644 index 0000000..92eda8d --- /dev/null +++ b/backend/api/routers/docmf_validation.py @@ -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 + } diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py new file mode 100644 index 0000000..fadd769 --- /dev/null +++ b/backend/api/routers/history.py @@ -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"} diff --git a/backend/api/routers/test_mongo.py b/backend/api/routers/test_mongo.py new file mode 100644 index 0000000..43dd15d --- /dev/null +++ b/backend/api/routers/test_mongo.py @@ -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)} diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py new file mode 100644 index 0000000..0dfc5a4 --- /dev/null +++ b/backend/api/routers/value_function.py @@ -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)) diff --git a/backend/api/routes.py b/backend/api/routes.py deleted file mode 100644 index 10544bc..0000000 --- a/backend/api/routes.py +++ /dev/null @@ -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} diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py new file mode 100644 index 0000000..7c88660 --- /dev/null +++ b/backend/api/services/docmf_build_service.py @@ -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} diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py new file mode 100644 index 0000000..5b31eb0 --- /dev/null +++ b/backend/api/services/docmf_evaluate_service.py @@ -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.") diff --git a/backend/api/services/docmf_simple_validation_service.py b/backend/api/services/docmf_simple_validation_service.py new file mode 100644 index 0000000..253f910 --- /dev/null +++ b/backend/api/services/docmf_simple_validation_service.py @@ -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 diff --git a/backend/api/services/docmf_validation_service.py b/backend/api/services/docmf_validation_service.py new file mode 100644 index 0000000..97ccfa5 --- /dev/null +++ b/backend/api/services/docmf_validation_service.py @@ -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 diff --git a/backend/api/services/value_function_service.py b/backend/api/services/value_function_service.py new file mode 100644 index 0000000..07039b0 --- /dev/null +++ b/backend/api/services/value_function_service.py @@ -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))]} diff --git a/backend/api/utils/interpolation.py b/backend/api/utils/interpolation.py new file mode 100644 index 0000000..ccfc35f --- /dev/null +++ b/backend/api/utils/interpolation.py @@ -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 diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py new file mode 100644 index 0000000..6dd73c5 --- /dev/null +++ b/backend/api/utils/security.py @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..26ebe6e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +motor +pydantic +passlib +bcrypt==4.0.1 +email-validator diff --git a/docker-compose.yaml b/docker-compose.yaml index 38e74c6..f0fa360 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,35 @@ +version: "3.9" + services: backend: build: context: ./backend + container_name: backend ports: - "8000:8000" volumes: - ./backend:/app + depends_on: + - db frontend: build: context: ./frontend + container_name: frontend ports: - "5173:5173" volumes: - ./frontend:/app - - /app/node_modules \ No newline at end of file + - /app/node_modules + + db: + image: mongo:6 + container_name: mongo + restart: always + ports: + - "27018:27017" + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: