Add initial web app scaffolding

This commit is contained in:
Ethan Paul 2022-02-08 23:17:35 -05:00
parent 5e00046622
commit 9278bfeee9
No known key found for this signature in database
GPG Key ID: 6A337337DF6B5B1A
10 changed files with 340 additions and 0 deletions

32
section7/__main__.py Normal file
View File

@ -0,0 +1,32 @@
"""Development server stub entrypoint
Flask comes with a built-in development server. This entrypoint allows ``section7``
to be run directly to run the development server and expose some simple config options
for ease of access. Run the below command to start the server:
::
python -m section7
.. warning:: As the development server will tell you on startup, do not use this for
production deployments.
"""
import argparse
from section7.application import APPLICATION
def main():
"""Run the werkzeug development server"""
parser = argparse.ArgumentParser()
parser.add_argument("-b", "--bind", help="Host to listen on", default="127.0.0.1")
parser.add_argument(
"-p", "--port", help="Port to listen on", default=5000, type=int
)
parser.add_argument("-d", "--debug", help="Run in debug mode", action="store_true")
args = parser.parse_args()
APPLICATION.run(host=args.bind, port=args.port, debug=args.debug, load_dotenv=True)
if __name__ == "__main__":
main()

33
section7/application.py Normal file
View File

@ -0,0 +1,33 @@
import flask
import flask_assets
from section7 import configuration
from section7 import database
from section7 import filters
from section7 import router
class Section7Flask(flask.Flask):
"""Stub class to typehint the ``section7`` attribute with the application config"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.section7 = configuration.load()
APPLICATION = Section7Flask(__name__)
assets = flask_assets.Environment(APPLICATION)
# Register the global application handler
APPLICATION.register_error_handler(Exception, router.handle_error)
# Initialize the database connection
APPLICATION.before_first_request(database.initialize)
# Inject the custom jinja2 filters so that they're available in the templates
for custom_filter in filters.FILTERS:
APPLICATION.jinja_env.filters[custom_filter.__name__] = custom_filter
# Attach the application route endpoints
for route in router.ROUTES:
APPLICATION.route(route.route, methods=list(route.methods))(route.entrypoint)

16
section7/filters.py Normal file
View File

@ -0,0 +1,16 @@
from typing import Callable
from typing import Optional
from typing import Tuple
import flask
def url_for(value: str, filename: Optional[str] = None) -> str:
"""Wrapper around :func:`flask.url_for` to expose the functionality in a template"""
if filename:
return flask.url_for(value, filename=filename)
return flask.url_for(value)
FILTERS: Tuple[Callable, ...] = (url_for,)

View File

@ -0,0 +1,23 @@
from typing import Callable
from typing import NamedTuple
from typing import Sequence
from typing import Tuple
from section7.router.error_handler import handle_error
from section7.router.home import display_home
class Routable(NamedTuple):
"""Structure for storing details for a given route
:param route: String indicating the Flask URL rule to apply to the route entry
:param entrypoint: The callable that Flask should invoke when the route is accessed
:param methods: Sequence of HTTP method verbs that should be accepted by the route
"""
route: str
entrypoint: Callable
methods: Sequence[str] = ("GET",)
ROUTES: Tuple[Routable, ...] = (Routable(route="/", entrypoint=display_home),)

View File

@ -0,0 +1,122 @@
"""Custom error handler and error page rendering endpoint"""
import enum
import logging
import uuid
from dataclasses import dataclass
from dataclasses import field
from typing import Tuple
import flask
import werkzeug
class ErrorColor(enum.Enum):
"""Enum of possible colors for the error page to be rendered with
Colors can be used to indicate the severity of the problem to a user. Generally, red is reserved
for fatal errors (errors that the user cannot fix themselves), orange for user input or
temporary errors (errors that the user can potentially fix themselves), and blue for
informational errors that don't necessarily indicate a problem, but still needs to block user
access.
Names are the intended usage of the color while values are the CSS classes that the page
uses for the corresponding color.
"""
WARNING = "warning"
ERROR = "error"
INFORMATIONAL = "informational"
class ErrorIcon(enum.Enum):
"""Enum of possible icons for the error page
Names are the intended usage of the icon while values are the Font Awesome CSS class structure
for the corresponding icon.
"""
EXCLAMATION = "fas fa-exclamation-triangle"
INFORMATION = "fas fa-exclamation-circle"
@dataclass
class ErrorDetails:
"""Container of details for an error that can be used to render an error page
:param code: The HTTP response code for the error
:param title: Page title/short description of the error
:param description: Long description of the error. Ideally should include an action the user
can take to debug or fix the problem.
:param icon: The icon that should be added to the page
:param color: The color of the page highlights
"""
code: int = 500
title: str = "Internal Server Error"
description: str = "The application encountered an unhandled error while trying to fulfill the request."
icon: ErrorIcon = ErrorIcon.EXCLAMATION
color: ErrorColor = ErrorColor.ERROR
event_id: str = field(default_factory=lambda: uuid.uuid4().hex)
def handle_error(error: Exception) -> Tuple[str, int]:
"""Handle an exception raised elsewhere in the application
:returns: Rendered template of an error page for the raised exception
"""
logger = logging.getLogger(__name__)
# If the application is running in debug mode then we re-raise the exception so that the
# developer gets the helpful browser-rendered stack trace werkzeug provides
if flask.current_app.config["DEBUG"]:
logger.warning(
"Application is running in debug mode, stack trace will be returned to client"
)
raise error
details = ErrorDetails()
if isinstance(error, NotImplementedError):
details.title = "Not Implemented"
details.description = "The functionality for this resource has not yet been implemented. The resource name is reserved for for future use"
details.color = ErrorColor.INFORMATIONAL
details.icon = ErrorIcon.INFORMATION
elif isinstance(error, werkzeug.exceptions.HTTPException):
details.code = error.code
details.title = error.name
details.description = error.description
if 400 <= error.code < 500:
details.color = ErrorColor.WARNING
details.icon = ErrorIcon.INFORMATION
else:
logger.exception(
f"Event ID {details.event_id}: unhandled application error at '{flask.request.full_path}'"
)
logger.error(
f"Event ID {details.event_id}: Error {details.code} {details.description}"
)
def _render_template(
title: str,
error: ErrorDetails,
) -> str:
"""Stub function to type, enumerate, and document the template parameters
:param title: Value of the page title header
:param error: Container of error details about the error to render on the page
"""
return flask.render_template(
"error.html.j2",
title=title,
error=error,
)
return (
_render_template(
title=f"{details.title} ({details.code})",
error=details,
),
details.code,
)

24
section7/router/home.py Normal file
View File

@ -0,0 +1,24 @@
"""Endpoint for displaying the main homepage of the application"""
import flask
def display_home() -> str:
"""Display the home page
:returns: A response rendering the home page template
"""
def _render_template() -> str:
"""Stub function to type, enumerate, and document the template parameters
:param version: Semantic version of the currently running application
:param common_urls: Container of common application URLs used in mutliple templates
:param testers: List of tester names that the logged in user can read to link to for
easy access search access
:param messages: Optional list of Flask flash messages to render as popup notifications
"""
return flask.render_template(
"home.html.j2",
)
return _render_template()

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block header %}{% endblock %}
</head>
<body>
<div id="preloader"><div class="spinner"><div></div></div></div>
<div id="main" class="grid-x grid-margin-x">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
{% extends "base.html.j2" %}
{% from "macros.html.j2" import make_header %}
{% block header %}
{{ make_header(title) }}
{% endblock %}
{% block content %}
{{ error.title }}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html.j2" %}
{% from "macros.html.j2" import make_header %}
{% block header %}
{{ make_header('Disclose My Pay') }}
{% endblock %}
{% block content %}
Hello world
{% endblock %}

View File

@ -0,0 +1,56 @@
{% macro make_header(title, js_bundle=none, css_bundle=none) %}
<!-- OpenGraph integration meta -->
<meta property="og:title" content="{{ title }}"/>
<meta property="og:url" content="https://enpaul.net/"/>
<meta property='og:site_name' content="Disclose My Pay"/>
<meta property="og:type" content="website"/>
<meta property='og:locale' content="en_US"/>
<meta property="og:image" content="{{ 'static' | url_for('assets/section7.png') }}"/>
<meta property='og:description' content="Share your compensation information privately with your coworkers"/>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="author" content="admin@enp.one"/>
<meta name="description" content="Share your compensation information privately with your coworkers"/>
<title>{{ title }}</title>
<link rel="shortcut icon" href="{{ 'static' | url_for('assets/section7.ico') }}">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="32x32">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="16x16">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="8x8">
<!-- third party includes -->
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/css/foundation.min.css"
integrity="sha256-ogmFxjqiTMnZhxCqVmcqTvjfe1Y/ec4WaRj/aQPvn+I="
crossorigin="anonymous"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/js/foundation.min.js"
integrity="sha256-pRF3zifJRA9jXGv++b06qwtSqX1byFQOLjqa2PTEb2o="
crossorigin="anonymous"
></script>
{% if js_bundle is not none %}{% assets js_bundle %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}{% endif %}
{% if css_bundle is not none %}{% assets css_bundle %}
<link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets %}{% endif %}
{% endmacro %}