commit 52977daf721367089fb8ff01692c99bc4a5dadb8 Author: Juan Marquez Date: Mon May 4 19:15:00 2026 +0200 frist commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81852f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y openssh-server && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Crear directorio de datos con permisos abiertos para lectura del usuario ssh +RUN mkdir -p /app/data && chmod 777 /app/data + +# Configurar SSH +RUN mkdir -p /var/run/sshd && \ + useradd -m -s /bin/bash craig && \ + echo 'craig:madrid' | chpasswd && \ + mkdir -p /home/craig/.ssh && \ + chown craig:craig /home/craig/.ssh + +# Habilitar autenticacion por contrasena +RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ + sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config + +# Al conectar como craig, forzar la ejecucion del visor de anuncios +RUN echo '' >> /etc/ssh/sshd_config && \ + echo 'Match User craig' >> /etc/ssh/sshd_config && \ + echo ' ForceCommand python3 /app/ssh_display.py' >> /etc/ssh/sshd_config && \ + echo ' PermitTTY yes' >> /etc/ssh/sshd_config && \ + echo ' AllowTcpForwarding no' >> /etc/ssh/sshd_config && \ + echo ' X11Forwarding no' >> /etc/ssh/sshd_config + +RUN chmod +x /app/start.sh + +EXPOSE 5000 22 + +CMD ["/app/start.sh"] diff --git a/__pycache__/api.cpython-314.pyc b/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000..fb983a9 Binary files /dev/null and b/__pycache__/api.cpython-314.pyc differ diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc new file mode 100644 index 0000000..08a7597 Binary files /dev/null and b/__pycache__/app.cpython-314.pyc differ diff --git a/__pycache__/models.cpython-314.pyc b/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000..cd579c4 Binary files /dev/null and b/__pycache__/models.cpython-314.pyc differ diff --git a/__pycache__/seed.cpython-314.pyc b/__pycache__/seed.cpython-314.pyc new file mode 100644 index 0000000..800b512 Binary files /dev/null and b/__pycache__/seed.cpython-314.pyc differ diff --git a/__pycache__/ssh_display.cpython-314.pyc b/__pycache__/ssh_display.cpython-314.pyc new file mode 100644 index 0000000..1c494ad Binary files /dev/null and b/__pycache__/ssh_display.cpython-314.pyc differ diff --git a/api.py b/api.py new file mode 100644 index 0000000..d24d52f --- /dev/null +++ b/api.py @@ -0,0 +1,66 @@ +from flask import Blueprint, request, jsonify +import models + +api_bp = Blueprint('api', __name__, url_prefix='/api') + + +@api_bp.route('/anuncios', methods=['GET']) +def api_listar(): + categoria = request.args.get('categoria', '').strip() or None + busqueda = request.args.get('q', '').strip() or None + ciudad = request.args.get('ciudad', '').strip() or None + anuncios = models.listar_anuncios(categoria=categoria, busqueda=busqueda, ciudad=ciudad) + return jsonify(anuncios) + + +@api_bp.route('/anuncios/', methods=['GET']) +def api_ver(anuncio_id): + anuncio = models.obtener_anuncio(anuncio_id) + if not anuncio: + return jsonify({'error': 'Anuncio no encontrado'}), 404 + return jsonify(anuncio) + + +@api_bp.route('/anuncios', methods=['POST']) +def api_crear(): + data = request.get_json(force=True, silent=True) or request.form + titulo = (data.get('titulo') or '').strip() + contenido = (data.get('contenido') or '').strip() + categoria = (data.get('categoria') or 'general').strip() + contacto = (data.get('contacto') or 'madrid@craigslist.local').strip() + ciudad = (data.get('ciudad') or 'madrid').strip() + + if not titulo or not contenido: + return jsonify({'error': 'Titulo y contenido son obligatorios'}), 400 + + anuncio_id, token = models.crear_anuncio(titulo, contenido, categoria, contacto, ciudad) + return jsonify({'id': anuncio_id, 'token': token, 'message': 'Anuncio creado'}), 201 + + +@api_bp.route('/anuncios/', methods=['DELETE']) +def api_borrar(anuncio_id): + data = request.get_json(force=True, silent=True) or request.form + token = (data.get('token') or '').strip() + if models.borrar_anuncio(anuncio_id, token): + return jsonify({'message': 'Anuncio borrado'}), 200 + return jsonify({'error': 'Anuncio no encontrado o token incorrecto'}), 404 + + +@api_bp.route('/anuncios/', methods=['PUT']) +def api_actualizar(anuncio_id): + data = request.get_json(force=True, silent=True) or request.form + campos = {} + for k in ('titulo', 'contenido', 'categoria', 'contacto', 'ciudad'): + if k in data: + campos[k] = data[k] + if not campos: + return jsonify({'error': 'No hay campos para actualizar'}), 400 + if models.actualizar_anuncio(anuncio_id, **campos): + return jsonify({'message': 'Anuncio actualizado'}) + return jsonify({'error': 'Anuncio no encontrado'}), 404 + + +@api_bp.route('/categorias', methods=['GET']) +def api_categorias(): + ciudad = request.args.get('ciudad', '').strip() or None + return jsonify(models.listar_categorias(ciudad=ciudad)) diff --git a/app.py b/app.py new file mode 100644 index 0000000..9a8065c --- /dev/null +++ b/app.py @@ -0,0 +1,62 @@ +from flask import Flask, render_template, request, redirect, url_for +import models +from api import api_bp + +app = Flask(__name__) +app.register_blueprint(api_bp) + +models.init_db() + +CIUDADES = [ + 'madrid', 'barcelona', 'valencia', 'sevilla', 'zaragoza', + 'malaga', 'murcia', 'palma', 'las-palmas', 'bilbao', + 'alicante', 'cordoba', 'valladolid', 'vigo' +] + + +@app.route('/') +def index(): + return render_template('index.html', modo='inicio', ciudades=CIUDADES) + + +@app.route('/c/') +def ciudad(city): + categoria = request.args.get('categoria', '').strip() or None + busqueda = request.args.get('q', '').strip() or None + anuncios = models.listar_anuncios(categoria=categoria, busqueda=busqueda, ciudad=city) + categorias = models.listar_categorias(ciudad=city) + return render_template( + 'index.html', + modo='ciudad', + city=city, + anuncios=anuncios, + categorias=categorias, + categoria_actual=categoria or '', + busqueda_actual=busqueda or '' + ) + + +@app.route('/nuevo', methods=['POST']) +def nuevo(): + titulo = request.form.get('titulo', '').strip() + contenido = request.form.get('contenido', '').strip() + categoria = request.form.get('categoria', 'general').strip() + contacto = request.form.get('contacto', 'madrid@craigslist.local').strip() + ciudad = request.form.get('ciudad', 'madrid').strip() + + if titulo and contenido: + anuncio_id, token = models.crear_anuncio(titulo, contenido, categoria, contacto, ciudad) + return redirect(f'/c/{ciudad}?nuevo={anuncio_id}&token={token}') + + return redirect(url_for('index')) + + +@app.route('/borrar/', methods=['POST']) +def borrar(anuncio_id): + token = request.form.get('token', '').strip() + models.borrar_anuncio(anuncio_id, token) + return redirect(request.referrer or '/') + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/data/anuncios.db b/data/anuncios.db new file mode 100644 index 0000000..cede400 Binary files /dev/null and b/data/anuncios.db differ diff --git a/data/anuncios.db.old b/data/anuncios.db.old new file mode 100644 index 0000000..c060780 Binary files /dev/null and b/data/anuncios.db.old differ diff --git a/data/anuncios.json.backup b/data/anuncios.json.backup new file mode 100644 index 0000000..aa87996 --- /dev/null +++ b/data/anuncios.json.backup @@ -0,0 +1,34 @@ +[ + { + "id": 4, + "titulo": "test", + "contenido": "test", + "categoria": "general", + "fecha": "2026-04-30 14:42", + "contacto": "madrid@craigslist.local" + }, + { + "id": 1, + "titulo": "Vendo bicicleta de carretera", + "contenido": "Bicicleta de carretera en buen estado. Precio negociable. Zona Chamberi, Madrid.", + "categoria": "venta", + "fecha": "2025-04-30 10:00", + "contacto": "bicis@madrid.es" + }, + { + "id": 2, + "titulo": "Clases de guitarra flamenca", + "contenido": "Profesor con 10 anos de experiencia. Clases en persona o online. Primera clase gratis.", + "categoria": "servicios", + "fecha": "2025-04-30 11:30", + "contacto": "guitarra@flamenco.local" + }, + { + "id": 3, + "titulo": "Se busca companero de piso", + "contenido": "Piso compartido en Malasana. 400 EUR/mes gastos incluidos. No fumadores.", + "categoria": "vivienda", + "fecha": "2025-04-30 09:15", + "contacto": "piso@malasana.local" + } +] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..34c9cc0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + madcraig: + build: . + ports: + - "5000:5000" + - "2222:22" + volumes: + - ./data:/app/data diff --git a/models.py b/models.py new file mode 100644 index 0000000..b0a0c8a --- /dev/null +++ b/models.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..047e950 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.0 diff --git a/seed.py b/seed.py new file mode 100644 index 0000000..ffdb9e8 --- /dev/null +++ b/seed.py @@ -0,0 +1,33 @@ +import os +import sqlite3 +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import models + +CIUDADES = [ + 'madrid', 'barcelona', 'valencia', 'sevilla', 'zaragoza', + 'malaga', 'murcia', 'palma', 'las-palmas', 'bilbao', + 'alicante', 'cordoba', 'valladolid', 'vigo' +] + +if __name__ == '__main__': + print("Inicializando base de datos...") + models.init_db() + + print("Migrando ciudad en anuncios existentes...") + models.migrar_ciudad_existentes(ciudad_default='madrid') + + conn = sqlite3.connect(os.path.join(os.environ.get('DATA_DIR', '/app/data'), 'anuncios.db')) + cur = conn.execute('SELECT COUNT(*) FROM anuncios') + count = cur.fetchone()[0] + conn.close() + + if count == 0: + print("Generando anuncios aleatorios...") + models.seed_anuncios(ciudades=CIUDADES, por_ciudad=4) + else: + print(f"Ya existen {count} anuncios. No se generan seeds nuevos.") + + print("Listo.") diff --git a/ssh_display.py b/ssh_display.py new file mode 100644 index 0000000..7b55455 --- /dev/null +++ b/ssh_display.py @@ -0,0 +1,281 @@ +import os +import sys +import textwrap + +import models + + +def limpiar(): + os.system('clear' if os.name != 'nt' else 'cls') + + +def pausa(): + try: + input("\n [ENTER para continuar]") + except (EOFError, KeyboardInterrupt): + pass + + +def leer_linea(prompt, por_defecto=''): + try: + if por_defecto: + val = input(f" {prompt} [{por_defecto}]: ").strip() + return val if val else por_defecto + return input(f" {prompt}: ").strip() + except (EOFError, KeyboardInterrupt): + return None + + +def cabecera(titulo=""): + limpiar() + print() + print("=" * 64) + print(" MadCraig :: Madrid, Espana :: Terminal / SSH") + if titulo: + print(f" {titulo}") + print("=" * 64) + print() + + +def formatear_anuncio(a, idx=None): + prefijo = f" [{idx}] " if idx is not None else " " + lineas = [] + lineas.append(f"{prefijo}[{a['categoria'].upper()}] {a['fecha']} (ID: {a['id']})") + lineas.append(f" Titulo: {a['titulo']}") + lineas.append(f" Contacto: {a['contacto']}") + lineas.append(" " + "-" * 58) + for parrafo in textwrap.wrap(a['contenido'], width=58): + lineas.append(f" {parrafo}") + lineas.append(" " + "=" * 58) + return "\n".join(lineas) + + +def menu_principal(): + cabecera("Menu Principal") + print(" [L] Listar todos los anuncios") + print(" [C] Filtrar por categoria") + print(" [B] Buscar anuncios") + print(" [N] Nuevo anuncio") + print(" [M] Modificar anuncio") + print(" [E] Eliminar anuncio") + print(" [V] Ver anuncio por ID") + print(" [S] Salir") + print() + try: + op = input(" Opcion: ").strip().lower() + except (EOFError, KeyboardInterrupt): + op = 's' + return op + + +def pantalla_listar(anuncios=None, titulo="Anuncios"): + if anuncios is None: + anuncios = models.listar_anuncios() + cabecera(titulo) + if not anuncios: + print(" No hay anuncios.") + pausa() + return + for i, a in enumerate(anuncios, start=1): + print(formatear_anuncio(a, idx=i)) + print() + print(f" Total: {len(anuncios)} anuncio(s)") + pausa() + + +def pantalla_filtrar_categoria(): + cabecera("Filtrar por categoria") + categorias = models.listar_categorias() + if not categorias: + print(" No hay categorias disponibles.") + pausa() + return + print(" Categorias disponibles:") + for i, cat in enumerate(categorias, start=1): + print(f" [{i}] {cat}") + print() + cat = leer_linea("Nombre de la categoria") + if cat is None: + return + if not cat: + print(" Categoria vacia, mostrando todos.") + pausa() + pantalla_listar() + return + anuncios = models.listar_anuncios(categoria=cat) + pantalla_listar(anuncios, f"Categoria '{cat}'") + + +def pantalla_buscar(): + cabecera("Buscar anuncios") + termino = leer_linea("Buscar (titulo/contenido)") + if termino is None: + return + if not termino: + print(" Busqueda vacia, mostrando todos.") + pausa() + pantalla_listar() + return + anuncios = models.listar_anuncios(busqueda=termino) + pantalla_listar(anuncios, f"Resultados para '{termino}'") + + +def pantalla_nuevo(): + cabecera("Nuevo anuncio") + titulo = leer_linea("Titulo") + if titulo is None: + return + if not titulo: + print(" El titulo es obligatorio. Cancelado.") + pausa() + return + contenido = leer_linea("Contenido") + if contenido is None: + return + if not contenido: + print(" El contenido es obligatorio. Cancelado.") + pausa() + return + categoria = leer_linea("Categoria", "general") + if categoria is None: + return + contacto = leer_linea("Contacto", "madrid@craigslist.local") + if contacto is None: + return + aid = models.crear_anuncio(titulo, contenido, categoria, contacto) + print(f"\n Anuncio creado con ID {aid}.") + pausa() + + +def pantalla_editar(): + cabecera("Modificar anuncio") + sid = leer_linea("ID del anuncio a modificar") + if sid is None: + return + if not sid.isdigit(): + print(" ID no valido.") + pausa() + return + a = models.obtener_anuncio(int(sid)) + if not a: + print(" Anuncio no encontrado.") + pausa() + return + print() + print(formatear_anuncio(a)) + print() + print(" Deja en blanco para mantener el valor actual.") + print() + titulo = leer_linea("Nuevo titulo", a['titulo']) + if titulo is None: + return + contenido = leer_linea("Nuevo contenido", a['contenido']) + if contenido is None: + return + categoria = leer_linea("Nueva categoria", a['categoria']) + if categoria is None: + return + contacto = leer_linea("Nuevo contacto", a['contacto']) + if contacto is None: + return + + campos = {} + if titulo != a['titulo']: + campos['titulo'] = titulo + if contenido != a['contenido']: + campos['contenido'] = contenido + if categoria != a['categoria']: + campos['categoria'] = categoria + if contacto != a['contacto']: + campos['contacto'] = contacto + + if not campos: + print(" No se realizaron cambios.") + pausa() + return + + if models.actualizar_anuncio(int(sid), **campos): + print(" Anuncio actualizado correctamente.") + else: + print(" No se pudo actualizar.") + pausa() + + +def pantalla_eliminar(): + cabecera("Eliminar anuncio") + sid = leer_linea("ID del anuncio a eliminar") + if sid is None: + return + if not sid.isdigit(): + print(" ID no valido.") + pausa() + return + a = models.obtener_anuncio(int(sid)) + if not a: + print(" Anuncio no encontrado.") + pausa() + return + print() + print(formatear_anuncio(a)) + print() + try: + conf = input(" Confirmar eliminacion [s/N]: ").strip().lower() + except (EOFError, KeyboardInterrupt): + conf = 'n' + if conf == 's': + if models.borrar_anuncio(int(sid)): + print(" Anuncio eliminado.") + else: + print(" No se pudo eliminar.") + else: + print(" Cancelado.") + pausa() + + +def pantalla_ver(): + cabecera("Ver anuncio") + sid = leer_linea("ID del anuncio") + if sid is None: + return + if not sid.isdigit(): + print(" ID no valido.") + pausa() + return + a = models.obtener_anuncio(int(sid)) + if not a: + print(" Anuncio no encontrado.") + pausa() + return + print() + print(formatear_anuncio(a)) + pausa() + + +def main(): + models.init_db() + while True: + op = menu_principal() + if op == 'l': + pantalla_listar() + elif op == 'c': + pantalla_filtrar_categoria() + elif op == 'b': + pantalla_buscar() + elif op == 'n': + pantalla_nuevo() + elif op == 'm': + pantalla_editar() + elif op == 'e': + pantalla_eliminar() + elif op == 'v': + pantalla_ver() + elif op == 's': + cabecera("Hasta luego!") + sys.exit(0) + else: + print(" Opcion no valida.") + pausa() + + +if __name__ == '__main__': + main() diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..b7e5bf0 --- /dev/null +++ b/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +# Arrancar servidor SSH +/usr/sbin/sshd + +# Arrancar aplicacion web Flask +exec python3 /app/app.py diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..adaf9a5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,422 @@ + + + + + {% if modo == 'ciudad' %}madcraig — {{ city }}{% else %}madcraig{% endif %} + + + + + + + +
+

{% if modo == 'ciudad' %}madcraig // {{ city }}{% else %}madcraig{% endif %}

+ + {% if modo == 'inicio' %} +

Selecciona una ciudad para ver los anuncios.

+
+ {% for c in ciudades %} + + {{ c|replace('-', ' ') }} + /c/{{ c }} + + {% endfor %} +
+ {% else %} +

Buscar anuncios

+
+
+ + +
+
+ + +
+
+ + {% if categoria_actual or busqueda_actual %} + Limpiar + {% endif %} +
+
+ +
+

Anuncios

+ {% for a in anuncios %} +
+
+ + {{ a.categoria }} + {{ a.fecha }} · {{ a.contacto }} + + +
+ {{ a.titulo }} +

{{ a.contenido }}

+
+ {% else %} +

No hay anuncios todavia.

+ {% endfor %} + {% endif %} + +
+ +
+ + +