From 60dd3fb038463459ef7f4c5a39d8c03303ebf8c2 Mon Sep 17 00:00:00 2001
From: Brandon Rodriguez <brodriguez8774@gmail.com>
Date: Mon, 1 Apr 2024 02:01:25 -0400
Subject: [PATCH] Import various changes from django_v4 app into django_v2 and
 django_v3, for consistency

---
 django_v2/Pipfile                             |   3 +-
 django_v2/settings/settings.py                |   4 +-
 django_v2/settings/urls.py                    |  20 +-
 django_v2/test_app/forms.py                   |  35 ++
 django_v2/test_app/models.py                  |  19 +-
 .../test_app/templates/test_app/api_send.html | 346 ++++++++++++
 .../test_app/templates/test_app/base.html     |   2 +-
 .../test_app/templates/test_app/index.html    |  48 +-
 .../test_app/root_project_home_page.html      |  13 +-
 django_v2/test_app/urls.py                    |   7 +
 django_v2/test_app/views.py                   | 510 ++++++++++++++++++
 django_v3/Pipfile                             |   3 +-
 django_v3/settings/urls.py                    |  16 +-
 django_v3/test_app/forms.py                   |  35 ++
 django_v3/test_app/models.py                  |  19 +-
 .../test_app/templates/test_app/api_send.html | 346 ++++++++++++
 .../test_app/templates/test_app/base.html     |   2 +-
 .../test_app/templates/test_app/index.html    |  47 +-
 .../test_app/root_project_home_page.html      |   2 +-
 django_v3/test_app/urls.py                    |   7 +
 django_v3/test_app/views.py                   | 510 ++++++++++++++++++
 21 files changed, 1924 insertions(+), 70 deletions(-)
 create mode 100644 django_v2/test_app/forms.py
 create mode 100644 django_v2/test_app/templates/test_app/api_send.html
 create mode 100644 django_v3/test_app/forms.py
 create mode 100644 django_v3/test_app/templates/test_app/api_send.html

diff --git a/django_v2/Pipfile b/django_v2/Pipfile
index 484820d..257d6db 100644
--- a/django_v2/Pipfile
+++ b/django_v2/Pipfile
@@ -21,14 +21,15 @@ python_version = "3.11"
 [packages]
 # General Django dependencies.
 django = "< 2.3.0"              # Core Django package, locked to latest 2.2 LTS.
+# django-adminlte2-pdq = "*"      # Adds framework for easily styling site like adminlte2.
 django-localflavor = "*"        # Easy implementation of localization info, such as addresses.
+requests = "*"                  # Simple HTTP library. Useful for things like initiating API requests.
 
 ###
  # Development and testing packages, installed via `pipenv sync --dev`.
  ##
 [dev-packages]
 # General dev dependencies.
-# django-adminlte2-pdq = "*"      # Adds framework for easily styling site like adminlte2.
 django-debug-toolbar = "*"      # Displays helpful debug-toolbar on side of browser window.
 # django-dump-die = "*"           # Dump-and-die debugging tool.
 
diff --git a/django_v2/settings/settings.py b/django_v2/settings/settings.py
index 0215ddb..feb18dd 100644
--- a/django_v2/settings/settings.py
+++ b/django_v2/settings/settings.py
@@ -35,7 +35,7 @@ INSTALLED_APPS = [
     'test_app.apps.TestAppConfig',
 
     # DjangoDD Package.
-    'django_dump_die',
+    # 'django_dump_die',
 
     # DjangoETC Package.
     'django_expanded_test_cases',
@@ -51,7 +51,7 @@ INSTALLED_APPS = [
 
 MIDDLEWARE = [
     # Package middleware.
-    'django_dump_die.middleware.DumpAndDieMiddleware',
+    # 'django_dump_die.middleware.DumpAndDieMiddleware',
 
     # Built-in Django middleware.
     'django.middleware.security.SecurityMiddleware',
diff --git a/django_v2/settings/urls.py b/django_v2/settings/urls.py
index 6f242cc..d64696f 100644
--- a/django_v2/settings/urls.py
+++ b/django_v2/settings/urls.py
@@ -1,17 +1,5 @@
-"""Django v2.2 test project URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
-    https://docs.djangoproject.com/en/2.2/topics/http/urls/
-Examples:
-Function views
-    1. Add an import:  from my_app import views
-    2. Add a URL to urlpatterns:  path('', views.home, name='home')
-Class-based views
-    1. Add an import:  from other_app.views import Home
-    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
-Including another URLconf
-    1. Import the include() function: from django.urls import include, path
-    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+Django v2.2 test project URL Configuration
 """
 from django.contrib import admin
 from django.urls import include, path
@@ -29,8 +17,8 @@ urlpatterns = [
     # Package testing views.
 
     # DjangoDD routes for demo purposes.
-    path('dd/', include('django_dump_die.urls')),
-    path('dd_tests/', include('django_dump_die.test_urls')),
+    # path('dd/', include('django_dump_die.urls')),
+    # path('dd_tests/', include('django_dump_die.test_urls')),
 
     # DjangoETC routes for demo purposes.
     path('django_etc/', include('django_expanded_test_cases.test_urls')),
diff --git a/django_v2/test_app/forms.py b/django_v2/test_app/forms.py
new file mode 100644
index 0000000..5af5038
--- /dev/null
+++ b/django_v2/test_app/forms.py
@@ -0,0 +1,35 @@
+"""
+Forms for Django v2.2 test project app.
+"""
+
+# System Imports.
+
+# Third-Party Imports.
+from django import forms
+
+# Internal Imports.
+
+
+class ApiSendForm(forms.Form):
+    """A single line item for sending in an API call."""
+
+    url = forms.URLField()
+    get_params = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '6'}),
+        help_text='Optional URL get param to append to URL.',
+    )
+    header_token = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '6'}),
+        help_text='Optional token to put into request header, such as required for API authentication.'
+    )
+    payload = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '20'}),
+        help_text=(
+            'Should be in proper JSON dictionary format or else will error. <br><br>'
+            'Ex: Use double quotes instead of single, booleans should be lower case, etc. <br><br>'
+            'If left empty, will send <br>{"success": true}.'
+        )
+    )
diff --git a/django_v2/test_app/models.py b/django_v2/test_app/models.py
index e120977..cb9bd41 100644
--- a/django_v2/test_app/models.py
+++ b/django_v2/test_app/models.py
@@ -11,6 +11,14 @@ from localflavor.us.models import USStateField, USZipCodeField
 MAX_LENGTH = 255
 
 
