Compare commits

..

3 Commits

Author SHA1 Message Date
Raoul Snyman f57e28f50b
Fix the date format and include the time 2021-09-17 12:00:01 -07:00
Raoul Snyman 7a826cc4bc
Add incidents to the page 2021-09-17 11:56:22 -07:00
Raoul Snyman 6b29e2a373
Get the ball rolling
- Add templates
- Add static assets
- Add module setup
2021-09-17 10:29:14 -07:00
14 changed files with 77 additions and 287 deletions

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length = 120

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
__pycache__ __pycache__
*.py[co] *.py[co]
*.sqlite *.sqlite
data/
dist/

View File

@ -1,6 +0,0 @@
FROM tiangolo/meinheld-gunicorn-flask
LABEL org.opencontainers.image.authors="Liberty Tech Force"
RUN pip install --no-cache-dir flask_admin flask_sqlalchemy flask_login psycopg2-binary pymysql 'greenlet<0.5,>=0.4.5'
COPY ./statusforce /app/statusforce
COPY ./docker/main.py /app/

View File

@ -1,20 +0,0 @@
version: '3'
services:
web:
build:
context: .
ports:
- "8000:80"
environment:
- SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://statusforce:statusforce@db/statusforce?sslmode=disable
- FLASK_SECRET_KEY=my-super-secret-key
depends_on:
- db
db:
image: postgres
environment:
- POSTGRES_USER=statusforce
- POSTGRES_PASSWORD=statusforce
- POSTGRES_DB=statusforce
volumes:
- "./data:/var/lib/postgresql/data"

View File

@ -1,3 +0,0 @@
from statusforce.app import get_app
app = get_app()

View File

@ -1,64 +0,0 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "StatusForce"
description = "Simple status page for system status"
readme = "README.rst"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Raoul Snyman", email = "raoul@libertytechforce.com" },
]
keywords = [
"status",
"website",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Utilities",
]
dependencies = [
"Flask",
"Flask-Admin",
"Flask-Login",
"Flask-SQLAlchemy",
"psycopg2-binary",
"pymysql",
"greenlet<0.5,>=0.4.5"
]
dynamic = ["version"]
[project.urls]
Homepage = "https://libertytechforce.com"
Issues = "https://git.libertytechforce.com/libertytechforce/statusforce/issues"
Source = "https://git.libertytechforce.com/libertytechforce/statusforce"
[project.optional-dependencies]
tests = [
"pytest",
"pytest-faker",
"pytest-flask"
]
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.sdist]
include = [
"/statusforce",
]
[tool.hatch.build.targets.wheel]
include = [
"statusforce",
]

37
setup.cfg 100644
View File

@ -0,0 +1,37 @@
[metadata]
name = StatusForce
version = 0.0.1
author = Raoul Snyman
author_email = raoul@libertytechforce.com
description = Simple status page for system status
long_description = file:README.rst
long_description_content_type = text/x-rst
url = https://libertytechforce.com
license = MIT
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Utilities
keywords = website, status
[options]
py_modules = statusforce
python_requires = >=3.7
install_requires =
Flask
Flask-Admin
Flask-SQLAlchemy
Flask-Login
[bdist_wheel]
universal = 1
[flake8]
max-line-length = 120

6
setup.py 100644
View File

@ -0,0 +1,6 @@
"""
The statusforce package
"""
from setuptools import setup
setup()

View File

@ -1,48 +1,36 @@
import os from flask import Flask, render_template
from flask import Flask
from flask_admin import Admin from flask_admin import Admin
from flask_login import LoginManager from flask_admin.contrib.sqla import ModelView
from statusforce.db import db, session from statusforce.db import db, session
from statusforce.models import Service, Incident, User from statusforce.models import Service, Incident
from statusforce.views import AuthAdminIndexView, AuthModelView, main
def get_app(): # Set up Flask application and initial config
# Set up Flask application and initial config app = Flask(__name__)
app = Flask(__name__) app.secret_key = b'GSADFGST#$%^$%&^2345234534576476'
app.secret_key = b'super-secret-please-replace-me' app.config['FLASK_ADMIN_SWATCH'] = 'materia'
app.config['FLASK_ADMIN_SWATCH'] = 'cosmo' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///statusforce.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///statusforce.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Pull configuration from environment variables # Set up database
for key, value in os.environ.items(): db.init_app(app)
if key.startswith('FLASK_ADMIN_') or key.startswith('SQLALCHEMY_'): with app.app_context():
app.config[key] = value db.create_all()
elif key == 'FLASK_SECRET_KEY':
# Special case, apparently
app.secret_key = value
elif key.startswith('FLASK_'):
app.config[key.replace('FLASK_', '')] = value
# Set up database # Set up admin interface
db.init_app(app) admin = Admin(app, name='StatusForce', template_mode='bootstrap4')
with app.app_context(): admin.add_view(ModelView(Service, session))
db.create_all() admin.add_view(ModelView(Incident, session))
# Set up the authentication
login_manager = LoginManager(app)
@login_manager.user_loader @app.route('/')
def load_user(user_id): def index():
return User.query.get(user_id) """
Show the status page
# Set up admin interface """
admin = Admin(app, name='StatusForce', index_view=AuthAdminIndexView(), template_mode='bootstrap4') services = Service.query.all()
admin.add_view(AuthModelView(Service, session)) incidents = Incident.query.order_by(Incident.created.desc()).limit(10).all()
admin.add_view(AuthModelView(Incident, session)) service_statuses = [service.status for service in services]
overall_status = 'operational' if all(status == 'operational' for status in service_statuses) else \
# Finally, attache the blueprint 'offline' if all(status == 'offline' for status in service_statuses) else 'unclear'
app.register_blueprint(main) return render_template('index.html', services=services, incidents=incidents, overall_status=overall_status)
return app

View File

