mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
minimal flask app starting point
This commit is contained in:
parent
a19eb0d17f
commit
24646e42ca
27 changed files with 393 additions and 1 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -158,5 +158,5 @@ cython_debug/
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# 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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|
||||||
|
|
91
app/__init__.py
Normal file
91
app/__init__.py
Normal 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
5
app/admin/__init__.py
Normal 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
0
app/admin/routes.py
Normal file
5
app/auth/__init__.py
Normal file
5
app/auth/__init__.py
Normal 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
0
app/auth/routes.py
Normal file
35
app/cli.py
Normal file
35
app/cli.py
Normal 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
5
app/errors/__init__.py
Normal 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
14
app/errors/handlers.py
Normal 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
5
app/main/__init__.py
Normal 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
15
app/main/routes.py
Normal 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
0
app/models.py
Normal file
9
app/static/browserconfig.xml
Normal file
9
app/static/browserconfig.xml
Normal 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
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
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
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
0
app/static/js/scripts.js
Normal file
21
app/static/site.webmanifest
Normal file
21
app/static/site.webmanifest
Normal 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": "/"
|
||||||
|
}
|
0
app/static/structure.scss
Normal file
0
app/static/structure.scss
Normal file
0
app/static/style.scss
Normal file
0
app/static/style.scss
Normal file
90
app/templates/base.html
Normal file
90
app/templates/base.html
Normal 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>
|
17
app/templates/errors/404.html
Normal file
17
app/templates/errors/404.html
Normal 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 %}
|
17
app/templates/errors/500.html
Normal file
17
app/templates/errors/500.html
Normal 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
3
babel.cfg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[python: app/**.py]
|
||||||
|
[jinja2: app/templates/**.html]
|
||||||
|
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
18
config.py
Normal file
18
config.py
Normal 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
17
pyfedi.py
Normal 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
11
requirements.txt
Normal 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
|
Loading…
Reference in a new issue