Add initial web app scaffolding
This commit is contained in:
parent
5e00046622
commit
9278bfeee9
32
section7/__main__.py
Normal file
32
section7/__main__.py
Normal 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
33
section7/application.py
Normal 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
16
section7/filters.py
Normal 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,)
|
23
section7/router/__init__.py
Normal file
23
section7/router/__init__.py
Normal 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),)
|
122
section7/router/error_handler.py
Normal file
122
section7/router/error_handler.py
Normal 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
24
section7/router/home.py
Normal 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()
|
14
section7/templates/base.html.j2
Normal file
14
section7/templates/base.html.j2
Normal 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>
|
10
section7/templates/error.html.j2
Normal file
10
section7/templates/error.html.j2
Normal 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 %}
|
10
section7/templates/home.html.j2
Normal file
10
section7/templates/home.html.j2
Normal 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 %}
|
56
section7/templates/macros.html.j2
Normal file
56
section7/templates/macros.html.j2
Normal 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 %}
|
Reference in New Issue
Block a user