Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,4 @@ def health_check():
return jsonify({
"status": "ok",
"service": "SkillStat API",
}), 200

}), 200
22 changes: 21 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# models/__init__ — SkillStat
from .category import Category
from .city import City
from .skill import Skill
from .job import Job
from .job_skill import JobSkill
from .user import User
from .alert import Alert
from .trend_snapshot import TrendSnapshot
from .backup import Backup

__all__ = [
'Category',
'City',
'Skill',
'Job',
'JobSkill',
'User',
'Alert',
'TrendSnapshot',
'Backup'
]
13 changes: 12 additions & 1 deletion backend/app/models/alert.py
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
# alert — SkillStat
from app.extensions import db
from datetime import datetime, timezone

class Alert(db.Model):
__tablename__ = 'user_alerts'

id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
skill_id = db.Column(db.Integer, db.ForeignKey('skills.id'), nullable=False)
threshold = db.Column(db.Integer, nullable=False)
active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
18 changes: 17 additions & 1 deletion backend/app/models/backup.py
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# backup — SkillStat
from app.extensions import db
from datetime import datetime, timezone

class Backup(db.Model):
__tablename__ = 'backups'

id = db.Column(db.Integer, primary_key=True)
# Habilitamos nulos para soportar la ejecución de respaldos automáticos desde el scheduler sin un usuario físico atado
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
filename = db.Column(db.String(255), nullable=False)
storage_url = db.Column(db.String(500), nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
status = db.Column(db.String(20), nullable=False)

__table_args__ = (
db.CheckConstraint("status IN ('PENDING', 'COMPLETED', 'FAILED')", name='chk_backups_status'),
)
10 changes: 9 additions & 1 deletion backend/app/models/category.py
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
# category — SkillStat
from app.extensions import db

class Category(db.Model):
__tablename__ = 'categories'

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False, unique=True)

skills = db.relationship('Skill', backref='category', lazy=True)
16 changes: 15 additions & 1 deletion backend/app/models/city.py
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# city — SkillStat
from app.extensions import db

class City(db.Model):
__tablename__ = 'cities'

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
state = db.Column(db.String(100), nullable=True)
# Establecemos México como default tanto a nivel aplicación como base de datos
country = db.Column(db.String(10), default='MX', server_default='MX', nullable=True)
Comment on lines +9 to +10
lat = db.Column(db.Numeric(9, 6), nullable=True)
lon = db.Column(db.Numeric(9, 6), nullable=True)

jobs = db.relationship('Job', backref='city', lazy=True)
trend_snapshots = db.relationship('TrendSnapshot', backref='city', lazy=True)
Comment on lines +14 to +15
25 changes: 24 additions & 1 deletion backend/app/models/job.py
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# job — SkillStat
from app.extensions import db
from datetime import datetime, timezone

class Job(db.Model):
__tablename__ = 'jobs'

id = db.Column(db.Integer, primary_key=True)
source = db.Column(db.String(50), nullable=False)
title = db.Column(db.String(255), nullable=False)
company = db.Column(db.String(255), nullable=True)
# Permitimos nulos en city_id para no bloquear la ingesta de vacantes remotas o sin geolocalización
city_id = db.Column(db.Integer, db.ForeignKey('cities.id'), nullable=True)
salary_min = db.Column(db.Numeric(10, 2), nullable=True)
salary_max = db.Column(db.Numeric(10, 2), nullable=True)
raw_description = db.Column(db.Text, nullable=False)

# Guardamos el hash de la descripción para detectar rápidamente si una vacante ya fue procesada o si cambió en su origen
description_hash = db.Column(db.String(64), nullable=False)
processed = db.Column(db.Boolean, default=False, nullable=False)

created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
Comment on lines +21 to +22

job_skills = db.relationship('JobSkill', backref='job', lazy=True)
13 changes: 12 additions & 1 deletion backend/app/models/job_skill.py
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
# job_skill — SkillStat
from app.extensions import db
from datetime import datetime, timezone

class JobSkill(db.Model):
__tablename__ = 'job_skills'

# Utilizamos llave primaria compuesta para evitar identificadores subrogados innecesarios y cumplir la 3FN
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'), primary_key=True)
skill_id = db.Column(db.Integer, db.ForeignKey('skills.id'), primary_key=True)

confidence_score = db.Column(db.Numeric(4, 3), nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
15 changes: 14 additions & 1 deletion backend/app/models/skill.py
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# skill — SkillStat
from app.extensions import db

class Skill(db.Model):
__tablename__ = 'skills'

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
canonical_name = db.Column(db.String(100), nullable=False, unique=True)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)

# Relacionamos bidireccionalmente mediante el patrón Association Object para mantener la normalización 3FN en job_skills
job_skills = db.relationship('JobSkill', backref='skill', lazy=True)
alerts = db.relationship('Alert', backref='skill', lazy=True)
trend_snapshots = db.relationship('TrendSnapshot', backref='skill', lazy=True)
Comment on lines +12 to +14
18 changes: 17 additions & 1 deletion backend/app/models/trend_snapshot.py
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# trend_snapshot — SkillStat
from app.extensions import db

class TrendSnapshot(db.Model):
__tablename__ = 'trend_snapshots'

id = db.Column(db.Integer, primary_key=True)
skill_id = db.Column(db.Integer, db.ForeignKey('skills.id'), nullable=False)
city_id = db.Column(db.Integer, db.ForeignKey('cities.id'), nullable=False)
date = db.Column(db.Date, nullable=False)
demand_count = db.Column(db.Integer, default=0, nullable=True)
growth_rate = db.Column(db.Numeric(6, 2), nullable=True)
avg_salary = db.Column(db.Numeric(10, 2), nullable=True)

# Forzamos unicidad combinada para garantizar que no existan métricas duplicadas para la misma ciudad, habilidad y fecha
__table_args__ = (
db.UniqueConstraint('skill_id', 'city_id', 'date', name='uq_trend_snapshot_skill_city_date'),
)
20 changes: 19 additions & 1 deletion backend/app/models/user.py
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# user — SkillStat
from app.extensions import db
from datetime import datetime, timezone

class User(db.Model):
__tablename__ = 'users'

id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.String(20), nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))

alerts = db.relationship('Alert', backref='user', lazy=True)
backups = db.relationship('Backup', backref='user', lazy=True)
Comment on lines +13 to +14

# Restringimos los roles permitidos directamente en la base de datos por seguridad
__table_args__ = (
db.CheckConstraint("role IN ('GUEST', 'REGISTERED', 'ADMIN')", name='chk_users_role'),
)