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.

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.

Build docs developers (and LLMs) love