Diferencia entre revisiones de «Usuario:Lmorillas/desarrollo web servidor/flask/ejemplo completo/con citas»
De WikiEducator
(Página creada con '{{MiTitulo| Aplicación con citas}} == Modelo == <source lang="python"> class Cita(Base): """Una cita en el calendario.""" __tablename__ = 'cita' id = Column(Integ…') |
(→Eliminar cita) |
||
(8 revisiones intermedias por el mismo usuario no mostrado) | |||
Línea 27: | Línea 27: | ||
fecha=self.inicio) | fecha=self.inicio) | ||
</source> | </source> | ||
+ | |||
+ | == Listado de citas == | ||
+ | === Controlador === | ||
+ | <source lang="python"> | ||
+ | @app.route('/citas/') | ||
+ | @login_required | ||
+ | def lista_citas(): | ||
+ | """Lista de todas las citas en la base de datos.""" | ||
+ | # Query: Recupera las citas del usuario, ordenadas por fecha. | ||
+ | citas = (db.session.query(Cita) | ||
+ | .filter_by(user_id=session['uid']) | ||
+ | .order_by(Cita.inicio.asc()).all()) | ||
+ | return render_template('cita/index.html', citas=citas) | ||
+ | </source> | ||
+ | === Tempate === | ||
+ | '''Macro auxiliar''' | ||
+ | <source lang="html4strict"> | ||
+ | {% macro detalle(cita) %} | ||
+ | <div class="detalle-cita"> | ||
+ | <h3>{{ cita.evento or '(sin titulo)' }}</h3> | ||
+ | |||
+ | {% if cita.lugar %}<p><i class="icon-home"></i> {{ cita.lugar }}</p>{% endif %} | ||
+ | {% if cita.todoeldia %} | ||
+ | <p><i class="icon-calendar"></i> {{ cita.inicio }}</p> | ||
+ | {% else %} | ||
+ | <p><i class="icon-calendar"></i> {{ cita.inicio }}. Duración: {{ cita.duracion() }}</p> | ||
+ | {% endif %} | ||
+ | </div> | ||
+ | {% endmacro %} | ||
+ | </source> | ||
+ | |||
+ | '''cita/index.html' | ||
+ | <source lang="html4strict"> | ||
+ | {% extends 'layout.html' %} | ||
+ | |||
+ | {% from 'cita/comun.html' import detalle %} | ||
+ | |||
+ | {% block content %} | ||
+ | <div class="row"> | ||
+ | {% for cita in citas %} | ||
+ | <div class="cita"> | ||
+ | {{ detalle(cita) }} | ||
+ | </div> | ||
+ | {% else %} | ||
+ | <h3 class="span12">No hay citas.</h3> | ||
+ | {% endfor %} | ||
+ | </div> | ||
+ | {% endblock content %} | ||
+ | </source> | ||
+ | |||
== Creación de nuevas citas == | == Creación de nuevas citas == | ||
=== Controlador === | === Controlador === | ||
<source lang="python"> | <source lang="python"> | ||
− | @app.route('/ | + | @app.route('/cita/crear/', methods=['GET', 'POST']) |
@login_required | @login_required | ||
− | def | + | def crear_cita(): |
− | """ | + | """Muestra el formulario para crear una cita""" |
− | form = | + | form = FormCita(request.form) |
+ | |||
if request.method == 'POST' and form.validate(): | if request.method == 'POST' and form.validate(): | ||
− | + | cita = Cita(user_id=session['uid']) | |
− | form.populate_obj( | + | form.populate_obj(cita) |
− | db.session.add( | + | db.session.add(cita) |
db.session.commit() | db.session.commit() | ||
− | # | + | # Exito: devuelve al usuario a la lista de citas |
− | return redirect(url_for(' | + | return redirect(url_for('lista_citas')) |
− | # | + | # Error o GET. |
− | return render_template(' | + | return render_template('cita/editar.html', form=form) |
+ | </source> | ||
+ | |||
=== Formulario === | === Formulario === | ||
<source lang="python"> | <source lang="python"> | ||
Línea 57: | Línea 110: | ||
descripcion = TextAreaField('Descripción') | descripcion = TextAreaField('Descripción') | ||
</source> | </source> | ||
+ | |||
=== Template === | === Template === | ||
+ | <source lang="html4strict"> | ||
+ | {% extends "layout.html" %} | ||
+ | |||
+ | {% block content %} | ||
+ | <h2>Crear Cita</h2> | ||
+ | |||
+ | {% for message in form.titulo.errors %} | ||
+ | <div class="flash">{{message }}</div> | ||
+ | {% endfor %} | ||
+ | |||
+ | {% for message in form.inicio.errors %} | ||
+ | <div class="flash">{{ message }}</div> | ||
+ | {% endfor %} | ||
+ | |||
+ | {% for message in form.fin.errors %} | ||
+ | <div class="flash">{{ message }}</div> | ||
+ | {% endfor %} | ||
+ | |||
+ | |||
+ | <form action="{{ url_for('crear_cita') }}" method=post> | ||
+ | {{ form.hidden_tag() }} | ||
+ | |||
+ | {{ form.titulo.label }} | ||
+ | {{ form.titulo }} | ||
+ | |||
+ | {{ form.inicio.label }} | ||
+ | {{ form.inicio }} | ||
+ | |||
+ | {{ form.fin.label }} | ||
+ | {{ form.fin }} | ||
+ | |||
+ | {{ form.todoeldia.label }} | ||
+ | {{ form.todoeldia}} | ||
+ | |||
+ | {{ form.lugar.label }} | ||
+ | {{ form.lugar}} | ||
+ | |||
+ | {{ form.descripcion.label }} | ||
+ | {{ form.descripcion}} | ||
+ | |||
+ | |||
+ | {{ form.crear }} | ||
+ | </form> | ||
+ | |||
+ | {% endblock %} | ||
+ | </source> | ||
+ | |||
+ | == Mostrar el detalle de una cita == | ||
+ | <source lang="python"> | ||
+ | from flask import abort | ||
+ | ... | ||
+ | @app.route('/cita/<int:cita_id>/') | ||
+ | @login_required | ||
+ | def detalle_cita(cita_id): | ||
+ | """Detalle de una cita dada.""" | ||
+ | # Query: obtiene el objeto por ID | ||
+ | cita = db.session.query(Cita).get(cita_id) | ||
+ | if cita is None or cita.user_id != session.uid: | ||
+ | # Abortar con Not Found. | ||
+ | abort(404) | ||
+ | return render_template('cita/detalle.html', cita=cita) | ||
+ | |||
+ | </source> | ||
+ | |||
+ | == Editar cita existente == | ||
+ | <source lang="python"> | ||
+ | @app.route('/citas/<int:cita_id>/editar/', methods=['GET', 'POST']) | ||
+ | @login_required | ||
+ | def editar_cita(cita_id): | ||
+ | """Prepara el formulario HTML para editar una cita.""" | ||
+ | cita = db.session.query(Cita).get(cita_id) | ||
+ | if cita is None: | ||
+ | abort(404) | ||
+ | if cita.user_id != session[uid]: | ||
+ | abort(403) | ||
+ | form = FormCita(request.form, cita) | ||
+ | if request.method == 'POST' and form.validate(): | ||
+ | form.populate_obj(cita) | ||
+ | db.session.commit() | ||
+ | # Si hay éxito, vuelve a la vista de detalle de la cita | ||
+ | return redirect(url_for('detalle_cita', cita_id=cita.id)) | ||
+ | return render_template('cita/edit.html', form=form) | ||
+ | </source> | ||
+ | |||
+ | == Eliminar cita == | ||
+ | * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html | ||
+ | * http://www.w3.org/TR/html401/interact/forms.html#h-17.13.1 | ||
+ | |||
+ | === Usando HTTP DELETE === | ||
+ | '''javascript en plantilla''' | ||
+ | <source lang="javascript" | ||
+ | <script> | ||
+ | $(function() { | ||
+ | $(".cita-delete-link").on("click", function() { | ||
+ | var delete_url = $(this).attr('data-delete-url'); | ||
+ | $.ajax({ | ||
+ | url: delete_url, | ||
+ | type: 'DELETE', | ||
+ | success: function(response) { | ||
+ | if (response.status == 'OK') { | ||
+ | window.location = {{ url_for('lista_citas') }}; | ||
+ | } else { | ||
+ | alert('Error al borrar.') | ||
+ | } | ||
+ | } | ||
+ | }); | ||
+ | return false; | ||
+ | }); | ||
+ | }); | ||
+ | </script> | ||
+ | </source> | ||
+ | |||
+ | <source lang="python"> | ||
+ | from flask import jsonify | ||
+ | ... | ||
+ | @app.route('/citas/<int:cita_id>/eliminar/', methods=['DELETE']) | ||
+ | @login_required | ||
+ | def eliminar_cita(appointment_id): | ||
+ | """Elimina un recurso usando HTTP DELETE, responde con JSON por JavaScript.""" | ||
+ | cita = db.session.query(cita).get(cita_id) | ||
+ | if cita is None: | ||
+ | # Abortar: no encontrado | ||
+ | response = jsonify({'status': 'Not Found'}) | ||
+ | response.status_code = 404 | ||
+ | return response | ||
+ | if cita.user_id != session.uid: | ||
+ | # Abortar con respuesta simple: prohibido. | ||
+ | response = jsonify({'status': 'Forbidden'}) | ||
+ | response.status_code = 403 | ||
+ | return response | ||
+ | db.session.delete(cita) | ||
+ | db.session.commit() | ||
+ | return jsonify({'status': 'OK'}) | ||
+ | </source> |
Última revisión de 19:50 15 nov 2013
Contenido
Modelo
class Cita(Base): """Una cita en el calendario.""" __tablename__ = 'cita' id = Column(Integer, primary_key=True) creada = Column(DateTime, default=datetime.now) modificada = Column(DateTime, default=datetime.now, onupdate=datetime.now) evento = Column(String(255)) inicio = Column(DateTime, nullable=False) fin = Column(DateTime, nullable=False) todoeldia = Column(Boolean, default=False) lugar = Column(String(255)) descripcion = Column(Text) user_id = Column(Integer, ForeignKey('usuarios.id'), nullable=False) usuario = relationship(User, lazy='joined', join_depth=1, viewonly=True) def duracion(self): tiempo = self.fin - self.inicio return tiempo.days * 24 * 60 * 60 + tiempo.seconds def __str__(self): return "{evento} [{fecha}}".format(evento=self.evento, fecha=self.inicio)
Listado de citas
Controlador
@app.route('/citas/') @login_required def lista_citas(): """Lista de todas las citas en la base de datos.""" # Query: Recupera las citas del usuario, ordenadas por fecha. citas = (db.session.query(Cita) .filter_by(user_id=session['uid']) .order_by(Cita.inicio.asc()).all()) return render_template('cita/index.html', citas=citas)
Tempate
Macro auxiliar
{% macro detalle(cita) %} <div class="detalle-cita"> <h3>{{ cita.evento or '(sin titulo)' }}</h3> {% if cita.lugar %}<p><i class="icon-home"></i> {{ cita.lugar }}</p>{% endif %} {% if cita.todoeldia %} <p><i class="icon-calendar"></i> {{ cita.inicio }}</p> {% else %} <p><i class="icon-calendar"></i> {{ cita.inicio }}. Duración: {{ cita.duracion() }}</p> {% endif %} </div> {% endmacro %}
cita/index.html'
{% extends 'layout.html' %} {% from 'cita/comun.html' import detalle %} {% block content %} <div class="row"> {% for cita in citas %} <div class="cita"> {{ detalle(cita) }} </div> {% else %} <h3 class="span12">No hay citas.</h3> {% endfor %} </div> {% endblock content %}
Creación de nuevas citas
Controlador
@app.route('/cita/crear/', methods=['GET', 'POST']) @login_required def crear_cita(): """Muestra el formulario para crear una cita""" form = FormCita(request.form) if request.method == 'POST' and form.validate(): cita = Cita(user_id=session['uid']) form.populate_obj(cita) db.session.add(cita) db.session.commit() # Exito: devuelve al usuario a la lista de citas return redirect(url_for('lista_citas')) # Error o GET. return render_template('cita/editar.html', form=form)
Formulario
class FormCita(Form): """Formulario para el modelo Citas. Genera HTML y valida entradas """ titulo = TextField('Cita', [validators.Length(max=255)]) inicio = DateTimeField('Inicio', [validators.Required()]) fin = DateTimeField('Fin') todoeldia = BooleanField('Todo el día') lugar = TextField('Lugar', [validators.Length(max=255)]) descripcion = TextAreaField('Descripción')
Template
{% extends "layout.html" %} {% block content %} <h2>Crear Cita</h2> {% for message in form.titulo.errors %} <div class="flash">{{message }}</div> {% endfor %} {% for message in form.inicio.errors %} <div class="flash">{{ message }}</div> {% endfor %} {% for message in form.fin.errors %} <div class="flash">{{ message }}</div> {% endfor %} <form action="{{ url_for('crear_cita') }}" method=post> {{ form.hidden_tag() }} {{ form.titulo.label }} {{ form.titulo }} {{ form.inicio.label }} {{ form.inicio }} {{ form.fin.label }} {{ form.fin }} {{ form.todoeldia.label }} {{ form.todoeldia}} {{ form.lugar.label }} {{ form.lugar}} {{ form.descripcion.label }} {{ form.descripcion}} {{ form.crear }} </form> {% endblock %}
Mostrar el detalle de una cita
from flask import abort ... @app.route('/cita/<int:cita_id>/') @login_required def detalle_cita(cita_id): """Detalle de una cita dada.""" # Query: obtiene el objeto por ID cita = db.session.query(Cita).get(cita_id) if cita is None or cita.user_id != session.uid: # Abortar con Not Found. abort(404) return render_template('cita/detalle.html', cita=cita)
Editar cita existente
@app.route('/citas/<int:cita_id>/editar/', methods=['GET', 'POST']) @login_required def editar_cita(cita_id): """Prepara el formulario HTML para editar una cita.""" cita = db.session.query(Cita).get(cita_id) if cita is None: abort(404) if cita.user_id != session[uid]: abort(403) form = FormCita(request.form, cita) if request.method == 'POST' and form.validate(): form.populate_obj(cita) db.session.commit() # Si hay éxito, vuelve a la vista de detalle de la cita return redirect(url_for('detalle_cita', cita_id=cita.id)) return render_template('cita/edit.html', form=form)
Eliminar cita
- http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
- http://www.w3.org/TR/html401/interact/forms.html#h-17.13.1
Usando HTTP DELETE
javascript en plantilla
$(function() { $(".cita-delete-link").on("click", function() { var delete_url = $(this).attr('data-delete-url'); $.ajax({ url: delete_url, type: 'DELETE', success: function(response) { if (response.status == 'OK') { window.location = {{ url_for('lista_citas') }}; } else { alert('Error al borrar.') } } }); return false; }); }); </script>
from flask import jsonify ... @app.route('/citas/<int:cita_id>/eliminar/', methods=['DELETE']) @login_required def eliminar_cita(appointment_id): """Elimina un recurso usando HTTP DELETE, responde con JSON por JavaScript.""" cita = db.session.query(cita).get(cita_id) if cita is None: # Abortar: no encontrado response = jsonify({'status': 'Not Found'}) response.status_code = 404 return response if cita.user_id != session.uid: # Abortar con respuesta simple: prohibido. response = jsonify({'status': 'Forbidden'}) response.status_code = 403 return response db.session.delete(cita) db.session.commit() return jsonify({'status': 'OK'})