Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/BrandonCVale/SISTEMA-HABITOS/llms.txt

Use this file to discover all available pages before exploring further.

The Repository pattern is a data-access design pattern in which all SQL queries are centralised inside dedicated “repository” classes, completely separate from the HTTP routes that consume them. In Hábito., HabitoRepository and UsuarioRepository are the only places in the codebase that call db.session directly. Routes call repository methods; they never construct queries themselves. This separation keeps the route code thin and readable, makes the query logic easy to locate and change in one place, and makes it straightforward to swap or mock the data layer in tests without touching any controller code.
Routes never call db.session directly. If you extend Hábito., follow the same pattern — add new database operations as static methods on the relevant Repository class, then call that method from the route.
Both repository classes live under app/repositories/ and contain only @staticmethod methods. No instantiation is needed — you call them as HabitoRepository.crear_habito(...) directly.

HabitoRepository

Source: app/repositories/habito_repository.py HabitoRepository owns all queries against the habitos and registro_habitos tables. It is imported by app/routes/habito.py and app/routes/pagina_principal.py.

obtener_habitos_por_usuario

@staticmethod
def obtener_habitos_por_usuario(usuario_id: int, estado: str = 'todos') -> List[Habito]:
Returns every Habito row belonging to usuario_id. The estado parameter adds an optional activo filter:
estadoSQL filter added
'activos'WHERE activo = TRUE
'inactivos'WHERE activo = FALSE
'todos' (default)No filter
consulta = db.select(Habito).where(Habito.usuario_id == usuario_id)

if estado == 'activos':
    consulta = consulta.where(Habito.activo == True)
elif estado == 'inactivos':
    consulta = consulta.where(Habito.activo == False)

return db.session.execute(consulta).scalars().all()

obtener_habito_por_id

@staticmethod
def obtener_habito_por_id(habito_id: int) -> Optional[Habito]:
Fetches a single Habito by primary key using db.session.get(). Returns None if no row with that ID exists. This is the method called by all routes that need to verify ownership before proceeding.
return db.session.get(Habito, habito_id)

crear_habito

@staticmethod
def crear_habito(
    nombre: str,
    usuario_id: int,
    dias_semana: str,
    descripcion: Optional[str] = None
) -> Habito:
Constructs a new Habito object, adds it to the session, commits, and returns the persisted object. The descripcion parameter is optional and defaults to None (stored as NULL).
nuevo_habito = Habito(
    nombre=nombre,
    descripcion=descripcion,
    usuario_id=usuario_id,
    dias_semana=dias_semana
)
db.session.add(nuevo_habito)
db.session.commit()
return nuevo_habito

editar_habito

@staticmethod
def editar_habito(
    habito_id: int,
    nombre: str,
    descripcion: Optional[str],
    dias_semana: str,
    activo: bool
) -> Optional[Habito]:
Looks up the habit by habito_id. If it does not exist, returns None. Otherwise updates the four mutable fields in-place and commits. Returning the updated object allows the caller to inspect the new state if needed.
habito = db.session.get(Habito, habito_id)
if not habito:
    return None

habito.nombre = nombre
habito.descripcion = descripcion
habito.dias_semana = dias_semana
habito.activo = activo

db.session.commit()
return habito

eliminar_habito

@staticmethod
def eliminar_habito(id_habito: int) -> None:
Deletes the Habito row with the given ID. Because the model declares cascade="all, delete-orphan" on the registros relationship, all associated RegistroHabito rows are also deleted by SQLAlchemy in the same transaction. The method is a no-op if the habit does not exist.
habito = db.session.get(Habito, id_habito)
if habito:
    db.session.delete(habito)
    db.session.commit()

marcar_completado_hoy

@staticmethod
def marcar_completado_hoy(id_habito: int) -> bool:
Toggles today’s completion state for the given habit. The method first queries registro_habitos for a row matching (habito_id, date.today()):
  • If a record exists → delete it (unmark), subtract 1 from racha_actual (floor 0), commit, return False.
  • If no record exists → create one with completado=True, increment racha_actual, update mejor_racha if the new streak exceeds the previous record, commit, return True.
The boolean return value is used by the /habitos/completar/<id> route to choose the appropriate flash message.
hoy = date.today()

consulta = db.select(RegistroHabito).where(
    RegistroHabito.habito_id == id_habito,
    RegistroHabito.fecha == hoy
)
registro_existente = db.session.execute(consulta).scalar_one_or_none()

habito = db.session.get(Habito, id_habito)

if registro_existente:
    db.session.delete(registro_existente)
    habito.racha_actual = max(0, habito.racha_actual - 1)
    db.session.commit()
    return False
