Adición de un formulario de contacto a una página multilingüe con contenido de una base de datos
Cuando el contenido de la página proviene de una base de datos, querrá añadir un formulario de contacto utilizando una etiqueta.
Actualización 11 de octubre de 2019: Cambié la etiqueta addon de '{% addon: ... %}' a '[[ addon: ... ]]". La razón es que quería poder renderizar el texto de la página procedente de la base de datos, usando render_template_string, y '{% ... %}' conflictos con las Jinja2 etiquetas. Y sí, no quiero implementar una etiqueta Jinja2 personalizada.
¿Qué tiene de difícil implementar una página de contacto con un formulario de contacto con Flask y WTForms? Puede encontrar soluciones sobre cómo implementar una página de contacto en Flask pero cada vez que la página es una página en un solo idioma y utiliza un archivo de Jinja2 plantilla. Entonces, ¿por qué escribir un post sobre esto?
La razón es que esto no es trivial cuando el contenido de la página puede ser multilingüe y proviene de una base de datos. Tengo el contenido de la página que puedo editar usando el administrador, y quiero que el formulario de contacto se coloque en algún lugar de la página usando una etiqueta. ¿Por qué una etiqueta? Porque debemos ser capaces de poner el formulario de contacto en cualquier lugar del contenido. Una vez que la etiqueta es reemplazada por el formulario de contacto, también debe ser procesada cuando se envía. ¿Fácil? Quizá para ti, pero no para mí.
Presentación de los complementos
En cuanto a otras soluciones, pensé que sería útil implementar el formulario de contacto como un complemento. Por qué? Porque un complemento es algo que deberías poder añadir de una manera muy sencilla a tu contenido. También debería ser posible añadir el formulario de contacto a varias páginas. Hay más que un complemento, por ejemplo, el complemento de formulario de contacto también añade una función de formulario de contacto al administrador, donde podemos ver los formularios de contacto que se han enviado.
Implementación del add-on
Lo primero que hice fue definir una etiqueta que identificara el complemento del formulario de contacto:
{% addon:contact_form,id=87 %}
Esta es la etiqueta que podemos añadir al contenido de nuestra página multilingüe que proviene de la base de datos. Otros componentes del complemento del formulario de contacto son:
- ContactForm, el modelo (tabla)
- Parte administrativa, donde podemos ver los formularios enviados
Y luego necesitamos un mecanismo general que procese el complemento cuando mostramos una página. Como recordará de un post anterior, sólo hay una función que genera una página. Como el contenido no cambia, se almacena en caché:
@pages_blueprint.route('/<slug>', methods=['GET', 'POST'])
def page_view(slug):
...
# get content_item
...
# render content_item of get from cache
hit, rendered_content_item = current_app.app_cache.load(cache_item_id)
if not hit:
rendered_content_item = render_template(
...
content_item=content_item,
content_item_translation=content_item_translation,
)
# cache it
current_app.app_cache.dump(cache_item_id, rendered_content_item)
...
return render_template(
...
rendered_content_item=rendered_content_item,
)
Esta función debe modificarse y ampliarse para que pueda manejar complementos.
Convertir MVC en una clase
En el Flask uso WTForms del formulario de contacto, la implementación es muy sencilla, por ejemplo:
@pages_blueprint.route('/contact-form', methods=['GET', 'POST'])
def contact_form():
form = ContactFormForm()
if form.validate_on_submit():
contact_form = ContactForm()
form.populate_obj(contact_form)
db.add(contact_form)
db.commit()
flash( _('Contact form submitted.'), 'info')
return redirect(url_for('pages.thank_you'))
return render_template(
'pages/contact_form.html',
form=form)
Y el formulario de contacto es:
class ContactFormForm(FlaskForm):
name = StringField(_l('Name'), validators=[
Length(min=2, max=60),
InputRequired()])
email = StringField(_l('Your email'), validators=[
InputRequired(),
Email()])
message = TextAreaField(_l('Your message'), validators=[
Length(min=6, max=500),
InputRequired()])
submit = SubmitField(_l('Send'))
No podemos usar esto aquí, así que lo reescribimos como una clase. Decidí devolver el éxito o error de los métodos GET y POST y tener un método separado para obtener el formulario de contacto renderizado.
class AddonContactForm:
def __init(self)__:
...
self.errors = False
self.rendered_contact_form = ''
def get_contact_form(self):
self.errors = False
form = ContactFormForm()
self.rendered_contact_form = render_template(
'addons/contact_form.html',
form=form)
return self.errors
def get_rendered_contact_form(self):
return self.rendered_contact_form
def post_contact_form(self):
self.errors = False
form = ContactFormForm()
if form.validate_on_submit():
contact_form = ContactForm()
form.populate_obj(contact_form)
db.add(contact_form)
db.commit()
flash( _('Contact form submitted.'), 'info')
return redirect(url_for('pages.thank_you'))
self.errors = True
self.rendered_contact_form = render_template(
'addons/contact_form.html',
form=form)
return self.errors
El ContactFormFormForm se amplía con un parámetro oculto que identifica al complemento:
addon_id = HiddenField('Addon id')
Usando esto ahora podemos cambiar la función page_view:
@pages_blueprint.route('/<slug>', methods=['GET', 'POST'])
def page_view(slug):
...
if request.method == 'POST':
addon_id = None
if 'addon_id' in request.form:
addon_id = request.form['addon_id']
if addon_id is not None:
if addon_id == 'contact_form':
addon_contact_form = AddonContactForm()
if addon_contact_form.process_contact_form():
addon_redirect_url = addon_contact_form.get_redirect_url()
return redirect(addon_redirect_url)
# error(s) found during processing
rendered_contact_form = addon_contact_form.get_rendered_contact_form()
addon_error = True
addon_error_message = addon_contact_form.get_error_message()
....
# addon: processing if '{% addon' found
if '{% addon:' in rendered_content_item:
m = re.findall('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_content_item)
addon_name = None
if m:
addon_name = m[0][1]
if addon_name == 'contact_form':
if request.method == 'GET':
addon_contact_form = AddonContactForm()
if addon_contact_form.get_contact_form():
rendered_contact_form = addon_contact_form.get_rendered_contact_form()
else:
rendered_contact_form = ''
error = True
error_message = addon_contact_form.get_error_message()
rendered_content_item = re.sub('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_contact_form, rendered_content_item)
elif request.method == 'POST':
# here we just paste the result from the addon
# typically we only come here when an error was detected in the form
rendered_content_item = re.sub('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_contact_form, rendered_content_item)
return render_template(
...
)
Resumen
Lo anterior es meramente un resumen, hay más, pero sólo quería darles lo básico. También implementé un complemento de preguntas frecuentes, donde sólo tenemos que tratar con un GET. Puede consultar las páginas de Contacto y Preguntas Frecuentes en este sitio web. Esto fue sólo un primer intento de implementar complementos, y no, no es definitivo. Ahora debería definir una interfaz clara de todos los métodos y atributos que un add-on puede o debe utilizar. En otro momento...
Enlaces / créditos
Exact difference between add-ons, plugins and extensions
https://stackoverflow.com/questions/33462500/exact-difference-between-add-ons-plugins-and-extensions
Intro to Flask: Adding a Contact Page
https://code.tutsplus.com/tutorials/intro-to-flask-adding-a-contact-page--net-28982
Leer más
Flask Jinja2 Multilanguage WTForms
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow