minimal flask app starting point

This commit is contained in:
rimu 2023-07-28 16:22:12 +12:00
parent a19eb0d17f
commit 24646e42ca
27 changed files with 393 additions and 1 deletions

2
.gitignore vendored
View file

@ -158,5 +158,5 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/

91
app/__init__.py Normal file
View file

@ -0,0 +1,91 @@
import logging
from logging.handlers import SMTPHandler, RotatingFileHandler
import os
from flask import Flask, request, current_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from flask_moment import Moment
from flask_babel import Babel, lazy_gettext as _l
from config import Config
db = SQLAlchemy(session_options={"autoflush": False})
migrate = Migrate()
#login = LoginManager()
#login.login_view = 'auth.login'
#login.login_message = _l('Please log in to access this page.')
mail = Mail()
moment = Moment()
babel = Babel()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
#login.init_app(app)
mail.init_app(app)
moment.init_app(app)
babel.init_app(app, locale_selector=get_locale)
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
from app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
def get_resource_as_string(name, charset='utf-8'):
with app.open_resource(name) as f:
return f.read().decode(charset)
app.jinja_env.globals['get_resource_as_string'] = get_resource_as_string
# send error reports via email
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'],
app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='rimu@chorebuster.net',
toaddrs=app.config['ADMINS'], subject='CB Failure',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
# log rotation
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/pyfedi.log',
maxBytes=1002400, backupCount=15)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Started!') # let's go!
return app
def get_locale():
try:
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
except:
return 'en_US'
from app import models

5
app/admin/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('admin', __name__)
from app.admin import routes

0
app/admin/routes.py Normal file
View file

5
app/auth/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes

0
app/auth/routes.py Normal file
View file

35
app/cli.py Normal file
View file

@ -0,0 +1,35 @@
import click
import os
def register(app):
@app.cli.group()
def translate():
"""Translation and localization commands."""
pass
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system(
'pybabel init -i messages.pot -d app/translations -l ' + lang):
raise RuntimeError('init command failed')
os.remove('messages.pot')
@translate.command()
def update():
"""Update all languages."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system('pybabel update -i messages.pot -d app/translations'):
raise RuntimeError('update command failed')
os.remove('messages.pot')
@translate.command()
def compile():
"""Compile all languages."""
if os.system('pybabel compile -d app/translations'):
raise RuntimeError('compile command failed')

5
app/errors/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers

14
app/errors/handlers.py Normal file
View file

@ -0,0 +1,14 @@
from flask import render_template
from app import db
from app.errors import bp
@bp.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500

5
app/main/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes

15
app/main/routes.py Normal file
View file

@ -0,0 +1,15 @@
from app.main import bp
from flask import g
from flask_moment import moment
from flask_babel import _, get_locale
@bp.before_app_request
def before_request():
g.locale = str(get_locale())
@bp.route('/', methods=['GET', 'POST'])
@bp.route('/index', methods=['GET', 'POST'])
def index():
return 'Hello world'

0
app/models.py Normal file
View file

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/static/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

6
app/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
app/static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
app/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

0
app/static/js/scripts.js Normal file
View file

View file

@ -0,0 +1,21 @@
{
"name": "PyFedi",
"short_name": "PyFedi",
"icons": [
{
"src": "/static/images/logo_square_192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/images/logo_square_512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#007BBF",
"background_color": "#ffffff",
"display": "standalone",
"start_url": "/",
"scope": "/"
}

View file

0
app/static/style.scss Normal file
View file

90
app/templates/base.html Normal file
View file

@ -0,0 +1,90 @@
<!doctype html>
<html lang="en">
<head>
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Language" content="en" />
<meta name="msapplication-TileColor" content="#007BBF">
<meta name="theme-color" content="#007BBF">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
{% block styles %}
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ url_for('static', filename='structure.css', changed=getmtime('structure.css')) }}" type="text/css" rel="stylesheet" />
<link href="{{ url_for('static', filename='styles.css', changed=getmtime('styles.css')) }}" type="text/css" rel="stylesheet" />
{% endblock %}
<title>{% if title %}{{ title }} - {{ _('PyFedi') }}{% else %}{{ _('PyFedi') }}{% endif %}</title>
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="manifest" href="/static/site.webmanifest">
<link rel="shortcut icon" type="image/png" href="/static/favicon-32x32.png">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="/static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
{% endblock %}
</head>
<body class="d-flex flex-column" style="padding-top: 43px;">
<!-- Page content -->
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="https://www.chorebuster.net/" target="_blank">Logo</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent" role="navigation">
<ul class="nav navbar-nav ml-md-4">
{% if current_user.is_anonymous %}
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/">{{ _('Home') }}</a></li>
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/how-it-works.php">{{ _('How it works') }}</a></li>
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/news.php">{{ _('News') }}</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/">{{ _('Home') }}</a></li>
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/how-it-works.php">{{ _('How it works') }}</a></li>
<li class="nav-item"><a class="nav-link" href="https://www.chorebuster.net/news.php">{{ _('News') }}</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endblock %}
{% block content %}
<div id="outer_container" class="container-fluid flex-shrink-0">
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{# application content needs to be provided in the app_content block #}
{% block app_content %}{% endblock %}
</div>
<footer class="footer mt-auto">
</footer>
{% endblock %}
{% block scripts %}
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}
{% endblock %}
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
{% block end_scripts %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block app_content %}
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ _('Ooops, something is broken!') }}</h3>
</div>
<div class="card-body">
<p>{{ _('The page your browser tried to load could not be found.') }}</p>
<p><a href="#">{{ _('Back') }}</a></p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block app_content %}
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ _('An unexpected error has occurred') }}</h3>
</div>
<div class="card-body">
<p>{{ _('Sorry for the inconvenience! Please let us know about this, so we can repair it and make ChoreBuster better for everyone.') }}</p>
<p><a href="#">{{ _('Back') }}</a></p>
</div>
</div>
</div>
</div>
{% endblock %}

3
babel.cfg Normal file
View file

@ -0,0 +1,3 @@
[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

18
config.py Normal file
View file

@ -0,0 +1,18 @@
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config(object):
SERVER_NAME = os.environ.get('SERVER_NAME') or 'app.chorebuster.net'
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')

17
pyfedi.py Normal file
View file

@ -0,0 +1,17 @@
from app import create_app, db, cli
import os
app = create_app()
cli.register(app)
@app.context_processor
def app_context_processor(): # NB there needs to be an identical function in cb.wsgi to make this work in production
def getmtime(filename):
return os.path.getmtime('app/static/' + filename)
return dict(getmtime=getmtime)
@app.shell_context_processor
def make_shell_context():
return {'db': db}

11
requirements.txt Normal file
View file

@ -0,0 +1,11 @@
Flask==2.3.2
python-dotenv==1.0.0
flask-wtf==1.1.1
flask-sqlalchemy==3.0.5
flask-migrate==4.0.4
flask-login==0.6.2
email-validator==2.0.0
flask-mail==0.9.1
flask-moment==1.0.5
flask-babel==3.1.0
psycopg2-binary