else:
    nuevo_registro = RegistroHabito(habito_id=id_habito, fecha=hoy, completado=True)
    db.session.add(nuevo_registro)
    habito.racha_actual += 1
    if habito.racha_actual > habito.mejor_racha:
        habito.mejor_racha = habito.racha_actual
    db.session.commit()
    return True

obtener_dias_activos_mes

@staticmethod
def obtener_dias_activos_mes(usuario_id: int, anio: int, mes: int) -> set:
Returns a Python set of integer day-numbers (e.g. {1, 4, 5, 12}) representing every calendar day in the given month on which the user completed at least one habit. The result is used by the dashboard to paint the heat-map calendar. The query performs a JOIN between RegistroHabito and Habito to filter by usuario_id, and bounds the date range to the first and last day of the requested month:
_, num_dias = calendar.monthrange(anio, mes)
primer_dia = date(anio, mes, 1)
ultimo_dia = date(anio, mes, num_dias)

consulta = db.select(RegistroHabito.fecha).join(Habito).where(
    Habito.usuario_id == usuario_id,
    RegistroHabito.fecha >= primer_dia,
    RegistroHabito.fecha <= ultimo_dia,
    RegistroHabito.completado == True
)
fechas = db.session.execute(consulta).scalars().all()

return {f.day for f in fechas}

UsuarioRepository

Source: app/repositories/usuario_repository.py UsuarioRepository owns all queries against the usuarios table, including password hashing and credential verification. It is imported exclusively by app/routes/auth.py.

crear_usuario

@staticmethod
def crear_usuario(correo: str, contrasena_plana: str, nombre_usuario: str) -> Usuario:
Hashes the plain-text password with bcrypt (using a randomly generated salt), constructs a Usuario object with the decoded hash string, adds it to the session, and commits. Raises sqlalchemy.exc.IntegrityError if either correo or nombre_usuario already exists (both columns have unique=True). The caller (auth.registro route) catches this exception and shows an error flash.
salt = bcrypt.gensalt()
hash_contrasena = bcrypt.hashpw(contrasena_plana.encode('utf-8'), salt)

nuevo_usuario = Usuario(
    correo=correo,
    contrasena=hash_contrasena.decode('utf-8'),
    nombre_usuario=nombre_usuario
)
db.session.add(nuevo_usuario)
db.session.commit()
return nuevo_usuario

obtener_usuario_por_correo

@staticmethod
def obtener_usuario_por_correo(correo: str) -> Optional[Usuario]:
Queries the usuarios table for a row matching the given correo value. Returns the Usuario object if found, or None if not. Uses scalar_one_or_none() — it will not raise an exception if the row is missing.
consulta = db.select(Usuario).where(Usuario.correo == correo)
resultado = db.session.execute(consulta).scalar_one_or_none()
return resultado

verificar_credenciales

@staticmethod
def verificar_credenciales(correo: str, contrasena_plana: str) -> Optional[Usuario]:
Two-step credential check used by the login route:
  1. Calls obtener_usuario_por_correo(correo) — returns None immediately if the email is not found.
  2. Re-encodes both the submitted plain-text password and the stored hash as bytes, then calls bcrypt.checkpw(). Returns the Usuario object on success, or None on a hash mismatch.
usuario = UsuarioRepository.obtener_usuario_por_correo(correo)
if not usuario:
    return None

contrasena_bytes = contrasena_plana.encode('utf-8')
hash_bytes = usuario.contrasena.encode('utf-8')

if bcrypt.checkpw(contrasena_bytes, hash_bytes):
    return usuario
return None

SQLAlchemy 2.0 Query Style

All queries in both repositories use the modern SQLAlchemy 2.0 Core-style select API rather than the legacy Model.query interface (which is deprecated and removed in SQLAlchemy 2.x).

Modern style (used in Hábito.)

consulta = db.select(Habito).where(
    Habito.usuario_id == usuario_id
)
results = db.session.execute(consulta).scalars().all()

Legacy style (not used)

# Deprecated — do not add new code like this
results = Habito.query.filter_by(
    usuario_id=usuario_id
).all()
Key patterns seen throughout the codebase:
PatternDescription
db.select(Model).where(...)Build a SELECT statement
db.session.execute(consulta).scalars().all()Execute and return all matching objects
db.session.execute(consulta).scalar_one_or_none()Execute and return one object or None
db.session.get(Model, pk)Fetch by primary key (identity map-aware)
db.session.add(obj) + db.session.commit()INSERT or track changes and persist
db.session.delete(obj) + db.session.commit()DELETE the row

Build docs developers (and LLMs) love