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('( )+', ' ', 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('( )+', ' ', 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