frist commit
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
DATA_DIR = os.environ.get('DATA_DIR', '/app/data')
|
||||
DB_PATH = os.path.join(DATA_DIR, 'anuncios.db')
|
||||
OLD_JSON_PATH = os.path.join(DATA_DIR, 'anuncios.json')
|
||||
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_conn():
|
||||
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def _add_column_if_missing(conn, table, column, col_type):
|
||||
"""SQLite-safe ALTER TABLE ADD COLUMN if the column does not exist."""
|
||||
cur = conn.execute(f"PRAGMA table_info({table})")
|
||||
cols = [row['name'] for row in cur.fetchall()]
|
||||
if column not in cols:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
|
||||
|
||||
|
||||
def init_db():
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS anuncios (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
titulo TEXT NOT NULL,
|
||||
contenido TEXT NOT NULL,
|
||||
categoria TEXT NOT NULL DEFAULT 'general',
|
||||
contacto TEXT NOT NULL DEFAULT 'madrid@craigslist.local',
|
||||
ciudad TEXT NOT NULL DEFAULT 'madrid',
|
||||
fecha TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
_add_column_if_missing(conn, 'anuncios', 'ciudad', "TEXT NOT NULL DEFAULT 'madrid'")
|
||||
_add_column_if_missing(conn, 'anuncios', 'token', "TEXT")
|
||||
conn.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_categoria ON anuncios(categoria)
|
||||
''')
|
||||
conn.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_ciudad ON anuncios(ciudad)
|
||||
''')
|
||||
conn.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_fecha ON anuncios(fecha DESC)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
_migrate_json()
|
||||
|
||||
|
||||
def _migrate_json():
|
||||
if not os.path.exists(OLD_JSON_PATH):
|
||||
return
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute('SELECT COUNT(*) as total FROM anuncios')
|
||||
if cur.fetchone()['total'] > 0:
|
||||
conn.close()
|
||||
return
|
||||
|
||||
with open(OLD_JSON_PATH, 'r', encoding='utf-8') as f:
|
||||
anuncios = json.load(f)
|
||||
|
||||
for a in anuncios:
|
||||
conn.execute('''
|
||||
INSERT INTO anuncios (id, titulo, contenido, categoria, contacto, ciudad, fecha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
a.get('id'),
|
||||
a.get('titulo', ''),
|
||||
a.get('contenido', ''),
|
||||
a.get('categoria', 'general'),
|
||||
a.get('contacto', 'madrid@craigslist.local'),
|
||||
a.get('ciudad', 'madrid'),
|
||||
a.get('fecha', datetime.now().strftime('%Y-%m-%d %H:%M'))
|
||||
))
|
||||
conn.execute("DELETE FROM sqlite_sequence WHERE name='anuncios'")
|
||||
if anuncios:
|
||||
max_id = max(a.get('id', 0) for a in anuncios)
|
||||
conn.execute("INSERT INTO sqlite_sequence(name,seq) VALUES ('anuncios', ?)", (max_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
os.rename(OLD_JSON_PATH, OLD_JSON_PATH + '.backup')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def listar_anuncios(categoria=None, busqueda=None, ciudad=None):
|
||||
query = 'SELECT * FROM anuncios WHERE 1=1'
|
||||
params = []
|
||||
if ciudad:
|
||||
query += ' AND ciudad = ?'
|
||||
params.append(ciudad)
|
||||
if categoria:
|
||||
query += ' AND categoria = ?'
|
||||
params.append(categoria)
|
||||
if busqueda:
|
||||
query += ' AND (titulo LIKE ? OR contenido LIKE ?)'
|
||||
params.extend([f'%{busqueda}%', f'%{busqueda}%'])
|
||||
query += ' ORDER BY fecha DESC'
|
||||
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute(query, params)
|
||||
rows = [dict(r) for r in cur.fetchall()]
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def obtener_anuncio(anuncio_id):
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute('SELECT * FROM anuncios WHERE id = ?', (anuncio_id,))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def crear_anuncio(titulo, contenido, categoria, contacto, ciudad='madrid'):
|
||||
fecha = datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||
token = secrets.token_urlsafe(16)
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute('''
|
||||
INSERT INTO anuncios (titulo, contenido, categoria, contacto, ciudad, fecha, token)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (titulo, contenido, categoria, contacto, ciudad, fecha, token))
|
||||
anuncio_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return anuncio_id, token
|
||||
|
||||
|
||||
def borrar_anuncio(anuncio_id, token=None):
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute('SELECT token FROM anuncios WHERE id = ?', (anuncio_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return False
|
||||
stored_token = row['token']
|
||||
if stored_token and stored_token != token:
|
||||
conn.close()
|
||||
return False
|
||||
cur = conn.execute('DELETE FROM anuncios WHERE id = ?', (anuncio_id,))
|
||||
deleted = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return deleted > 0
|
||||
|
||||
|
||||
def actualizar_anuncio(anuncio_id, titulo=None, contenido=None, categoria=None, contacto=None, ciudad=None):
|
||||
campos = []
|
||||
valores = []
|
||||
if titulo is not None:
|
||||
campos.append('titulo = ?')
|
||||
valores.append(titulo)
|
||||
if contenido is not None:
|
||||
campos.append('contenido = ?')
|
||||
valores.append(contenido)
|
||||
if categoria is not None:
|
||||
campos.append('categoria = ?')
|
||||
valores.append(categoria)
|
||||
if contacto is not None:
|
||||
campos.append('contacto = ?')
|
||||
valores.append(contacto)
|
||||
if ciudad is not None:
|
||||
campos.append('ciudad = ?')
|
||||
valores.append(ciudad)
|
||||
if not campos:
|
||||
return False
|
||||
valores.append(anuncio_id)
|
||||
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute(
|
||||
f"UPDATE anuncios SET {', '.join(campos)} WHERE id = ?",
|
||||
valores
|
||||
)
|
||||
updated = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return updated > 0
|
||||
|
||||
|
||||
def listar_categorias(ciudad=None):
|
||||
query = 'SELECT DISTINCT categoria FROM anuncios WHERE 1=1'
|
||||
params = []
|
||||
if ciudad:
|
||||
query += ' AND ciudad = ?'
|
||||
params.append(ciudad)
|
||||
query += ' ORDER BY categoria'
|
||||
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
cur = conn.execute(query, params)
|
||||
rows = [r['categoria'] for r in cur.fetchall()]
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed / migración helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TITULOS = {
|
||||
'trabajo': [
|
||||
'Desarrollador backend senior',
|
||||
'Community manager freelance',
|
||||
'Repartidor con moto propia',
|
||||
'Camarero/a para fin de semana',
|
||||
'Diseñador UX/UI remoto',
|
||||
'Ingeniero devops',
|
||||
'Profesor de inglés nativo',
|
||||
'Ayudante de almacén',
|
||||
],
|
||||
'venta': [
|
||||
'Sofa chaise longue casi nuevo',
|
||||
'iPhone 14 pro 256gb',
|
||||
'Bicicleta de montaña trek',
|
||||
'Libros de programación pack',
|
||||
'Monitor 27 pulgadas 4k',
|
||||
'Nevera combi no frost',
|
||||
'Coche citroen c3 2019',
|
||||
'Patinete electrico xiaomi',
|
||||
],
|
||||
'servicios': [
|
||||
'Limpieza de hogar por horas',
|
||||
'Reparación de electrodomesticos',
|
||||
'Clases particulares de mates',
|
||||
'Diseño web profesional',
|
||||
'Fontanero económico',
|
||||
'Paseador de perros',
|
||||
'Traducciones inglés-español',
|
||||
'Electricista autorizado',
|
||||
],
|
||||
'alquiler': [
|
||||
'Piso centro 2 habitaciones',
|
||||
'Habitación en piso compartido',
|
||||
'Estudio luminoso con terraza',
|
||||
'Alquiler temporal verano',
|
||||
'Duplex con parking incluido',
|
||||
'Chalet adosado zona norte',
|
||||
'Loft industrial reformado',
|
||||
'Apartamento playa primera línea',
|
||||
],
|
||||
'compra': [
|
||||
'Busco piso en el centro',
|
||||
'Compro coche gasolina manual',
|
||||
'Se busca mobiliario vintage',
|
||||
'Quiero comprar portatil gaming',
|
||||
'Busco terreno para autocaravana',
|
||||
'Compro material de escalada',
|
||||
'Se busca piano de pared',
|
||||
'Quiero comprar patinete eléctrico',
|
||||
],
|
||||
'eventos': [
|
||||
'Concierto indie en la sala',
|
||||
'Fiesta de inauguración local',
|
||||
'Mercadillo artesanal domingo',
|
||||
'Taller de cocina vegana',
|
||||
'Ruta senderista guiada',
|
||||
'Exposición fotográfica gratis',
|
||||
'Open mic comedy night',
|
||||
'Yoga en el parque sábados',
|
||||
],
|
||||
'tecnologia': [
|
||||
'Raspberry pi 4 kit completo',
|
||||
'Teclado mecánico custom',
|
||||
'Router mesh wifi 6',
|
||||
'Gafas realidad virtual',
|
||||
'Disco duro externo 2tb',
|
||||
'Webcam 4k para streaming',
|
||||
'Impresora 3d ender 3',
|
||||
'Switch oled con juegos',
|
||||
],
|
||||
'hogar': [
|
||||
'Lámpara de pie nordica',
|
||||
'Juego de sillas de comedor',
|
||||
'Cortinas blackout a medida',
|
||||
'Aspirador robot con mapeo',
|
||||
'Planta monstera grande',
|
||||
'Mesa de centro elevable',
|
||||
'Colchón viscoelástico 150',
|
||||
'Cubertería acero inox 72 piezas',
|
||||
],
|
||||
'ocio': [
|
||||
'Entradas cine miercoles',
|
||||
'Mazo cartas magic coleccion',
|
||||
'Patines en línea talla 42',
|
||||
'Videojuego elden ring ps5',
|
||||
'Kitesurf completo principiante',
|
||||
'Libro cocina japonesa',
|
||||
'Entrada festival música',
|
||||
'Guitarra acústica yamaha',
|
||||
],
|
||||
'contactos': [
|
||||
'Busco compañero de piso',
|
||||
'Quedada rol en la taberna',
|
||||
'Club de lectura mensual',
|
||||
'Grupo de running mañanas',
|
||||
'Busco duo para ranked lol',
|
||||
'Quedada idiomas intercambio',
|
||||
'Compañero de escalada',
|
||||
'Busco banda para batería',
|
||||
],
|
||||
}
|
||||
|
||||
_DESCRIPCIONES = {
|
||||
'trabajo': 'Horario flexible. Se valora experiencia previa. Envía CV por correo.',
|
||||
'venta': 'En perfecto estado. Precio negociable. Entrega en mano preferible.',
|
||||
'servicios': 'Presupuesto sin compromiso. Atención rápida y profesional.',
|
||||
'alquiler': 'Sin comisión de agencia. Gastos incluidos o no según caso.',
|
||||
'compra': 'Busco buen estado y precio razonable. Pago al contado.',
|
||||
'eventos': 'Entrada libre hasta completar aforo. Más info por privado.',
|
||||
'tecnologia': 'Con garantía y caja original. Factura disponible.',
|
||||
'hogar': 'Muy cuidado, sin ralladuras. Recogida en domicilio.',
|
||||
'ocio': 'Perfecto para regalo o uso personal. Consulta detalles.',
|
||||
'contactos': 'Gente con buen rollo y ganas de pasarlo bien. Únete!',
|
||||
}
|
||||
|
||||
_CATEGORIAS = list(_TITULOS.keys())
|
||||
|
||||
|
||||
def _random_date(days_back=90):
|
||||
delta = timedelta(days=random.randint(0, days_back), hours=random.randint(0, 23))
|
||||
return (datetime.now() - delta).strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
|
||||
def seed_anuncios(ciudades, por_ciudad=4):
|
||||
"""Genera anuncios aleatorios distribuidos por ciudades."""
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
for ciudad in ciudades:
|
||||
for _ in range(por_ciudad):
|
||||
cat = random.choice(_CATEGORIAS)
|
||||
titulo = random.choice(_TITULOS[cat])
|
||||
contenido = _DESCRIPCIONES[cat]
|
||||
contacto = f"{ciudad}@craigslist.local"
|
||||
fecha = _random_date()
|
||||
conn.execute('''
|
||||
INSERT INTO anuncios (titulo, contenido, categoria, contacto, ciudad, fecha)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (titulo, contenido, cat, contacto, ciudad, fecha))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def migrar_ciudad_existentes(ciudad_default='madrid'):
|
||||
"""Asigna ciudad a anuncios que aún no la tengan definida."""
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
conn.execute('''
|
||||
UPDATE anuncios SET ciudad = ? WHERE ciudad IS NULL OR ciudad = ''
|
||||
''', (ciudad_default,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user