Search code examples
htmlflaskjinja2flask-wtforms

Rendering in nested Jinja2 statement in html file


The origin of my issue is a validator function for a form which is defined as:

def AirlineICAORequired():
    """
    Validates whether string is a valid ICAO code
    """
    message = '''Must be an airline ICAO code. Please refer to: <a id="err-icao" href="{{ url_for('api.airlines') }}" class="link-danger">List of Airlines</a> for a list of airlines with ICAO codes listed.'''

    def _airlineicao(form, field):
        airline = get_airline(field.data)
        
        if not airline:
            raise ValidationError(message)
    return _airlineicao

Notice that I am rendering an html a tag to which I'm trying to render the url for the 'api.airlines' route. I have the following issue to consider:

  1. I cannot use app_context either from from flask import current_app nor from the actual app instance defined as app from app.py for the following two reasons:

    a) with current_app.app_context():... will still raise the RuntimeError which states that I am working outside of the application context

    b) If I tried to import from app import app in the file in which the function is located (forms.py) then I will run into a circular import error as other files such as routes.py (which has a Blueprint and is imported in app.py) import from this file.

In the html file, I load the error message as:

{% for error in initializer_form.initializer.errors %}
<li class="list-group-item list-group-item-danger">
 {{ error|safe }}
</li>
{% endfor %}

unfortunately this results in the following html lines:

<li class="list-group-item list-group-item-danger">
    Must be an airline ICAO code. Please refer to: <a id="err-icao" href="{{ url_for('api.airlines') }}" class="link-danger">List of Airlines</a> for a list of airlines with ICAO codes listed.
</li>

For context, initializer_form is the form in which that same validator is set to validate.

Also here's a picture of what it looks like in the website with the url to redirect to if "List of Airlines" is clicked is shown in the bottom: Image of failed validation for airline ICAO

Other Useful Information:

Project File Structure With Relevant Files Shown:

routelookup/
├─ Config/
├─ data/
├─ static/
├─ templates/
├─ api.py
├─ app.py
├─ form.py
├─ routes.py

Imports (for all files imports are at the top of the file):

app.py:

# imports
from flask import Flask
from routes import routes
from api import api
from routeparser import reset_contents, fr_api_logout
from Config import config
import atexit

form.py:

# imports
from flask_wtf import FlaskForm
from routeparser import get_airline
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import InputRequired, Email, Length, ValidationError

routes.py:

# Imports
from flask import Blueprint, render_template, redirect, url_for
from pathlib import Path
from forms import AirlineInitForm

What I Want

If I want to get the correct url into the href and not as shown above thru the image and 2nd html code block, how could I approach it considering the issue that I have and if I want to do it by using url_for or something close to that without hard coding it in?

Edit 1

Date: 10/2/23

I made a new file called form_validators which is partially shown in full below:

# imports
from flask import url_for
from app import app
from routeparser import get_airline
from wtforms.validators import ValidationError

# Relavant validator to the issue
def AirlineICAORequired():
    """
    Validates whether string is a valid ICAO code
    """
    with app.app_context():
        message = f'''Must be an airline ICAO code. Please refer to: <a id="err-icao" href="{url_for('api.airlines') }" class="link-danger">List of Airlines</a> for a list of airlines with ICAO codes listed.'''

    def _airlineicao(form, field):
        airline = get_airline(field.data)
        
        if not airline:
            raise ValidationError(message)
    return _airlineicao

all validators were moved to this file with the relevant imports.

Only import added to forms.py:

from form_validators import AtLength, AirlineICAORequired

Solution

  • So I resolved my issue. I would like to thank @PGHE for helping me as much as they could which inspired me to delve deeper into my own issue. I have made the following changes, some which can be seen on "Edit 1" in the body of the issue. To summarize that, I did create a new file called: "forms_validators.py" where I will add all custom validators into and import them respectively into "forms.py". As for the validator in question that I needed to "fix" I made the following changes as shown in the code block below:

    def AirlineICAORequired(msg = None):
        """
        Validates whether string is a valid ICAO code
        """
        # added method to add custom messages
        if isinstance(msg, str):
            message = msg
        else:
            message = "Must be an airline ICAO code. Please double check to make sure it's correct."
    
        def _airlineicao(form, field):
            airline = get_airline(field.data)
            
            if not airline:
                raise ValidationError(message)
        return _airlineicao
    

    This is important as will be seen later in the answer. This just adds support to add messages to replace the default message shown if the validator invalidates the form data.

    As per form.py, I removed the import: AirlineICAORequired from form_validators.py and within the form, I added a method to add validators after the input field been added into the form with validators set:

    # forms.py, lines 20-26 within class AirlineInitForm
    def add_validator_to(self, attr, *args):
        validator_attr = getattr(self, attr)
        
        if hasattr(validator_attr, "validators"):
            validator_attr.validators = validator_attr.validators + list(args)
        else:
            raise AttributeError("Check again if you inputed the correct attribute name.")
    

    as for in the routes page, I determined that I will be able to use url_for without any issues of application context and what not. Thus:

    @routes.route('/', methods=['GET', 'POST'], subdomain='application')
    def main():
        form = AirlineInitForm()
    
        form.add_validator_to(
            "initializer", 
            AirlineICAORequired(f'''
            Must be an airline ICAO code. Please refer to: <a id="err-icao" href="{url_for("api.airlines")}" class="link-danger">List of Airlines</a> for a list of airlines with ICAO codes listed.
            ''')
        )
        
        if form.validate_on_submit():
            icao = form.initializer.data
    
            return redirect(url_for('api.generate', icao=icao), code=307)
    
        return render_template('main.html', initializer_form=form)
    

    before checking for form validity (if form.validate_on_submit()) I added the validator (AirlineICAORequired) I needed to add to the initializer field thru the aftermentioned method added to the form class, using url_for to get the url for the airlines api route as I wanted to, adding it in an f string which is the error message sent if the validation fails.