+class BaseAbstractModel(models.Model):
+    """Expanded version of the default Django model."""
+
+    # Self-setting/Non-user-editable fields.
+    date_created = models.DateTimeField(auto_now_add=True)
+    date_modified = models.DateTimeField(auto_now=True)
+
+
 class User(AbstractUser):
     """Custom user model definition.
     Defined as per the Django docs. Not yet directly used.
@@ -38,7 +46,7 @@ class User(AbstractUser):
             UserProfile.objects.create(user=self)
 
 
-class UserProfile(models.Model):
+class UserProfile(BaseAbstractModel):
     """Basic model to act as a test fk to user model."""
 
     # Relationship Keys.
@@ -52,7 +60,7 @@ class UserProfile(models.Model):
     zipcode = USZipCodeField()
 
 
-class FavoriteFood(models.Model):
+class FavoriteFood(BaseAbstractModel):
     """Basic model to act as a test m2m relation to user model."""
 
     # Relationship Keys.
@@ -60,3 +68,10 @@ class FavoriteFood(models.Model):
 
     # Model fields.
     name = models.CharField(max_length=MAX_LENGTH)
+
+
+class ApiRequestJson(BaseAbstractModel):
+    """Used to retain data for API testing views."""
+
+    # Model fields.
+    json_value = models.CharField(max_length=MAX_LENGTH)
diff --git a/django_v2/test_app/templates/test_app/api_send.html b/django_v2/test_app/templates/test_app/api_send.html
new file mode 100644
index 0000000..25b1811
--- /dev/null
+++ b/django_v2/test_app/templates/test_app/api_send.html
@@ -0,0 +1,346 @@
+{% extends "test_app/base.html" %}
+
+
+{% block page_header %}
+  Django LTS v2.2 - API Send
+{% endblock page_header %}
+
+
+{% block page_subheader %}
+  API Send Page
+{% endblock page_subheader %}
+
+
+{% block stylesheets %}
+<style>
+  form h2, form h3, form h4, form h5, form h6,
+  .result-box h2, .result-box h3, .result-box h4, .result-box h5, .result-box h6,
+  .example h2, .example h3, .example h4, .example h5, .example h6 {
+    margin-top: 6px;
+    margin-bottom: 6px;
+  }
+
+  form {
+    margin: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  form div {
+    padding-top: 5px;
+    padding-bottom: 5px;
+  }
+
+  input {
+    width: 100%;
+  }
+
+  textarea {
+    width: 100%;
+  }
+
+  input {
+    margin-top: 5px;
+  }
+
+  pre {
+    width: 100%;
+    margin: 5px;
+    padding: 5px;
+    background-color: LightSteelBlue;
+    border: 1px solid grey;
+  }
+
+  pre.allow-break {
+    white-space: pre-wrap;
+  }
+
+  .error {
+    color: red;
+  }
+
+  .field-group {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+  }
+
+  .label p {
+    margin-top: 8px;
+    margin-bottom: 8px;
+  }
+
+  .label {
+    width: 10%;
+    padding: 0 10px 0 10px;
+    text-align: right;
+  }
+
+  .help-text {
+    font-size: 70%;
+    font-style: italic;
+    text-align: center;
+  }
+
+  .field {
+    width: 80%;
+    padding: 0 10px 0 10px;
+  }
+
+  .submit-buttons {
+    display: flex;
+    flex-direction: row;
+  }
+
+  .submit-buttons input {
+    margin: 5px;
+    padding: 5px;
+  }
+
+  .example {
+    margin-top: 25px;
+    margin-right: 10px;
+    margin-bottom: 25px;
+    margin-left: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  .result-box {
+    margin-top: 25px;
+    margin-right: 10px;
+    margin-bottom: 25px;
+    margin-left: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  .result-box.sent-data pre {
+    background-color: LightBlue;
+  }
+
+  .italics {
+    margin-top: 5px;
+    margin-bottom: 8px;
+
+    font-size: 90%;
+    font-style: italic;
+    color: #575757;
+  }
+
+  h3.success-return {
+    color: DarkGreen;
+  }
+  div.success-return pre {
+    background-color: #cde4e4;
+  }
+
+  h3.error-return {
+    color: DarkRed;
+  }
+  div.error-return pre {
+    background-color: #d9cde4;
+  }
+</style>
+{% endblock stylesheets %}
+
+
+{% block content %}
+  <p>Use this to generate and send test API requests to other projects.</p>
+
+  <form method="POST">
+    <h2>API Send Form</h2>
+    <p class="italics">Use the below form to send a JSON API ping to the desired url.</p>
+
+    {% csrf_token %}
+
+    {% if form.non_field_errors %}
+      <div class="error">
+        <p>Non Field Errors:</p>
+        {{ form.non_field_errors }}
+      </div>
+      <hr>
+    {% endif %}
+
+    {% for field in form %}
+
+      <div>
+        {% if field.errors %}
+          <p class="error"> Field Error:
+          {% for error in field.errors %}
+            {{ error }}
+          {% endfor %}
+          </p>
+        {% endif %}
+
+        <div class="field-group">
+
+          <div class="label">
+            <p>{{ field.label }}:</p>
+            {% if field.help_text %}
+              <p class="help-text">{{ field.help_text|safe }}</p>
+            {% endif %}
+          </div>
+          <div class="field">
+            {{ field }}
+          </div>
+        </div>
+
+      </div>
+
+    {% endfor %}
+
+    <div class="submit-buttons">
+      <input
+        type="submit"
+        name="submit_get"
+        value="Submit as GET"
+        title="Generally used to retrieve data from the server."
+      >
+      <input
+        type="submit"
+        name="submit_post"
+        value="Submit as POST"
+        title="Generally used to send data to the server, and create a new resource."
+      >
+      <input
+        type="submit"
+        name="submit_put"
+        value="Submit as PUT"
+        title="Generally used to send data to the server, and update an existing resource by full replacement."
+      >
+      <input
+        type="submit"
+        name="submit_patch"
+        value="Submit as PATCH"
+        title="Generally used to send data to the server, and update an existing resource by partial replacement."
+      >
+      <input
+        type="submit"
+        name="submit_delete"
+        value="Submit as DELETE"
+        title="Generally used to send data to the server, and delete an existing resource."
+      >
+    </div>
+  </form>
+
+  <div class="result-box">
+    <h2>Parsed Return-Response</h2>
+
+    {% if response_error or response_success %}
+      <p class="italics">This is the data that was returned after the previous API send.</p>
+    {% endif %}
+
+    {% if response_success %}
+      <h3 class="success-return">Success Sending API Ping</h3>
+      {% for key, value in response_success.items %}
+        <div class="field-group success-return">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    {% endif %}
+
+    {% if response_error %}
+      <h3 class="error-return">Error Sending API Ping</h3>
+      {% for key, value in response_error.items %}
+        <div class="field-group error-return">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    {% endif %}
+
+    {% if not response_error and not response_success %}
+      <p class="italics">No return value yet. Submit the API form and the resulting return response will display here.</p>
+    {% endif %}
+  </div>
+
+  {% if sent_data %}
+    <div class="result-box sent-data">
+      <h2>Sent Data</h2>
+      <p class="italics">This is what was sent out from this form, on the previous API call.</p>
+      {% for key, value in sent_data.items %}
+        <div class="field-group">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    </div>
+  {% endif %}
+
+  <div class="example">
+    <h2>Example Send Values:</h2>
+
+    <p>Below are some example form values to get started.</p>
+
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Url:
+        </p>
+      </div>
+      <pre>http://127.0.0.1:8000/test_app/api/parse/</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Get Params:
+        </p>
+      </div>
+      <pre>test-param-1=Abc&test-param-2=123</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Header Token:
+        </p>
+      </div>
+      <pre>MyExampleHeaderAuthToken-112233445566778899ABABCDCD</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Payload:
+        </p>
+      </div>
+      <pre>{
+  "test": true,
+  "Aaa": "Bbb",
+  "MyNumber": 5
+}</pre>
+    </div>
+
+  <hr>
+
+  <p>Above values will send:</p>
+  <div class="field-group">
+    <pre>url: http://127.0.0.1:8000/test_app/api/parse/?test-param-1=Abc&test-param-2=123
+
+header: {
+  "Accept": "application/json",
+  "token": "MyExampleHeaderAuthToken-112233445566778899ABABCDCD"
+}
+
+data: {
+  "test": true,
+  "Aaa": "Bbb",
+  "MyNumber": 5
+}</pre>
+  </div>
+
+  </div>
+
+{% endblock content %}
diff --git a/django_v2/test_app/templates/test_app/base.html b/django_v2/test_app/templates/test_app/base.html
index b823203..f8bedfc 100644
--- a/django_v2/test_app/templates/test_app/base.html
+++ b/django_v2/test_app/templates/test_app/base.html
@@ -4,7 +4,7 @@
 <head>
   <meta charset="utf-8"/>
   <title>
-    Django v2.2 -
+    Django LTS v2.2 -
     {% block title %}
       Test App Title
     {% endblock title %}
diff --git a/django_v2/test_app/templates/test_app/index.html b/django_v2/test_app/templates/test_app/index.html
index 5bbf45a..cd6d998 100644
--- a/django_v2/test_app/templates/test_app/index.html
+++ b/django_v2/test_app/templates/test_app/index.html
@@ -1,6 +1,5 @@
 {% extends "test_app/base.html" %}
 
-
 {% block page_header %}
   Django LTS v2.2 - Test App Index
 {% endblock page_header %}
@@ -8,18 +7,49 @@
 
 {% block content %}
   <p>Test Page Content</p>
+
+  {% if is_class_view %}
+    <p><a href="{% url 'test_app:index' %}">Render page from Function view</a></p>
+  {% else %}
+    <p><a href="{% url 'test_app:index_as_class' %}">Render page from Class view</a></p>
+  {% endif %}
+
   <ul>
     <li>
-      <p><a href="{% url 'root_project_home_page:index' %}">Back to Django v2.2 Home</a></p>
-    </li>
-    <li>
-      <p><a href="{% url 'test_app:view_with_login_check' %}">Test App - Login Check Page</a></p>
-    </li>
-    <li>
-      <p><a href="{% url 'test_app:view_with_permission_check' %}">Test App - Permission Check Page</a></p>
+      <p>Standard Views:</p>
+      <ul>
+        <li>
+          <p><a href="{% url 'root_project_home_page:index' %}">Back to Django v2.2 Home</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_login_check' %}">Test App - Login Check Page</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_permission_check' %}">Test App - Permission Check Page</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_group_check' %}">Test App - Group Check Page</a></p>
+        </li>
+      </ul>
     </li>
     <li>
-      <p><a href="{% url 'test_app:view_with_group_check' %}">Test App - Group Check Page</a></p>
+      <p>API Views:</p>
+      <ul>
+        <li>
+          <p><a href="{% url 'test_app:api_parse' %}">API Parse - Receive API requests here to parse them.</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:api_display' %}">API Display - View parsed API requests here.</a></p>
+          <p>
+            Note: Only displays the first received API request since last access of api_display view.
+            <br>
+            All parsed API data is purged after page access.
+          </p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:api_send' %}">API Send - Generate and send API requests here.</a></p>
+        </li>
+      </ul>
     </li>
   </ul>
 {% endblock content %}
diff --git a/django_v2/test_app/templates/test_app/root_project_home_page.html b/django_v2/test_app/templates/test_app/root_project_home_page.html
index 90ec6be..d005a93 100644
--- a/django_v2/test_app/templates/test_app/root_project_home_page.html
+++ b/django_v2/test_app/templates/test_app/root_project_home_page.html
@@ -7,7 +7,7 @@
 
 
 {% block page_subheader %}
-  Official LTS Support ended on April 11, 2022
+  Official LTS Support ended in April 2022
 {% endblock page_subheader %}
 
 
@@ -28,21 +28,10 @@
 
   <h3>Package Debug Views</h3>
 
-  <h4>Django DD (DumpDie)</h4>
-  <ul>
-    <li>
-      <p><a href="{% url 'django_dump_die:index' %}">Django DD Debug Views</a></p>
-    </li>
-    <li>
-      <p><a href="{% url 'django_dump_die_tests:index' %}">Django DD Test Views</a></p>
-    </li>
-  </ul>
-
   <h4>Django ETC (ExpandedTestCases)</h4>
   <ul>
     <li>
       <p><a href="{% url 'django_expanded_test_cases:index' %}">Django ETC Test Views</a></p>
     </li>
   </ul>
-
 {% endblock content %}
diff --git a/django_v2/test_app/urls.py b/django_v2/test_app/urls.py
index 84b9479..cb4d62d 100644
--- a/django_v2/test_app/urls.py
+++ b/django_v2/test_app/urls.py
@@ -16,6 +16,13 @@ urlpatterns = [
     path('view_with_permission_check/', views.view_with_permission_check, name='view_with_permission_check'),
     path('view_with_group_check/', views.view_with_group_check, name='view_with_group_check'),
 
+    # Test API views.
+    path('api/parse/', views.api_parse, name='api_parse'),
+    path('api/display/', views.api_display, name='api_display'),
+    path('api/send/', views.api_send, name='api_send'),
+
+    # Test app root, but as a class.
+    path('as_class', views.ExampleClassView.as_view(), name='index_as_class'),
     # App root.
     path('', views.index, name='index')
 ]
diff --git a/django_v2/test_app/views.py b/django_v2/test_app/views.py
index 0e3d9e7..34432df 100644
--- a/django_v2/test_app/views.py
+++ b/django_v2/test_app/views.py
@@ -2,10 +2,24 @@
 Views for Django v2.2 test project app.
 """
 
