Question 3 == The Formstack student application form to collect applicants' information listed below 1. Personal Details - a. First Name - b. Last Name - c. Email - d. Gender - e. Where are you from? - f. How did you hear about Moringa School? - g. Current Employment Status - h. Class Start Date 2. Professional Background - a. What is your highest level of education completed? - b. What is the name of the institution you attended - c. Please select your area of study from the following - d. How would you describe your professional background? - e. Please select your industry from the following 3. Next of Kin Details - a. Name - b. Phone Number - c. Email - d. Data Processing Consent[checkbox] - A. Write a query that submits the form with an applicant's bio-data via API - B. Provide a sample JavaScript snippet to validate the start date (ensuring it’s at least two weeks from the current date). ## Solution - [How to submit an applicant's bio-data via API](#How-to-submit-an-applicants-bio-data-via-API) Referrence material: https://developers.formstack.com/docs/getting-started - First let's update our code structure, since I'm using a single repository, this is the approach I choose to organise my files ``` client handlers.js index.js index.html config __init__.py models courses.py forms.py routes __init__.py courses.py forms.py main.py .env .gitignore requirements.txt ``` update the .env file ``` FORM_API_URL=YOUR_FORM_API_URL FORM_CLIENT_ID=YOUR_FORM_CLIENT_ID FORM_CLIENT_SECRET=YOUR_FORM_CLIENT_SECRET REDIERCT_URL=REDIERCT_URL ``` To follow along git clone this repository: https://github.com/x0can/LMS-API Navigate to formstack admin page and obtain the above environment variables Next `pip3 install -r requirements.txt` `python3 main.py` On your browser send this request to obtain redirect_url for generating `auth token` ``` GET http://127.0.0.1:5000/api/authorize ``` ### How to submit an applicant's bio-data via API Requirements and Assumptions: 1. Need to be an Admin to create an application on FormStack and obtain configurations 2. Follow through the process below on how to get Admin API environment variable `FORM_API_URL, FORM_CLIENT_ID, FORM_CLIENT_SECRET, REDIRECT_URL, ` Formstack Aouth2: https://developers.formstack.com/reference/oauth2-authorize-get - First we need to obtain an OAuth2 token for us to proceed. Reference material: https://developers.formstack.com/reference/oauth2-token-post - Before genearating the token, let's create a `callback` url that will work as our `redirect_url`. It will handle the flow where you first get the authorization code and then exchange it for the OAuth2 token using the `get_oauth2_token` method below, returning all the relevant token data in the response. Steps 1. Generate the authorization URL and prompt the user to visit it 2. The user is redirected 3. Extract `code` from the URL and store it 4. Use the `code` to retrieve the OAuth2 token github: https://github.com/x0can/LMS-API/blob/main/models/forms.py ``` GET /api/v2/oauth2/authorize" POST /api/v2/oauth2/token ``` models.forms.py ```python=- import requests class FormProcess: def __init__(self, api_url, client_id, client_secret, redirect_uri, code=None,access_token=None): self.api_url = api_url self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri self.code = code self.access_token=access_token self.headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json" } def handle_redirect_callback_code(self, code): self.code = code return code def authorize_aouth2(self): endpoint = f"{self.api_url}/api/v2/oauth2/authorize" try: # Construct the authorization URL auth_url = f"{endpoint}?client_id={self.client_id}&redirect_uri={self.redirect_uri}&response_type=code" return auth_url except requests.exceptions.RequestException as e: raise Exception(f"Error generating OAuth2 token {str(e)}") def get_oauth2_token(self): """ Generates an OAuth2 token using the Formstack API. """ if not self.code: return "Authorization code is missing. Please authorize first." # Define endpoint and payload endpoint = f"{self.api_url}/oauth2/token" payload = { "grant_type": "authorization_code", "client_id": self.client_id, "client_secret": self.client_secret, "redirect_uri": self.redirect_uri, "code": self.code } try: # Send POST request to get the token response = requests.post(endpoint, data=payload) # Raise an error if the request failed response.raise_for_status() # Return the access token if successful token_data = response.json() access_token = token_data.get('access_token') self.access_token = access_token return self.access_token except requests.exceptions.RequestException as e: raise Exception(f"Error generating OAuth2 token {str(e)}") def handle_token(self, access_token): self.access_token = access_token return "Authorization Successfull" ``` routes.forms.py ```python= from flask import Blueprint, request, jsonify, session from models.forms import FormProcess from config import Config form_routes = Blueprint('form_routes', __name__) form_handler = FormProcess( Config.FORM_API_URL, Config.FORM_CLIENT_ID, Config.FORM_CLIENT_SECRET, Config.REDIRECT_URL # Always set this as '/api/callback' ) @form_routes.route('/api/authorize') def authorize(): """ Auto redirects to the authorization URL to start the OAuth2 flow. """ auth_url = form_handler.authorize_aouth2() if auth_url: return redirect(auth_url) # Redirect the user to the authorization URL return "Failed to generate authorization URL." #Always set this as 'redirect_url' @form_routes.route('/api/callback', methods=['GET', 'POST']) def callback(): """ Handles the OAuth2 redirect callback and processes the authorization code. """ # Check if the request is a GET or POST request if request.method == 'GET': # Handle query parameters (like 'code') auth_code = request.args.get('code') if auth_code: form_handler.handle_redirect_callback_code(auth_code) token_data = form_handler.get_oauth2_token() if token_data: # Return the full token data return f"Authorization successful. You can close this window.\n {jsonify(token_data)}" else: return "Failed to retrieve the access token.", 500 else: return "Authorization failed." elif request.method == 'POST': # Handle JSON payload data = request.get_json() access_token = data.get('access_token') if data else None if access_token: status = form_handler.handle_token(access_token) return jsonify(status), 200 else: return "Authorization failed.", 500 ``` - Next we create the `POST` request to submit applicants data Referrence material: https://developers.formstack.com/reference/submissions ``` POST api/v2/form/{id}/submission.json ``` Assumptions: Data validation is handled on the client, you can skip to check [here](#How-to-Validate-the-start-date) how the start date is validated to make sure it's at least two weeks from the current date models.forms.py ```python= class FormProcess: ... def submit_formstack_application(self, form_id, applicant_data): """ Submits an application to the Formstack API using OAuth2 tokens. """ # Endpoint for form submissions endpoint = f"{self.api_url}/form/{form_id}/submission.json" try: #get access_token access_token = self.get_oauth2_token() # Define headers for authentication headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } # Send POST request to Formstack response = requests.post(endpoint, json=applicant_data, headers=headers) # Raise an error if the request failed response.raise_for_status() # Return the JSON response if successful return response.json() except requests.exceptions.RequestException as e: print(f"Error submitting the form: {e}") return None ``` When testing you might incur the following error,`Authorization code is missing. Please authorize first.` If you do, send a request to this endpoint ``` GET Http://Your-local-host/api/authorize ``` Then resume the process create the '/submit-form' endpoint routes.forms.py ```python= ... @form_routes.route('/api/submit_form', methods=['POST']) def submit_form(): data = request.json # Validate the required fields in the form data required_fields = [ "form_id" "name", "type", "first_name", "last_name", "email", "gender", "from_location", "source", "employment_status", "start_date", "education_level", "institution", "area_of_study", "professional_background", "industry", "kin_name", "kin_phone", "kin_email", "consent" ] missing_fields = [field for field in required_fields if field not in data] if missing_fields: return jsonify({"error": f"Missing required fields. {missing_fields}"}), 400 try: # Call the submit_form_data method to send the data response_data, status_code = form_handler.submit_formstack_application( data['form_id'], data) return jsonify(response_data), status_code except Exception as e: return jsonify({"error": str(e)}), 500 ``` ### How to Validate the start date Validate data before sending the `POST` request to the API endpoint `/api/submit-form` above using JavaScript process - Create an input validation function to check if the date is two weeks from current date - Check if applicant's start date is okay and - Send a `POST` request to our backend Test by starting a server on the following path `/clients/index.html` Or simply open the html file on a browser. client.handlers.js ```javascript= /** * Validates the start date to ensure it’s at least two weeks from the current date. * @param {string} startDate - The date to be validated in 'YYYY-MM-DD' format. * @returns {boolean} - Returns true if the start date is valid, false otherwise. */ // validator const validateStartDate = (startDate) => { const today = new Date(); const startDateObj = new Date(startDate); const twoWeeksFromToday = new Date(today); twoWeeksFromToday.setDate(today.getDate() + 14); // Set date to 14 days from today // Early return if the start date is invalid return ( startDateObj >= twoWeeksFromToday || { error: "Start date must be at least 14 days from today.", } ); }; /** * Submits the form to the following endpoint {url}/submit_form. * @param {object} formData - The form data to send over to the API. * @param {string} url - The API endpoint url, must match the server's endpoint. * @returns {object} - Returns object if successful, error otherwise. */ // handlers const submitForm = async (formData, url) => { // Validate form data validDate = validateStartDate(formData["start_date"]); // If validation fails (validDate is an object with an error), return the error if (validDate.error) { return validDate; } try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || "Failed to submit form"); } return result; } catch (error) { console.error("Error submitting form:", error.message); throw error; } }; function handle_form() { const form = document.getElementById("submitForm"); const url = "http://127.0.0.1:5000/api/submit_form"; const formStatus = document.getElementById("formStatus"); form.addEventListener("submit", async (e) => { e.preventDefault(); const formData = { form_id: 'F1O1', name: form.name.value, type: form.type.value, first_name: form.first_name.value, last_name: form.last_name.value, email: form.email.value, gender: form.gender.value, from_location: form.from_location.value, source: form.source.value, employment_status: form.employment_status.value, start_date: form.start_date.value, education_level: form.education_level.value, institution: form.institution.value, area_of_study: form.area_of_study.value, professional_background: form.professional_background.value, industry: form.industry.value, kin_name: form.kin_name.value, kin_phone: form.kin_phone.value, kin_email: form.kin_email.value, consent: form.consent.checked, }; try { const result = await submitForm(url, formData); const { error } = result; if (error) { formStatus.textContent = error; formStatus.style.color = "#F44336"; } else { formStatus.textContent = "Form submitted successfully!"; formStatus.style.color = "green"; } } catch (error) { console.log(error.message); formStatus.textContent = error.message; formStatus.style.color = "#F44336"; } }); } handle_form(); ``` To test the above Run a live server inside the `/clients/index.html` file then submit the form there.