From 52977daf721367089fb8ff01692c99bc4a5dadb8 Mon Sep 17 00:00:00 2001 From: Juan Marquez Date: Mon, 4 May 2026 19:15:00 +0200 Subject: [PATCH] frist commit --- Dockerfile | 38 +++ __pycache__/api.cpython-314.pyc | Bin 0 -> 5075 bytes __pycache__/app.cpython-314.pyc | Bin 0 -> 3742 bytes __pycache__/models.cpython-314.pyc | Bin 0 -> 16947 bytes __pycache__/seed.cpython-314.pyc | Bin 0 -> 1692 bytes __pycache__/ssh_display.cpython-314.pyc | Bin 0 -> 11671 bytes api.py | 66 ++++ app.py | 62 ++++ data/anuncios.db | Bin 0 -> 36864 bytes data/anuncios.db.old | Bin 0 -> 20480 bytes data/anuncios.json.backup | 34 ++ docker-compose.yml | 8 + models.py | 371 +++++++++++++++++++++ requirements.txt | 1 + seed.py | 33 ++ ssh_display.py | 281 ++++++++++++++++ start.sh | 8 + templates/index.html | 422 ++++++++++++++++++++++++ 18 files changed, 1324 insertions(+) create mode 100644 Dockerfile create mode 100644 __pycache__/api.cpython-314.pyc create mode 100644 __pycache__/app.cpython-314.pyc create mode 100644 __pycache__/models.cpython-314.pyc create mode 100644 __pycache__/seed.cpython-314.pyc create mode 100644 __pycache__/ssh_display.cpython-314.pyc create mode 100644 api.py create mode 100644 app.py create mode 100644 data/anuncios.db create mode 100644 data/anuncios.db.old create mode 100644 data/anuncios.json.backup create mode 100644 docker-compose.yml create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 seed.py create mode 100644 ssh_display.py create mode 100644 start.sh create mode 100644 templates/index.html 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 0000000000000000000000000000000000000000..fb983a93b5b708a96754ddece31b421347851ffa GIT binary patch literal 5075 zcmcgwU2GHC6`mQ7|1utr?Z74_2Fxa##+x5Q9TG^g3!(XO*ltK?gOhFp(Zn8%i{tU$ zap)59;64>psenXFUaI;ryhVMYQl;)wMf)_)APt&DT2)=C;w==Zt@_k+?|5v7#YqKi zuaxUMbM86!&OPV*&bjIJ^DPv#Km6&R+0!uk3x2Sht2%gDped?EU8Y2OhDy*yI>8i~ zgrn#~Z0AEQLaC8x-W&S8h8S9D!+%?Y-#`mg8`-2=4f8D^|^qPLA2tcr`C zCOR#z=x_}>-X=OO(YI?IzCov@iB5~?-=!lo=x|MS0=smA4LZIiI-ys-r?o+c7nx2f zdf+)elc>Y=p3AMt1(eMfO*fK1Ta#57{76;u*(*0@O@~y-uCp=dqqNzwhH}dVBwxvX zzV3}l`L%p1tEi@9dU775?p3*XRY|L<>S2=~>}2e)mm9>%OEuYC)jhQrFqm^TK3QWC z!eX#Gch3Dyx$W9f`rB1u2G$ExM zaM}orYk~1c^QD<*!aH|v-o9xFM=Qe7vZE~OLXRN~RD=OtIBp21G~v|NL@Du%KX7O1 z_L9LLuJDI9u5L0q-)rzED*OqZj~o1`#*c0lOH2XAqD;G;J5|iNM4D^`IM3`A}OsrzYm)Q&$q(v zOhH*G%6YlwO3RZB_{UO(k2Br28!=G3Bj_Eh=*AAb4(mkV3HC~+)WC?%UNHJg(m&$^1);#y8YcvWCtK%Jb=rWK26-5HEFDQ9}HKuQ&r^|nBx;p&1?j6c|=*sWiwI{2wqX2-vxM^4B*yT zRIbT+oVit1%E;?}n+{S)1|EodESmQK!7t;Iz+~Y=RuQ0uLLuDY6R~K5$0{|Fh$hV?NJ;-?R%DiH#mrm{nY9g5fJeqI z32l_8EtJ>p1Nf#;NBQN)1m$%p+!5+h(L<2xokyLp2nk98O*8w=Am6$RnsXyL5K ze!DAiMwAFaR3rufH_;%*`9_@U}a= z@u}gCZM$PQXuVqJpw@O$7ls}=G+|WpjN;&&)(%f=tqGl*c^uccS&f~wV$v~pasKrI zIre`E$QV^%L~nqaI!I9k$HIxza4OcV;3FXIh1NbymDRS;i7DzMs4%_Mk#D9ATy>Hr zGNPj)WLOeV`&8daDo)41<~f?BYJm#u2UT0$u2Xc??Lx&?moTph(w1tF60$!$t3<9! zK=smUK~W09(u&iC`v zPus)4a^ITz*~|+kFQA&7);!ZBt|Qy-Ncm93J+yhPEI#f9z<8LhqBt-V`APg+Oxx<{gZD6MjmdJSP}v_iGML(lSZURsrxm&u)Sd8J@F zk+N2l$#s(4N=SJ@*pmby!W(=@5ocSKh-VD$vuio|Lo^K{{Audnq1$oLH2oFTU!nTH zr1(F3Lw8SZ^lV;#;vFivc3c$W`Dyqk;ak_rljY%3Sf_hmFi!gLO9#dD?cmW)pgKkK zzrS^HYZ}iPd%>`D7|;1&2ak6Abu&}tQ+P_>3&uzPfW~wBcgS$(NQ0rpt@C)+8NAp9 rGV3gjm%BhWS+9j2tF8y5okR9|t7W;ofGzdEU>Ix4z~AtQoXfufjaQZ1 literal 0 HcmV?d00001 diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08a7597e04626c59d81115e13e0bf035579ad009 GIT binary patch literal 3742 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 WU4t{Mx?6oe&Hw!o;Ln)uC*?mB6Oriv literal 0 HcmV?d00001 diff --git a/__pycache__/ssh_display.cpython-314.pyc b/__pycache__/ssh_display.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c494ad0463aa1c7382acb9eb8305dddf71ab2c3 GIT binary patch literal 11671 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 lCCM(xPc%c;-%7T8F-pL#XI`', 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 0000000000000000000000000000000000000000..cede40085c83dae2ec26c01780b6988ae055d9db GIT binary patch literal 36864 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 g=118|jb;E2Ijh&O8CrF+M4_q_jn`u{(s~AV z))q=nu2l8ZU(nyck7$1Zx1MszwYRE2pl@xjV*^poA#cUX%XsF^yw7lWqw&%AH4$;> zsIbQ(C>x&{rfKW~7>1F>`ySrOW#L0Q`2}Cindc8ZW{u*T^BlgL&i!gE{kF7|dy5Zr zkpKyh011!)36KB@kN^pgz^5zFOlR^d1@k8%+x$YbFJ5=Jf6AEbOJ68`uiEe`O%L|! zFT7VUeH#wykaSra4S*NkQFVP0CfASFLVmSi4$Wy;pGCZ@LNN`Uz7cxbWiI@*^?mfi zR3^W%VV*>+6>xnXU>*5d^YcEJ{`Ft2$t=yv^O^_O`mRaVDAy#^dM_Vz1{6ZpIzLF7_qW%9J6YBYh_z`_mIj%KfwaX8IOy`My(_R1xf}&@**6DqK9QR`0*Uj4Ot{~QS)#h;+>UbzTL@u zYFtUrthH3yF1y3e&8*=Ko&1LL64O)A!qI-fBNng4#})GkCwYErYmC{#T@m0qLZ*00 z9Onj!J2AL?+&n455AmX_r>xh)tTxd;F7S&3E^#9SgKD%KwQxPVGv|6||}T_ivPBtQZrKmsH{0wh2JBtQZr zKmvD0;DKos%;fMyW)bsB+gGOjul?5k+rF}|?o6VT2nmn?36KB@kN^pg011!)36KB@ z+*yGKSllOvFsz)lVor}bq_S52{^&%(qV?DupAyIz 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 %} + +
+ +
+ + +