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.
Hábito. is structured around a four-layer, MVC-inspired pattern that enforces a clean separation of concerns at every level of the stack. Each layer has a single, well-defined responsibility: Models declare what data looks like, Repositories are the sole gateway to the database, Services perform complex business logic that would otherwise bloat the route handlers, and Routes (Flask Blueprints) translate HTTP requests and form submissions into calls against the lower layers. This strict separation makes it straightforward to test, extend, or replace any individual layer without touching the others.
Project Structure
SISTEMA-HABITOS/
├── main.py # Entry point — app factory + db.create_all()
├── requirements.txt
└── app/
├── __init__.py # Application factory (create_app)
├── models/
│ ├── __init__.py
│ ├── usuario.py # Usuario ORM model
│ ├── habito.py # Habito ORM model
│ └── registro_habito.py # RegistroHabito ORM model
├── repositories/
│ ├── __init__.py
│ ├── usuario_repository.py # DB access for Usuario
│ └── habito_repository.py # DB access for Habito + RegistroHabito
├── services/
│ ├── __init__.py
│ └── estadisticas_grafica.py # Chart generation (Plotly)
├── routes/
│ ├── __init__.py
│ ├── auth.py # Blueprint: /auth
│ ├── pagina_principal.py # Blueprint: /pagina_principal
│ └── habito.py # Blueprint: /habitos
├── templates/ # Jinja2 HTML templates
└── static/ # CSS, JS, and audio assets
Layer 1 — Models (app/models/)
Models define the relational schema using SQLAlchemy’s modern Mapped / mapped_column declarative API (SQLAlchemy 2.0 style). There are three tables: usuarios, habitos, and registro_habitos.
Habito — the central entity
class Habito(db.Model):
__tablename__ = 'habitos'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
usuario_id: Mapped[int] = mapped_column(ForeignKey("usuarios.id"), nullable=False)
nombre: Mapped[str] = mapped_column(String(100), nullable=False)
descripcion: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
dias_semana: Mapped[str] = mapped_column(String(20), nullable=False)
fecha_creacion: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
activo: Mapped[bool] = mapped_column(Boolean, default=True)
racha_actual: Mapped[int] = mapped_column(Integer, default=0)
mejor_racha: Mapped[int] = mapped_column(Integer, default=0)
horario: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
dueno: Mapped["Usuario"] = relationship("Usuario", back_populates="habitos")
registros: Mapped[List["RegistroHabito"]] = relationship(
"RegistroHabito", back_populates="habito", cascade="all, delete-orphan"
)
Key design decisions:
dias_semana stores the user’s chosen days as a comma-separated string (e.g. "L,M,X,J,V,S,D"), where L=Monday, M=Tuesday, X=Wednesday, J=Thursday, V=Friday, S=Saturday, D=Sunday.
racha_actual and mejor_racha are maintained by the Repository layer on every toggle — they are never computed on the fly.
- The
cascade="all, delete-orphan" on registros ensures that deleting a Habito automatically removes all its daily completion records.
RegistroHabito — the daily log
class RegistroHabito(db.Model):
__tablename__ = 'registro_habitos'
__table_args__ = (
UniqueConstraint('habito_id', 'fecha', name='uq_habito_fecha_diaria'),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
habito_id: Mapped[int] = mapped_column(ForeignKey("habitos.id"), nullable=False)
fecha: Mapped[date] = mapped_column(Date, nullable=False, default=date.today)
completado: Mapped[bool] = mapped_column(Boolean, default=False)
habito: Mapped["Habito"] = relationship("Habito", back_populates="registros")
The UniqueConstraint on (habito_id, fecha) enforces the rule that a habit can only be marked complete once per calendar day at the database level.
Layer 2 — Repositories (app/repositories/)
Repositories are the only layer in the application that is allowed to call db.session directly. Every query, insert, update, and delete is encapsulated as a @staticmethod on a repository class. Routes and Services request data through these methods and never touch the ORM session themselves.
HabitoRepository — method signatures
class HabitoRepository:
@staticmethod
def obtener_habitos_por_usuario(usuario_id: int, estado: str = 'todos') -> List[Habito]:
"""Returns all habits for a user, optionally filtered by 'activos' or 'inactivos'."""
@staticmethod
def obtener_habito_por_id(habito_id: int) -> Optional[Habito]:
"""Fetches a single habit by its primary key."""
@staticmethod
def crear_habito(nombre: str, usuario_id: int, dias_semana: str,
descripcion: Optional[str] = None) -> Habito:
"""Inserts a new habit row and returns the persisted object."""
@staticmethod
def editar_habito(habito_id: int, nombre: str, descripcion: Optional[str],
dias_semana: str, activo: bool) -> Optional[Habito]:
"""Updates an existing habit's fields in place."""
@staticmethod
def eliminar_habito(id_habito: int) -> None:
"""Hard-deletes a habit and all its RegistroHabito records (cascade)."""
@staticmethod
def marcar_completado_hoy(id_habito: int) -> bool:
"""Toggles today's completion record. Returns True if marked done, False if unmarked."""
@staticmethod
def obtener_dias_activos_mes(usuario_id: int, anio: int, mes: int) -> set:
"""Returns a set of day-numbers in which the user completed at least one habit."""
marcar_completado_hoy is the most important method. It implements an atomic toggle: if a RegistroHabito exists for today it deletes it (and decrements racha_actual); otherwise it creates one and increments racha_actual, updating mejor_racha if the new streak exceeds the historical record.
UsuarioRepository — authentication helpers
class UsuarioRepository:
@staticmethod
def crear_usuario(correo: str, contrasena_plana: str, nombre_usuario: str) -> Usuario:
"""Hashes the password with bcrypt, then inserts a new Usuario row."""
@staticmethod
def obtener_usuario_por_correo(correo: str) -> Optional[Usuario]:
"""Looks up a user by email address using SQLAlchemy 2.0 select syntax."""
@staticmethod
def verificar_credenciales(correo: str, contrasena_plana: str) -> Optional[Usuario]:
"""Returns the Usuario if credentials are valid, otherwise None."""
Because all database access is centralised in Repositories, the Routes layer never imports db from app. This means you can swap out the underlying SQLAlchemy query implementation, add caching, or introduce a different ORM without touching a single route handler.
Layer 3 — Services (app/services/)
Services encapsulate business logic that is too complex to live in a route handler but does not belong in a Repository (because it does not directly query the database). Currently the single service is EstadisticasService, which processes pre-loaded ORM data and produces embeddable Plotly HTML.
EstadisticasService.generar_grafica_ultimos_dias
class EstadisticasService:
@staticmethod
def generar_grafica_ultimos_dias(registros, dias=30):
"""
Generates an interactive Plotly line chart showing completion
vs. failure for each of the last `dias` days.
Args:
registros: The list of RegistroHabito objects from habito.registros.
dias (int): Number of past days to display. Defaults to 30.
Returns:
str: A self-contained HTML fragment (no <html>/<body> wrapper)
with Plotly JS loaded from CDN, ready to inject into a template.
"""
hoy = date.today()
ultimos_dias = [hoy - timedelta(days=i) for i in range(dias - 1, -1, -1)]
fechas_completadas = {registro.fecha for registro in registros if registro.completado}
eje_x = [d.strftime("%d/%m") for d in ultimos_dias]
eje_y = [1 if d in fechas_completadas else 0 for d in ultimos_dias]
fig = px.line(
x=eje_x,
y=eje_y,
title=f"Progreso de los últimos {dias} días",
markers=True,
color_discrete_sequence=['#235336']
)
# ...layout customisation...
return fig.to_html(full_html=False, include_plotlyjs='cdn')
The route that calls this service (/habitos/visualizar_progreso/<id>) passes habito.registros — the already-loaded SQLAlchemy relationship — directly to the service. The service never touches db.session; it works only with the Python objects it receives.
Layer 4 — Routes (app/routes/)
Routes are Flask Blueprints that handle incoming HTTP requests. Each Blueprint maps to a URL prefix, contains decorated view functions, and delegates all data work to Repositories or Services.
The three Blueprints
# app/routes/auth.py
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
# Endpoints: /auth/registro, /auth/inicio_sesion, /auth/cerrar_sesion
# app/routes/pagina_principal.py
pag_principal_bp = Blueprint('pag_principal', __name__, url_prefix='/pagina_principal')
# Endpoints: /pagina_principal/inicio
# app/routes/habito.py
habito_bp = Blueprint('habitos', __name__, url_prefix='/habitos')
# Endpoints: /habitos/mis_habitos, /habitos/crear, /habitos/editar/<id>,
# /habitos/visualizar_progreso/<id>, /habitos/eliminar/<id>,
# /habitos/completar/<id>
All protected endpoints are decorated with @login_required from Flask-Login, which automatically redirects unauthenticated users to /auth/inicio_sesion.
Application Factory (app/__init__.py)
Hábito. uses the Application Factory pattern, which makes the app instance configurable and testable without running a live server.
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'sistema_habitos_76Fu-39+'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///habitos.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True # prints raw SQL to the console
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.inicio_sesion'
login_manager.login_message = 'Por favor inicia sesión para acceder.'
login_manager.login_message_category = 'error'
from app.models.usuario import Usuario
@login_manager.user_loader
def load_user(user_id: str) -> Optional[Usuario]:
return db.session.get(Usuario, int(user_id))
# Register Blueprints
from app.routes.auth import auth_bp
app.register_blueprint(auth_bp)
from app.routes.pagina_principal import pag_principal_bp
app.register_blueprint(pag_principal_bp)
from app.routes.habito import habito_bp
app.register_blueprint(habito_bp)
@app.route('/')
def index():
return redirect(url_for('auth.inicio_sesion'))
return app
db and login_manager are module-level singletons initialised without an app instance (SQLAlchemy(), LoginManager()). create_app() binds them to the concrete Flask instance via .init_app(app). This is what allows main.py to call create_app() and then open an app_context() to run db.create_all() before the server starts.
SQLALCHEMY_ECHO = True is set in the factory, which means every SQL statement Hábito. executes is printed to your terminal. This is extremely helpful during development for understanding what queries each Repository method generates. Disable it (set to False) in any environment where you don’t want the console flooded with SQL output.