@ -7,11 +7,9 @@ from statusforce.db import Model, Column, ForeignKey, Boolean, DateTime, Enum, I
class Service(Model): class Service(Model):
__tablename__ = 'services'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
status = Column(Enum('operational', 'unclear', 'offline', native_enum=False), default='operational') status = Column(Enum('operational', 'unclear', 'offline'), default='operational')
incidents = relationship('Incident', backref=backref('service', lazy=True)) incidents = relationship('Incident', backref=backref('service', lazy=True))
def __str__(self): def __str__(self):
@ -19,10 +17,8 @@ class Service(Model):
class Incident(Model): class Incident(Model):
__tablename__ = 'incidents'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
service_id = Column(Integer, ForeignKey('services.id')) service_id = Column(Integer, ForeignKey('service.id'))
title = Column(String, nullable=False) title = Column(String, nullable=False)
description = Column(Text) description = Column(Text)
is_resolved = Column(Boolean, default=False) is_resolved = Column(Boolean, default=False)
@ -31,27 +27,3 @@ class Incident(Model):
def __str__(self): def __str__(self):
return '{} ({})'.format(self.title, self.service.name) return '{} ({})'.format(self.title, self.service.name)
class User(Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
full_name = Column(String)
email = Column(String, nullable=False)
password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
@property
def is_authenticated(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return self.id
def __str__(self):
return self.full_name or self.email

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@ -1,22 +0,0 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% block head %}
{{ super() }}
{{ lib.form_css() }}
{% endblock head %}
{% block body %}
{{ super() }}
{% if current_user.is_authenticated %}
<p class="lead">Use the menu items above to add services and incidents.</p>
{% else %}
{{lib.render_form(form)}}
{% endif %}
{% endblock body %}
{% block tail %}
{{super()}}
{{lib.form_js()}}
{% endblock tail %}

View File

@ -6,7 +6,6 @@
<title>System Status</title> <title>System Status</title>
<link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet"> <link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet">
<link href="{{url_for('static', filename='css/bootstrap-icons.css')}}" rel="stylesheet"> <link href="{{url_for('static', filename='css/bootstrap-icons.css')}}" rel="stylesheet">
<link rel="icon" href="{{url_for('static', filename='img/ltf-logo.png')}}">
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
font-size: 1.125rem; font-size: 1.125rem;
@ -27,7 +26,7 @@
<main> <main>
<div class="py-5 text-center"> <div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="{{url_for('static', filename='img/ltf-logo.svg')}}" alt="" width="72" height="57"> <img class="d-block mx-auto mb-4" src="{{url_for('static', filename='img/ltf-logo.svg')}}" alt="" width="72" height="57">
<h2>Systems Status</h2> <h2>Server Status</h2>
<p class="lead">The current status of the Liberty Tech Force systems.</p> <p class="lead">The current status of the Liberty Tech Force systems.</p>
</div> </div>
{% if overall_status == 'operational' %} {% if overall_status == 'operational' %}
@ -56,21 +55,13 @@
{% endif %} {% endif %}
{{service.name}} {{service.name}}
{% if service.status == 'operational' %} {% if service.status == 'operational' %}
<span class="text-success"> <span class="p-2 bg-success rounded-circle">
{% elif service.status == 'unclear' %} {% elif service.status == 'unclear' %}
<span class="text-warning"> <span class="p-2 bg-warning rounded-circle">
{% elif service.status == 'offline' %} {% elif service.status == 'offline' %}
<span class="text-danger"> <span class="p-2 bg-danger rounded-circle">
{% endif %} {% endif %}
{{service.status.title()}} <span class="visually-hidden">{{service.status}}</span>
&nbsp;
{% if service.status == 'operational' %}
<i class="bi-check-circle-fill"></i>
{% elif service.status == 'unclear' %}
<i class="bi-question-circle-fill"></i>
{% elif service.status == 'unclear' %}
<i class="bi-exclamation-circle-fill"></i>
{% endif %}
</span> </span>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -1,85 +0,0 @@
from flask import Blueprint, redirect, request, url_for, render_template
from flask_admin import AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from flask_admin.helpers import validate_form_on_submit
from flask_login import current_user, login_user, logout_user
from werkzeug.security import check_password_hash
from wtforms.form import Form
from wtforms.fields import StringField, PasswordField
from wtforms.validators import ValidationError, required
from statusforce.models import Service, Incident, User
# The blueprint for the main page
main = Blueprint('main', __name__, url_prefix='/')
class LoginForm(Form):
email = StringField(validators=[required()])
password = PasswordField(validators=[required()])
def validate_login(self, field):
user = self.get_user()
if user is None or not check_password_hash(user.password, self.password.data):
raise ValidationError('Unable to log user in')
def get_user(self):
return User.query.filter_by(email=self.email.data).first()
class AuthModelView(ModelView):
"""
A derivative ModelView that supports user authentication
"""
def is_accessible(self):
return current_user.is_authenticated
class AuthAdminIndexView(AdminIndexView):
"""
A derivative AdminIndexView that supports user authentication
"""
@expose('/')
def index(self):
"""
Override the index page implementation to check for user permissions, and redirect to the login page
"""
if not current_user.is_authenticated:
return redirect(url_for('.login'))
return super().index()
@expose('/login/', methods=('GET', 'POST'))
def login(self):
"""
A page for the user to log in
"""
form = LoginForm(request.form)
if validate_form_on_submit(form):
user = form.get_user()
login_user(user)
if current_user.is_authenticated:
return redirect(url_for('.index'))
self._template_args['form'] = form
return super().index()
@expose('/logout/')
def logout(self):
"""
Log the user out of the system
"""
logout_user()
return redirect(url_for('.index'))
@main.route('')
def index():
"""
Show the status page
"""
services = Service.query.all()
incidents = Incident.query.order_by(Incident.created.desc()).limit(10).all()
service_statuses = [service.status for service in services]
overall_status = 'operational' if all(status == 'operational' for status in service_statuses) else \
'offline' if all(status == 'offline' for status in service_statuses) else 'unclear'
return render_template('index.html', services=services, incidents=incidents, overall_status=overall_status)