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
|
||||
# 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
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