+# System Imports.
+import json
+import html
+import re
+import requests
+
 # Third-Party Imports.
 from django.contrib.auth.decorators import login_required, permission_required
+from django.http import JsonResponse, QueryDict
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_http_methods
+from django.views.generic import TemplateView
 from django.shortcuts import redirect, render, reverse
 
+# Internal Imports.
+from test_app.forms import ApiSendForm
+from test_app.models import ApiRequestJson
+
 
 # region Index/Root Views
 
@@ -18,6 +32,122 @@ def index(request):
     """Test app index page."""
     return render(request, 'test_app/index.html')
 
+
+class ExampleClassView(TemplateView):
+    """A basic Class Django view,
+    with some of the more common built-in methods and documentation of what they do.
+
+    Note: Some of these methods won't do anything with TemplateView. For example,
+          the form valid/invalid methods require a class that will POST form data.
+          Such as CreateView or UpdateView.
+    """
+
+    # Magic DjangoView args. Often times, can just define these and skip most method calls.
+
+    # Template to render.
+    template_name = 'test_app/index.html'
+
+    # Url to use if redirecting.
+    # If args/kwargs are needed, then probably need to use get_redirect_url() instead.
+    url = None
+
+    # If using a ModelView (ListView, DetailView, etc), these define what model data to call with.
+    model = None
+    queryset = None     # Can call more complicated query logic in get_queryset().
+
+    # If using a ListView, this determines the number of results to display per page with pagination.
+    paginate_by = 25
+
+    # Params for views with form logic.
+    form_class = None       # Form class to use.
+    initial = {}            # Initial data to populate into form, if applicable.
+    success_url = None      # If args/kwargs are needed, then probably need to use get_success_url() instead.
+
+    def dispatch(self, request, *args, **kwargs):
+        """Determines initial logic to call on view access.
+        This is one of the first methods called by Django class views.
+        This determines which of [GET(), POST(), etc] base class handling methods are called.
+        If you need redirecting or other logic prior to calling these, do it here.
+
+        If not redirecting outside of this class, then should probably always finish
+        this function by returning a call to the original dispatch method.
+        """
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        """Pulls additional context data to be used in the template."""
+
+        # Get base context object.
+        context = super().get_context_data(**kwargs)
+
+        # Add new value to context.
+        context['is_class_view'] = True
+
+        # Return context.
+        return context
+
+    def get_queryset(self):
+        """If using a view that uses models (DetailView, ListView, etc), then this modifies the default queryset."""
+        queryset = super().get_queryset()
+
+        # Use additional model query logic here.
+
+        # Return our modified queryset.
+        return queryset
+
+    def get_ordering(self):
+        """Return the field or fields to use for ordering the queryset."""
+
+        # Replace this with a return to a single model field or list of model fields to order by.
+        return super().get_ordering()
+
+    def get(self, request, *args, **kwargs):
+        """Handling for GET response type."""
+
+        # Replace this with a response object.
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        """Handling for POST response type."""
+
+        # Replace this with either a response object, or a call to
+        # form_valid()/form_invalid() functions. Depending on what you need for class logic.
+        return super().post(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        """When processing a form, this is the logic to run on form validation success."""
+
+        # Call parent logic. Should always include this line, as default views sometimes do additional processing.
+        response = super().form_valid(form)
+
+        # Do some handling with response here.
+
+        # Return some response object for render.
+        return response
+
+    def form_invalid(self, form):
+        """When processing a form, this is the logic to run on form validation failure."""
+
+        # Call parent logic. Should always include this line, as default views sometimes do additional processing.
+        response = super().form_invalid(form)
+
+        # Do some handling with response here.
+
+        # Return some response object for render.
+        return response
+
+    def get_success_url(self):
+        """When processing a form, determines how to get the url for form success redirect."""
+
+        # Replace this with a `reverse()` call to generate the correct URL.
+        return super().get_success_url()
+
+    def get_redirect_url(self, *args, **kwargs):
+        """When handling a redirect view, this determines how to get the url."""
+
+        # Replace this with a `reverse()` call to generate the correct URL.
+        return super().get_redirect_url()
+
 # endregion Index/Root Views
 
 
@@ -47,3 +177,383 @@ def view_with_group_check(request):
     return render(request, 'test_app/group_check.html')
 
 # endregion Login/Permission Test Views
+
+
+# region API Views
+
+@csrf_exempt
+@require_http_methods(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
+def api_parse(request):
+    """Takes in JSON ping, and saves incoming value to web cookies.
+
+    Then if api_display view is called after, will display the saved cookie value to web page.
+
+    Allows quick debugging to make sure the expected, correct data is being sent.
+    """
+    print('')
+    print('api_parse():')
+
+    # Get data from response.
+    get_data = {}
+    post_data = {}
+    body_data = {}
+    header_data = {}
+    if request.headers:
+        print('Received HEADERS:')
+        header_data = dict(request.headers)
+        print(header_data)
+    if request.GET:
+        print('Received GET:')
+        get_data = _recurisive_json_parse(request.GET)
+        for key, value in get_data.items():
+            get_data[key] = value[0]
+        print(get_data)
+    if request.POST:
+        print('Received POST:')
+        post_data = _recurisive_json_parse(dict(request.POST))
+        print(post_data)
+    if request.body:
+        print('Received BODY:')
+        # Attempt to escape. Limited functionality so may not work.
+        # To be precise, functions well with a standard JSON response.
+        # But with any other response type that has a body, might break and be ugly.
+        body_data = _recurisive_json_parse(html.unescape(request.body.decode('UTF-8')))
+        print(body_data)
+    print('\n')
+
+    # Combine data.
+    data = {}
+    if header_data:
+        data['HEADERS'] = header_data
+    if get_data:
+        data['GET'] = get_data
+    if post_data:
+        data['POST'] = post_data
+    if body_data:
+        data['body'] = body_data
+    if not data:
+        data = {'data': 'No data found in request.'}
+
+    # Save api data to database.
+    model_instance = ApiRequestJson.objects.first()
+    if not model_instance:
+        model_instance = ApiRequestJson.objects.create()
+    model_instance.json_value = data
+    model_instance.save()
+
+    # Generate response.
+    return JsonResponse({'success': True})
+
+
+def _recurisive_json_parse(data_item):
+    """Helper function to ensure all sub-items of response are properly read-in."""
+
+    # Convert from potentially problematic types, for easier handling.
+    if isinstance(data_item, QueryDict):
+        data_item = dict(data_item)
+    if isinstance(data_item, tuple):
+        data_item = list(data_item)
+
+    # Process some known types.
+    if isinstance(data_item, dict):
+        # Is dictionary. Iterate over each (key, value) pair and attempt to convert.
+        for key, value in data_item.items():
+            data_item[key] = _recurisive_json_parse(value)
+
+    elif isinstance(data_item, list):
+        # Is iterable. Iterate over each item and attempt to convert.
+        for index in range(len(data_item)):
+            sub_item = data_item[index]
+            data_item[index] = _recurisive_json_parse(sub_item)
+
+    else:
+        # For all other types, just attempt naive conversion
+        try:
+            data_item = json.loads(data_item)
+        except:
+            # On any failure, just skip. Leave item as-is.
+            pass
+
+    # Return parsed data.
+    return data_item
+
+
+def api_display(request):
+    """After a JSON ping to api_parse view, this displays parsed value to web page.
+
+    Allows quick debugging to make sure the expected, correct data is being sent.
+    """
+
+    # Grab api data from session, if any.
+    model_instance = ApiRequestJson.objects.first()
+    if model_instance:
+        content = {
+            'payload_data': model_instance.json_value,
+            'payload_sent_at': model_instance.date_created,
+        }
+    else:
+        content = {
+            'payload_data': {},
+            'payload_sent_at': 'N/A',
+        }
+
+    # Attempt to output api data to browser.
+    response = JsonResponse(content, safe=False)
+
+    # Delete all existing instances of saved API data.
+    ApiRequestJson.objects.all().delete()
+
+    # Return data view to user.
+    return response
+
+
+def api_send(request):
+    """Test app index page."""
+    print('\n')
+    print('api_send():')
+
+    response_success = {}
+    response_error = {}
+    sent_data = {}
+
+    # Initialize formset.
+    form = ApiSendForm()
+
+    # Check if POST.
+    if request.POST:
+        # Is POST. Process data.
+        print('Is POST submission.')
+        has_error = False
+
+        post_data = request.POST
+        form = ApiSendForm(data=post_data)
+
+        if form.is_valid():
+            # Handle for form submission.
+            print('Submitted form data:')
+            print('{0}'.format(form.cleaned_data))
+
+            send_type = ''
+            if 'submit_get' in post_data:
+                send_type = 'GET'
+                # data.pop('submit_get')
+            if 'submit_post' in post_data:
+                send_type = 'POST'
+                # data.pop('submit_post')
+            if 'submit_put' in post_data:
+                send_type = 'PUT'
+                # data.pop('submit_put')
+            if 'submit_patch' in post_data:
+                send_type = 'PATCH'
+                # data.pop('submit_patch')
+            if 'submit_delete' in post_data:
+                send_type = 'DELETE'
+                # data.pop('submit_delete')
+
+            url = str(form.cleaned_data['url']).strip()
+            get_params = str(form.cleaned_data.get('get_params', '')).strip()
+            header_token = str(form.cleaned_data.get('header_token', '')).strip()
+            payload = str(form.cleaned_data.get('payload', '{}')).strip()
+            if len(payload) > 0:
+                try:
+                    payload = json.loads(payload)
+                except json.decoder.JSONDecodeError:
+                    has_error = True
+                    payload = {}
+                    form.add_error(
+                        'payload',
+                        'Unrecognized/invalid JSON syntax. Please double check syntax and try again.',
+                    )
+            else:
+                has_error = True
+                form.add_error(
+                    'payload',
+                    'Please provide JSON data to send. If API query is meant to be empty, use {}.',
+                )
+
+            # Determine url.
+            if get_params and len(get_params) > 0:
+                if url[-1] != '?' and get_params[0] != '?':
+                    url += '?'
+                url += get_params
+
+            # Determine header values.
+            headers = {'Accept': 'application/json'}
+            if header_token:
+                headers['token'] = header_token
+
+            # Determine data values.
+            if payload:
+                data = json.dumps(payload)
+            else:
+                data = json.dumps({'success': True})
+
+            # Generate API send object.
+            try:
+                # Generate based on clicked send button.
+                if not has_error and send_type == 'GET':
+                    response = requests.get(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'POST':
+                    response = requests.post(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'PUT':
+                    response = requests.put(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'PATCH':
+                    response = requests.patch(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'DELETE':
+                    response = requests.delete(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error:
+                    # Unknown send type. Somehow. Raise error.
+                    form.add_error(None, 'Invalid send_type. Was "{0}".'.format(send_type))
+            except Exception as err:
+                has_error = True
+                response_error['query_sent'] = False if not err.response else True
+                response_error['message'] = str(err.message) if hasattr(err, 'message') else str(err)
+                if 'Max retries exceeded with url' in response_error['message']:
+                    response_error['help_text'] = (
+                        'This error is often the result of a typo in the URL, or the desired endpoint being down. '
+                        'Are you sure you entered the destination URL correctly?'
+                    )
+
+            if not has_error:
+                # Handle for success state.
+
+                # Display sent input data to user.
+                # That way they can change the form for a subsequent request and still see what was sent last time.
+                sent_data['send_type'] = send_type
+                sent_data['url'] = url
+                sent_data['headers'] = headers
+                sent_data['content'] = data
+
+                # Parse returned response status code.
+                response_success['status'] = response.status_code
+                if response_success['status'] >= 400:
+                    # Define help_text key now to preserve location in display ordering.
+
+                    # Provide help text for some common error statuses.
+                    if response_success['status'] == 400:
+                        # 400: Bad Request
+                        response_success['help_text'] = (
+                            '400: Bad Request - This error is often the result of a bad or malformed request, such '
+                            'as incorrect or unexpected syntax. Double check that the sent request data is correct.'
+                        )
+                    elif response_success['status'] == 401:
+                        # 401: Unauthorized
+                        response_success['help_text'] = (
+                            '401: Unauthorized - This error is often the result of invalid or missing authentication '
+                            'credentials. Are you sure the authentication tokens are correctly provided?'
+                        )
+                    elif response_success['status'] == 403:
+                        # 403: Forbidden
+                        response_success['help_text'] = (
+                            '403: Forbidden - This error is often the result of invalid or missing authentication '
+                            'credentials. Are you sure the authentication tokens are correctly provided?'
+                        )
+                    elif response_success['status'] == 404:
+                        # 404: Not Found
+                        response_success['help_text'] = (
+                            '404: Not Found - This error is often the result of the requested url not existing on the '
+                            'server. Are you sure you entered the destination URL correctly?'
+                        )
+                    elif response_success['status'] == 405:
+                        # 405: Method Not Allowed
+                        response_success['help_text'] = (
+                            '405: Method Not Allowed - This error is often the result of the destination understanding '
+                            'the sent response type (GET/POST/PUT/PATCH/DELETE), but not supporting said type. '
+                            'If this is a server you have access to, then double check that the endpoint is configured '
+                            'correctly.'
+                        )
+                    elif response_success['status'] == 415:
+                        # 415: Unsupported Media Type
+                        response_success['help_text'] = (
+                            '415: Unsupported Media Type - This error is often the result of the destination '
+                            'being unable to parse the provided content. Are you sure the payload was entered '
+                            'correctly?'
+                        )
+                    elif response_success['status'] == 500:
+                        # 500: Server Error
+                        response_success['help_text'] = (
+                            '500: Server Error - This error is often the result of your request being received, but '
+                            'the server broke when trying to process the request. If this is a server you have '
+                            'access to, then double check the server logs for more details.'
+                        )
+
+                # Parse returned response header data.
+                if response.headers:
+                    response_success['headers'] = response.headers
+
+                # Parse returned response content.
+                if response.headers['content-Type'] and response.headers['Content-Type'] == 'application/json':
+                    response_success['content'] = response.json()
+                else:
+                    content = html.unescape(response.content.decode('UTF-8'))
+
+                    # NOTE: Below copied from Django ExpandedTestCase package.
+                    # Replace html linebreak with actual newline character.
+                    content = re.sub('<br>|</br>|<br/>|<br />', '\n', content)
+
+                    # Replace non-breaking space with actual space character.
+                    content = re.sub('(&nbsp;)+', ' ', content)
+
+                    # Replace any carriage return characters with newline character.
+                    content = re.sub(r'\r+', '\n', content)
+
+                    # Replace any whitespace trapped between newline characters.
+                    # This is empty/dead space, likely generated by how Django handles templating.
+                    content = re.sub(r'\n\s+\n', '\n', content)
+
+                    # Replace any repeating linebreaks.
+                    content = re.sub(r'\n\n+', '\n', content)
+
+                    # # Reduce any repeating whitespace instances.
+                    # content = re.sub(r' ( )+', ' ', content)
+
+                    # Strip final calculated string of extra outer whitespace.
+                    content = str(content).strip()
+                    response_success['content'] = content
+
+                # Handle if was response was received, but it gave error level status.
+                if response_success['status'] >= 400:
+                    response_error = response_success
+                    response_success = {}
+
+    print('Rendering response...')
+    print('')
+
+    return render(request, 'test_app/api_send.html', {
+        'form': form,
+        'sent_data': sent_data,
+        'response_success': response_success,
+        'response_error': response_error,
+    })
+
+# endregion API Views
diff --git a/django_v3/Pipfile b/django_v3/Pipfile
index f1acfa9..d5df847 100644
--- a/django_v3/Pipfile
+++ b/django_v3/Pipfile
@@ -21,14 +21,15 @@ python_version = "3.11"
 [packages]
 # General Django dependencies.
 django = "< 3.3.0"              # Core Django package, locked to latest 3.2 LTS.
+django-adminlte2-pdq = "*"      # Adds framework for easily styling site like adminlte2.
 django-localflavor = "*"        # Easy implementation of localization info, such as addresses.
+requests = "*"                  # Simple HTTP library. Useful for things like initiating API requests.
 
 ###
  # Development and testing packages, installed via `pipenv sync --dev`.
  ##
 [dev-packages]
 # General dev dependencies.
-django-adminlte2-pdq = "*"      # Adds framework for easily styling site like adminlte2.
 django-debug-toolbar = "*"      # Displays helpful debug-toolbar on side of browser window.
 django-dump-die = "*"           # Dump-and-die debugging tool.
 
diff --git a/django_v3/settings/urls.py b/django_v3/settings/urls.py
index f304417..d7f96b3 100644
--- a/django_v3/settings/urls.py
+++ b/django_v3/settings/urls.py
@@ -1,17 +1,5 @@
-"""Django v3.2 test project URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
-    https://docs.djangoproject.com/en/3.2/topics/http/urls/
-Examples:
-Function views
-    1. Add an import:  from my_app import views
-    2. Add a URL to urlpatterns:  path('', views.home, name='home')
-Class-based views
-    1. Add an import:  from other_app.views import Home
-    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
-Including another URLconf
-    1. Import the include() function: from django.urls import include, path
-    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+Django v3.2 test project URL Configuration
 """
 from django.contrib import admin
 from django.urls import include, path
diff --git a/django_v3/test_app/forms.py b/django_v3/test_app/forms.py
new file mode 100644
index 0000000..85e2b4e
--- /dev/null
+++ b/django_v3/test_app/forms.py
@@ -0,0 +1,35 @@
+"""
+Forms for Django v4.2 test project app.
+"""
+
+# System Imports.
+
+# Third-Party Imports.
+from django import forms
+
+# Internal Imports.
+
+
+class ApiSendForm(forms.Form):
+    """A single line item for sending in an API call."""
+
+    url = forms.URLField()
+    get_params = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '6'}),
+        help_text='Optional URL get param to append to URL.',
+    )
+    header_token = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '6'}),
+        help_text='Optional token to put into request header, such as required for API authentication.'
+    )
+    payload = forms.CharField(
+        required=False,
+        widget=forms.Textarea(attrs={'rows': '20'}),
+        help_text=(
+            'Should be in proper JSON dictionary format or else will error. <br><br>'
+            'Ex: Use double quotes instead of single, booleans should be lower case, etc. <br><br>'
+            'If left empty, will send <br>{"success": true}.'
+        )
+    )
diff --git a/django_v3/test_app/models.py b/django_v3/test_app/models.py
index 465d515..c0be7e0 100644
--- a/django_v3/test_app/models.py
+++ b/django_v3/test_app/models.py
@@ -11,6 +11,14 @@ from localflavor.us.models import USStateField, USZipCodeField
 MAX_LENGTH = 255
 
 
+class BaseAbstractModel(models.Model):
+    """Expanded version of the default Django model."""
+
+    # Self-setting/Non-user-editable fields.
+    date_created = models.DateTimeField(auto_now_add=True)
+    date_modified = models.DateTimeField(auto_now=True)
+
+
 class User(AbstractUser):
     """Custom user model definition.
     Defined as per the Django docs. Not yet directly used.
@@ -38,7 +46,7 @@ class User(AbstractUser):
             UserProfile.objects.create(user=self)
 
 
-class UserProfile(models.Model):
+class UserProfile(BaseAbstractModel):
     """Basic model to act as a test fk to user model."""
 
     # Relationship Keys.
@@ -52,7 +60,7 @@ class UserProfile(models.Model):
     zipcode = USZipCodeField()
 
 
-class FavoriteFood(models.Model):
+class FavoriteFood(BaseAbstractModel):
     """Basic model to act as a test m2m relation to user model."""
 
     # Relationship Keys.
@@ -61,3 +69,10 @@ class FavoriteFood(models.Model):
     # Model fields.
     name = models.CharField(max_length=MAX_LENGTH)
 
+
+class ApiRequestJson(BaseAbstractModel):
+    """Used to retain data for API testing views."""
+
+    # Model fields.
+    json_value = models.JSONField(default=dict)
+
diff --git a/django_v3/test_app/templates/test_app/api_send.html b/django_v3/test_app/templates/test_app/api_send.html
new file mode 100644
index 0000000..48d1b13
--- /dev/null
+++ b/django_v3/test_app/templates/test_app/api_send.html
@@ -0,0 +1,346 @@
+{% extends "test_app/base.html" %}
+
+
+{% block page_header %}
+  Django LTS v4.2 - API Send
+{% endblock page_header %}
+
+
+{% block page_subheader %}
+  API Send Page
+{% endblock page_subheader %}
+
+
+{% block stylesheets %}
+<style>
+  form h2, form h3, form h4, form h5, form h6,
+  .result-box h2, .result-box h3, .result-box h4, .result-box h5, .result-box h6,
+  .example h2, .example h3, .example h4, .example h5, .example h6 {
+    margin-top: 6px;
+    margin-bottom: 6px;
+  }
+
+  form {
+    margin: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  form div {
+    padding-top: 5px;
+    padding-bottom: 5px;
+  }
+
+  input {
+    width: 100%;
+  }
+
+  textarea {
+    width: 100%;
+  }
+
+  input {
+    margin-top: 5px;
+  }
+
+  pre {
+    width: 100%;
+    margin: 5px;
+    padding: 5px;
+    background-color: LightSteelBlue;
+    border: 1px solid grey;
+  }
+
+  pre.allow-break {
+    white-space: pre-wrap;
+  }
+
+  .error {
+    color: red;
+  }
+
+  .field-group {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+  }
+
+  .label p {
+    margin-top: 8px;
+    margin-bottom: 8px;
+  }
+
+  .label {
+    width: 10%;
+    padding: 0 10px 0 10px;
+    text-align: right;
+  }
+
+  .help-text {
+    font-size: 70%;
+    font-style: italic;
+    text-align: center;
+  }
+
+  .field {
+    width: 80%;
+    padding: 0 10px 0 10px;
+  }
+
+  .submit-buttons {
+    display: flex;
+    flex-direction: row;
+  }
+
+  .submit-buttons input {
+    margin: 5px;
+    padding: 5px;
+  }
+
+  .example {
+    margin-top: 25px;
+    margin-right: 10px;
+    margin-bottom: 25px;
+    margin-left: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  .result-box {
+    margin-top: 25px;
+    margin-right: 10px;
+    margin-bottom: 25px;
+    margin-left: 10px;
+    padding: 15px;
+
+    background-color: #e1e0eb;
+    border: 1px solid grey;
+  }
+
+  .result-box.sent-data pre {
+    background-color: LightBlue;
+  }
+
+  .italics {
+    margin-top: 5px;
+    margin-bottom: 8px;
+
+    font-size: 90%;
+    font-style: italic;
+    color: #575757;
+  }
+
+  h3.success-return {
+    color: DarkGreen;
+  }
+  div.success-return pre {
+    background-color: #cde4e4;
+  }
+
+  h3.error-return {
+    color: DarkRed;
+  }
+  div.error-return pre {
+    background-color: #d9cde4;
+  }
+</style>
+{% endblock stylesheets %}
+
+
+{% block content %}
+  <p>Use this to generate and send test API requests to other projects.</p>
+
+  <form method="POST">
+    <h2>API Send Form</h2>
+    <p class="italics">Use the below form to send a JSON API ping to the desired url.</p>
+
+    {% csrf_token %}
+
+    {% if form.non_field_errors %}
+      <div class="error">
+        <p>Non Field Errors:</p>
+        {{ form.non_field_errors }}
+      </div>
+      <hr>
+    {% endif %}
+
+    {% for field in form %}
+
+      <div>
+        {% if field.errors %}
+          <p class="error"> Field Error:
+          {% for error in field.errors %}
+            {{ error }}
+          {% endfor %}
+          </p>
+        {% endif %}
+
+        <div class="field-group">
+
+          <div class="label">
+            <p>{{ field.label }}:</p>
+            {% if field.help_text %}
+              <p class="help-text">{{ field.help_text|safe }}</p>
+            {% endif %}
+          </div>
+          <div class="field">
+            {{ field }}
+          </div>
+        </div>
+
+      </div>
+
+    {% endfor %}
+
+    <div class="submit-buttons">
+      <input
+        type="submit"
+        name="submit_get"
+        value="Submit as GET"
+        title="Generally used to retrieve data from the server."
+      >
+      <input
+        type="submit"
+        name="submit_post"
+        value="Submit as POST"
+        title="Generally used to send data to the server, and create a new resource."
+      >
+      <input
+        type="submit"
+        name="submit_put"
+        value="Submit as PUT"
+        title="Generally used to send data to the server, and update an existing resource by full replacement."
+      >
+      <input
+        type="submit"
+        name="submit_patch"
+        value="Submit as PATCH"
+        title="Generally used to send data to the server, and update an existing resource by partial replacement."
+      >
+      <input
+        type="submit"
+        name="submit_delete"
+        value="Submit as DELETE"
+        title="Generally used to send data to the server, and delete an existing resource."
+      >
+    </div>
+  </form>
+
+  <div class="result-box">
+    <h2>Parsed Return-Response</h2>
+
+    {% if response_error or response_success %}
+      <p class="italics">This is the data that was returned after the previous API send.</p>
+    {% endif %}
+
+    {% if response_success %}
+      <h3 class="success-return">Success Sending API Ping</h3>
+      {% for key, value in response_success.items %}
+        <div class="field-group success-return">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    {% endif %}
+
+    {% if response_error %}
+      <h3 class="error-return">Error Sending API Ping</h3>
+      {% for key, value in response_error.items %}
+        <div class="field-group error-return">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    {% endif %}
+
+    {% if not response_error and not response_success %}
+      <p class="italics">No return value yet. Submit the API form and the resulting return response will display here.</p>
+    {% endif %}
+  </div>
+
+  {% if sent_data %}
+    <div class="result-box sent-data">
+      <h2>Sent Data</h2>
+      <p class="italics">This is what was sent out from this form, on the previous API call.</p>
+      {% for key, value in sent_data.items %}
+        <div class="field-group">
+          <div class="label">
+            <p>{{ key }}</p>
+          </div>
+          <pre class="allow-break">{{ value }}</pre>
+        </div>
+      {% endfor %}
+    </div>
+  {% endif %}
+
+  <div class="example">
+    <h2>Example Send Values:</h2>
+
+    <p>Below are some example form values to get started.</p>
+
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Url:
+        </p>
+      </div>
+      <pre>http://127.0.0.1:8000/test_app/api/parse/</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Get Params:
+        </p>
+      </div>
+      <pre>test-param-1=Abc&test-param-2=123</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Header Token:
+        </p>
+      </div>
+      <pre>MyExampleHeaderAuthToken-112233445566778899ABABCDCD</pre>
+    </div>
+    <div class="field-group">
+      <div class="label">
+        <p>
+          Payload:
+        </p>
+      </div>
+      <pre>{
+  "test": true,
+  "Aaa": "Bbb",
+  "MyNumber": 5
+}</pre>
+    </div>
+
+  <hr>
+
+  <p>Above values will send:</p>
+  <div class="field-group">
+    <pre>url: http://127.0.0.1:8000/test_app/api/parse/?test-param-1=Abc&test-param-2=123
+
+header: {
+  "Accept": "application/json",
+  "token": "MyExampleHeaderAuthToken-112233445566778899ABABCDCD"
+}
+
+data: {
+  "test": true,
+  "Aaa": "Bbb",
+  "MyNumber": 5
+}</pre>
+  </div>
+
+  </div>
+
+{% endblock content %}
diff --git a/django_v3/test_app/templates/test_app/base.html b/django_v3/test_app/templates/test_app/base.html
index 0403e5d..93905d9 100644
--- a/django_v3/test_app/templates/test_app/base.html
+++ b/django_v3/test_app/templates/test_app/base.html
@@ -4,7 +4,7 @@
 <head>
   <meta charset="utf-8"/>
   <title>
-    Django v3.2 -
+    Django LTS v3.2 -
     {% block title %}
       Test App Title
     {% endblock title %}
diff --git a/django_v3/test_app/templates/test_app/index.html b/django_v3/test_app/templates/test_app/index.html
index c60d69c..5b82590 100644
--- a/django_v3/test_app/templates/test_app/index.html
+++ b/django_v3/test_app/templates/test_app/index.html
@@ -7,18 +7,49 @@
 
 {% block content %}
   <p>Test Page Content</p>
+
+  {% if is_class_view %}
+    <p><a href="{% url 'test_app:index' %}">Render page from Function view</a></p>
+  {% else %}
+    <p><a href="{% url 'test_app:index_as_class' %}">Render page from Class view</a></p>
+  {% endif %}
+
   <ul>
     <li>
-      <p><a href="{% url 'root_project_home_page:index' %}">Back to Django v2.2 Home</a></p>
-    </li>
-    <li>
-      <p><a href="{% url 'test_app:view_with_login_check' %}">Test App - Login Check Page</a></p>
-    </li>
-    <li>
-      <p><a href="{% url 'test_app:view_with_permission_check' %}">Test App - Permission Check Page</a></p>
+      <p>Standard Views:</p>
+      <ul>
+        <li>
+          <p><a href="{% url 'root_project_home_page:index' %}">Back to Django v3.2 Home</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_login_check' %}">Test App - Login Check Page</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_permission_check' %}">Test App - Permission Check Page</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:view_with_group_check' %}">Test App - Group Check Page</a></p>
+        </li>
+      </ul>
     </li>
     <li>
-      <p><a href="{% url 'test_app:view_with_group_check' %}">Test App - Group Check Page</a></p>
+      <p>API Views:</p>
+      <ul>
+        <li>
+          <p><a href="{% url 'test_app:api_parse' %}">API Parse - Receive API requests here to parse them.</a></p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:api_display' %}">API Display - View parsed API requests here.</a></p>
+          <p>
+            Note: Only displays the first received API request since last access of api_display view.
+            <br>
+            All parsed API data is purged after page access.
+          </p>
+        </li>
+        <li>
+          <p><a href="{% url 'test_app:api_send' %}">API Send - Generate and send API requests here.</a></p>
+        </li>
+      </ul>
     </li>
   </ul>
 {% endblock content %}
diff --git a/django_v3/test_app/templates/test_app/root_project_home_page.html b/django_v3/test_app/templates/test_app/root_project_home_page.html
index 86d9f57..415ec6a 100644
--- a/django_v3/test_app/templates/test_app/root_project_home_page.html
+++ b/django_v3/test_app/templates/test_app/root_project_home_page.html
@@ -7,7 +7,7 @@
 
 
 {% block page_subheader %}
-  Official LTS Support ends in April 2024
+  Official LTS Support ended in April 2024
 {% endblock page_subheader %}
 
 
diff --git a/django_v3/test_app/urls.py b/django_v3/test_app/urls.py
index 116c5b7..26528aa 100644
--- a/django_v3/test_app/urls.py
+++ b/django_v3/test_app/urls.py
@@ -16,6 +16,13 @@ urlpatterns = [
     path('view_with_permission_check/', views.view_with_permission_check, name='view_with_permission_check'),
     path('view_with_group_check/', views.view_with_group_check, name='view_with_group_check'),
 
+    # Test API views.
+    path('api/parse/', views.api_parse, name='api_parse'),
+    path('api/display/', views.api_display, name='api_display'),
+    path('api/send/', views.api_send, name='api_send'),
+
+    # Test app root, but as a class.
+    path('as_class', views.ExampleClassView.as_view(), name='index_as_class'),
     # App root.
     path('', views.index, name='index')
 ]
diff --git a/django_v3/test_app/views.py b/django_v3/test_app/views.py
index 65286b9..fecc304 100644
--- a/django_v3/test_app/views.py
+++ b/django_v3/test_app/views.py
@@ -2,10 +2,24 @@
 Views for Django v3.2 test project app.
 """
 
+# System Imports.
+import json
+import html
+import re
+import requests
+
 # Third-Party Imports.
 from django.contrib.auth.decorators import login_required, permission_required
+from django.http import JsonResponse, QueryDict
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_http_methods
+from django.views.generic import TemplateView
 from django.shortcuts import redirect, render, reverse
 
+# Internal Imports.
+from test_app.forms import ApiSendForm
+from test_app.models import ApiRequestJson
+
 
 # region Index/Root Views
 
@@ -18,6 +32,122 @@ def index(request):
     """Test app index page."""
     return render(request, 'test_app/index.html')
 
+
+class ExampleClassView(TemplateView):
+    """A basic Class Django view,
+    with some of the more common built-in methods and documentation of what they do.
+
+    Note: Some of these methods won't do anything with TemplateView. For example,
+          the form valid/invalid methods require a class that will POST form data.
+          Such as CreateView or UpdateView.
+    """
+
+    # Magic DjangoView args. Often times, can just define these and skip most method calls.
+
+    # Template to render.
+    template_name = 'test_app/index.html'
+
+    # Url to use if redirecting.
+    # If args/kwargs are needed, then probably need to use get_redirect_url() instead.
+    url = None
+
+    # If using a ModelView (ListView, DetailView, etc), these define what model data to call with.
+    model = None
+    queryset = None     # Can call more complicated query logic in get_queryset().
+
+    # If using a ListView, this determines the number of results to display per page with pagination.
+    paginate_by = 25
+
+    # Params for views with form logic.
+    form_class = None       # Form class to use.
+    initial = {}            # Initial data to populate into form, if applicable.
+    success_url = None      # If args/kwargs are needed, then probably need to use get_success_url() instead.
+
+    def dispatch(self, request, *args, **kwargs):
+        """Determines initial logic to call on view access.
+        This is one of the first methods called by Django class views.
+        This determines which of [GET(), POST(), etc] base class handling methods are called.
+        If you need redirecting or other logic prior to calling these, do it here.
+
+        If not redirecting outside of this class, then should probably always finish
+        this function by returning a call to the original dispatch method.
+        """
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        """Pulls additional context data to be used in the template."""
+
+        # Get base context object.
+        context = super().get_context_data(**kwargs)
+
+        # Add new value to context.
+        context['is_class_view'] = True
+
+        # Return context.
+        return context
+
+    def get_queryset(self):
+        """If using a view that uses models (DetailView, ListView, etc), then this modifies the default queryset."""
+        queryset = super().get_queryset()
+
+        # Use additional model query logic here.
+
+        # Return our modified queryset.
+        return queryset
+
+    def get_ordering(self):
+        """Return the field or fields to use for ordering the queryset."""
+
+        # Replace this with a return to a single model field or list of model fields to order by.
+        return super().get_ordering()
+
+    def get(self, request, *args, **kwargs):
+        """Handling for GET response type."""
+
+        # Replace this with a response object.
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        """Handling for POST response type."""
+
+        # Replace this with either a response object, or a call to
+        # form_valid()/form_invalid() functions. Depending on what you need for class logic.
+        return super().post(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        """When processing a form, this is the logic to run on form validation success."""
+
+        # Call parent logic. Should always include this line, as default views sometimes do additional processing.
+        response = super().form_valid(form)
+
+        # Do some handling with response here.
+
+        # Return some response object for render.
+        return response
+
+    def form_invalid(self, form):
+        """When processing a form, this is the logic to run on form validation failure."""
+
+        # Call parent logic. Should always include this line, as default views sometimes do additional processing.
+        response = super().form_invalid(form)
+
+        # Do some handling with response here.
+
+        # Return some response object for render.
+        return response
+
+    def get_success_url(self):
+        """When processing a form, determines how to get the url for form success redirect."""
+
+        # Replace this with a `reverse()` call to generate the correct URL.
+        return super().get_success_url()
+
+    def get_redirect_url(self, *args, **kwargs):
+        """When handling a redirect view, this determines how to get the url."""
+
+        # Replace this with a `reverse()` call to generate the correct URL.
+        return super().get_redirect_url()
+
 # endregion Index/Root Views
 
 
@@ -47,3 +177,383 @@ def view_with_group_check(request):
     return render(request, 'test_app/group_check.html')
 
 # endregion Login/Permission Test Views
+
+
+# region API Views
+
+@csrf_exempt
+@require_http_methods(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
+def api_parse(request):
+    """Takes in JSON ping, and saves incoming value to web cookies.
+
+    Then if api_display view is called after, will display the saved cookie value to web page.
+
+    Allows quick debugging to make sure the expected, correct data is being sent.
+    """
+    print('')
+    print('api_parse():')
+
+    # Get data from response.
+    get_data = {}
+    post_data = {}
+    body_data = {}
+    header_data = {}
+    if request.headers:
+        print('Received HEADERS:')
+        header_data = dict(request.headers)
+        print(header_data)
+    if request.GET:
+        print('Received GET:')
+        get_data = _recurisive_json_parse(request.GET)
+        for key, value in get_data.items():
+            get_data[key] = value[0]
+        print(get_data)
+    if request.POST:
+        print('Received POST:')
+        post_data = _recurisive_json_parse(dict(request.POST))
+        print(post_data)
+    if request.body:
+        print('Received BODY:')
+        # Attempt to escape. Limited functionality so may not work.
+        # To be precise, functions well with a standard JSON response.
+        # But with any other response type that has a body, might break and be ugly.
+        body_data = _recurisive_json_parse(html.unescape(request.body.decode('UTF-8')))
+        print(body_data)
+    print('\n')
+
+    # Combine data.
+    data = {}
+    if header_data:
+        data['HEADERS'] = header_data
+    if get_data:
+        data['GET'] = get_data
+    if post_data:
+        data['POST'] = post_data
+    if body_data:
+        data['body'] = body_data
+    if not data:
+        data = {'data': 'No data found in request.'}
+
+    # Save api data to database.
+    model_instance = ApiRequestJson.objects.first()
+    if not model_instance:
+        model_instance = ApiRequestJson.objects.create()
+    model_instance.json_value = data
+    model_instance.save()
+
+    # Generate response.
+    return JsonResponse({'success': True})
+
+
+def _recurisive_json_parse(data_item):
+    """Helper function to ensure all sub-items of response are properly read-in."""
+
+    # Convert from potentially problematic types, for easier handling.
+    if isinstance(data_item, QueryDict):
+        data_item = dict(data_item)
+    if isinstance(data_item, tuple):
+        data_item = list(data_item)
+
+    # Process some known types.
+    if isinstance(data_item, dict):
+        # Is dictionary. Iterate over each (key, value) pair and attempt to convert.
+        for key, value in data_item.items():
+            data_item[key] = _recurisive_json_parse(value)
+
+    elif isinstance(data_item, list):
+        # Is iterable. Iterate over each item and attempt to convert.
+        for index in range(len(data_item)):
+            sub_item = data_item[index]
+            data_item[index] = _recurisive_json_parse(sub_item)
+
+    else:
+        # For all other types, just attempt naive conversion
+        try:
+            data_item = json.loads(data_item)
+        except:
+            # On any failure, just skip. Leave item as-is.
+            pass
+
+    # Return parsed data.
+    return data_item
+
+
+def api_display(request):
+    """After a JSON ping to api_parse view, this displays parsed value to web page.
+
+    Allows quick debugging to make sure the expected, correct data is being sent.
+    """
+
+    # Grab api data from session, if any.
+    model_instance = ApiRequestJson.objects.first()
+    if model_instance:
+        content = {
+            'payload_data': model_instance.json_value,
+            'payload_sent_at': model_instance.date_created,
+        }
+    else:
+        content = {
+            'payload_data': {},
+            'payload_sent_at': 'N/A',
+        }
+
+    # Attempt to output api data to browser.
+    response = JsonResponse(content, safe=False)
+
+    # Delete all existing instances of saved API data.
+    ApiRequestJson.objects.all().delete()
+
+    # Return data view to user.
+    return response
+
+
+def api_send(request):
+    """Test app index page."""
+    print('\n')
+    print('api_send():')
+
+    response_success = {}
+    response_error = {}
+    sent_data = {}
+
+    # Initialize formset.
+    form = ApiSendForm()
+
+    # Check if POST.
+    if request.POST:
+        # Is POST. Process data.
+        print('Is POST submission.')
+        has_error = False
+
+        post_data = request.POST
+        form = ApiSendForm(data=post_data)
+
+        if form.is_valid():
+            # Handle for form submission.
+            print('Submitted form data:')
+            print('{0}'.format(form.cleaned_data))
+
+            send_type = ''
+            if 'submit_get' in post_data:
+                send_type = 'GET'
+                # data.pop('submit_get')
+            if 'submit_post' in post_data:
+                send_type = 'POST'
+                # data.pop('submit_post')
+            if 'submit_put' in post_data:
+                send_type = 'PUT'
+                # data.pop('submit_put')
+            if 'submit_patch' in post_data:
+                send_type = 'PATCH'
+                # data.pop('submit_patch')
+            if 'submit_delete' in post_data:
+                send_type = 'DELETE'
+                # data.pop('submit_delete')
+
+            url = str(form.cleaned_data['url']).strip()
+            get_params = str(form.cleaned_data.get('get_params', '')).strip()
+            header_token = str(form.cleaned_data.get('header_token', '')).strip()
+            payload = str(form.cleaned_data.get('payload', '{}')).strip()
+            if len(payload) > 0:
+                try:
+                    payload = json.loads(payload)
+                except json.decoder.JSONDecodeError:
+                    has_error = True
+                    payload = {}
+                    form.add_error(
+                        'payload',
+                        'Unrecognized/invalid JSON syntax. Please double check syntax and try again.',
+                    )
+            else:
+                has_error = True
+                form.add_error(
+                    'payload',
+                    'Please provide JSON data to send. If API query is meant to be empty, use {}.',
+                )
+
+            # Determine url.
+            if get_params and len(get_params) > 0:
+                if url[-1] != '?' and get_params[0] != '?':
+                    url += '?'
+                url += get_params
+
+            # Determine header values.
+            headers = {'Accept': 'application/json'}
+            if header_token:
+                headers['token'] = header_token
+
+            # Determine data values.
+            if payload:
+                data = json.dumps(payload)
+            else:
+                data = json.dumps({'success': True})
+
+            # Generate API send object.
+            try:
+                # Generate based on clicked send button.
+                if not has_error and send_type == 'GET':
+                    response = requests.get(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'POST':
+                    response = requests.post(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'PUT':
+                    response = requests.put(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'PATCH':
+                    response = requests.patch(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error and send_type == 'DELETE':
+                    response = requests.delete(
+                        url,
+                        headers=headers,
+                        data=data,
+                        timeout=5,
+                    )
+
+                elif not has_error:
+                    # Unknown send type. Somehow. Raise error.
+                    form.add_error(None, 'Invalid send_type. Was "{0}".'.format(send_type))
+            except Exception as err:
+                has_error = True
+                response_error['query_sent'] = False if not err.response else True
+                response_error['message'] = str(err.message) if hasattr(err, 'message') else str(err)
+                if 'Max retries exceeded with url' in response_error['message']:
+                    response_error['help_text'] = (
+                        'This error is often the result of a typo in the URL, or the desired endpoint being down. '
+                        'Are you sure you entered the destination URL correctly?'
+                    )
+
+            if not has_error:
+                # Handle for success state.
+
+                # Display sent input data to user.
+                # That way they can change the form for a subsequent request and still see what was sent last time.
+                sent_data['send_type'] = send_type
+                sent_data['url'] = url
+                sent_data['headers'] = headers
+                sent_data['content'] = data
+
+                # Parse returned response status code.
+                response_success['status'] = response.status_code
+                if response_success['status'] >= 400:
+                    # Define help_text key now to preserve location in display ordering.
+
+                    # Provide help text for some common error statuses.
+                    if response_success['status'] == 400:
+                        # 400: Bad Request
+                        response_success['help_text'] = (
+                            '400: Bad Request - This error is often the result of a bad or malformed request, such '
+                            'as incorrect or unexpected syntax. Double check that the sent request data is correct.'
+                        )
+                    elif response_success['status'] == 401:
+                        # 401: Unauthorized
+                        response_success['help_text'] = (
+                            '401: Unauthorized - This error is often the result of invalid or missing authentication '
+                            'credentials. Are you sure the authentication tokens are correctly provided?'
+                        )
+                    elif response_success['status'] == 403:
+                        # 403: Forbidden
+                        response_success['help_text'] = (
+                            '403: Forbidden - This error is often the result of invalid or missing authentication '
+                            'credentials. Are you sure the authentication tokens are correctly provided?'
+                        )
+                    elif response_success['status'] == 404:
+                        # 404: Not Found
+                        response_success['help_text'] = (
+                            '404: Not Found - This error is often the result of the requested url not existing on the '
+                            'server. Are you sure you entered the destination URL correctly?'
+                        )
+                    elif response_success['status'] == 405:
+                        # 405: Method Not Allowed
+                        response_success['help_text'] = (
+                            '405: Method Not Allowed - This error is often the result of the destination understanding '
+                            'the sent response type (GET/POST/PUT/PATCH/DELETE), but not supporting said type. '
+                            'If this is a server you have access to, then double check that the endpoint is configured '
+                            'correctly.'
+                        )
+                    elif response_success['status'] == 415:
+                        # 415: Unsupported Media Type
+                        response_success['help_text'] = (
+                            '415: Unsupported Media Type - This error is often the result of the destination '
+                            'being unable to parse the provided content. Are you sure the payload was entered '
+                            'correctly?'
+                        )
+                    elif response_success['status'] == 500:
+                        # 500: Server Error
+                        response_success['help_text'] = (
+                            '500: Server Error - This error is often the result of your request being received, but '
+                            'the server broke when trying to process the request. If this is a server you have '
+                            'access to, then double check the server logs for more details.'
+                        )
+
+                # Parse returned response header data.
+                if response.headers:
+                    response_success['headers'] = response.headers
+
+                # Parse returned response content.
+                if response.headers['content-Type'] and response.headers['Content-Type'] == 'application/json':
+                    response_success['content'] = response.json()
+                else:
+                    content = html.unescape(response.content.decode('UTF-8'))
+
+                    # NOTE: Below copied from Django ExpandedTestCase package.
+                    # Replace html linebreak with actual newline character.
+                    content = re.sub('<br>|</br>|<br/>|<br />', '\n', content)
+
+                    # Replace non-breaking space with actual space character.
+                    content = re.sub('(&nbsp;)+', ' ', content)
+
+                    # Replace any carriage return characters with newline character.
+                    content = re.sub(r'\r+', '\n', content)
+
+                    # Replace any whitespace trapped between newline characters.
+                    # This is empty/dead space, likely generated by how Django handles templating.
+                    content = re.sub(r'\n\s+\n', '\n', content)
+
+                    # Replace any repeating linebreaks.
+                    content = re.sub(r'\n\n+', '\n', content)
+
+                    # # Reduce any repeating whitespace instances.
+                    # content = re.sub(r' ( )+', ' ', content)
+
+                    # Strip final calculated string of extra outer whitespace.
+                    content = str(content).strip()
+                    response_success['content'] = content
+
+                # Handle if was response was received, but it gave error level status.
+                if response_success['status'] >= 400:
+                    response_error = response_success
+                    response_success = {}
+
+    print('Rendering response...')
+    print('')
+
+    return render(request, 'test_app/api_send.html', {
+        'form': form,
+        'sent_data': sent_data,
+        'response_success': response_success,
+        'response_error': response_error,
+    })
+
+# endregion API Views
-- 
GitLab