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