Initial commit

This commit is contained in:
Maryam Bint Ibrahim 2023-08-17 16:25:36 +02:00
commit 001de296cc
37 changed files with 1866 additions and 0 deletions

1
.flaskenv Normal file
View File

@ -0,0 +1 @@
FLASK_APP=project.app:create_app

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
actual.env
.env
.venv
venv
data.db
*__pycache__*

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"args": [
"run",
"--debug",
],
"jinja": true,
"justMyCode": true
}
]
}

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn project.app:app

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Flask Secure application
This is a template for a Flask application, using Flask-Security-Too and SQLAlchemy.
## Usage
1. Clone the project
2. Rename prod.env or dev.env to .env
3. In .env, modify what you need to
4. Create and activate python virtual env
5. Install requirements with `pip install -r requirements.txt`
6. Generate secret keys and paswords salts and put them in .env : `openssl rand -hex 32`
6. Now you can run your app with `flask run`

22
model.env Normal file
View File

@ -0,0 +1,22 @@
APP_SETTINGS="project.config.DevelopmentConfig"
DATABASE_URL="sqlite:///data.db"
DEV_SECRET_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
DEV_SECURITY_PASSWORD_SALT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
DOKKU_APP_RESTORE="1"
DOKKU_APP_TYPE="herokuish"
DOKKU_PROXY_PORT="80"
DOKKU_PROXY_PORT_MAP="http:80:5000 https:443:5000"
DOKKU_PROXY_SSL_PORT="443"
FLASK_APP="project.app"
GOOGLE_OAUTH_CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
GOOGLE_OAUTH_CLIENT_SECRET="GOCSPX-XXXXXX-XXXXXXXXXXXXXXXXX"
MAIL_PASSWORD="xxxxxxxxxxxxxxxxxx"
MAIL_PORT="587"
MAIL_SERVER="mail.xx.xx"
MAIL_USERNAME="mail@mail.xx"
MAIL_USE_TLS="True"
OAUTHLIB_INSECURE_TRANSPORT="true"
PRODUCTION_SECRET_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx"
PRODUCTION_SECURITY_PASSWORD_SALT="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx"
SQLALCHEMY_ECHO="True"
ALLOWED_USERS=["user@test.xx, "admin@test.xx"]

53
project/admin.py Normal file
View File

@ -0,0 +1,53 @@
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_security import Security, current_user
from wtforms.fields import PasswordField
from flask import current_app
from database import db_session
from models import User, Role
bulma = ["https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css"]
current_app.config['FLASK_ADMIN_EXTRA_CSS'] = bulma
admin = Admin(current_app, name='Admin', template_mode='bootstrap4')
class UserModelView(ModelView):
column_list = ['email','role']
create_modal = False
can_view_details = True
extra_css = ["https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css"]
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for('security.login', next=request.url))
def scaffold_form(self):
form_class = super(UserModelView, self).scaffold_form()
form_class.password2 = PasswordField('New Password')
return form_class
def on_model_change(self, form, model, is_created):
if len(model.password2):
model.password = utils.encrypt_password(model.password2)
def get_save_return_url(self, model, is_created):
return self.get_url('.details_view', id=model.id)
# define a context processor for merging flask-admin's template context into the
# flask-security views.
security = current_app.security
@security.context_processor
def security_context_processor():
def get_url(admin, **kwargs):
return url_for(admin, **kwargs)
return dict(
admin_base_template=admin.base_template,
admin_view=admin.index_view,
h=admin_helpers,
get_url=url_for
)
admin.add_view(UserModelView(User, db_session))

154
project/app.py Normal file
View File

@ -0,0 +1,154 @@
import os
import sys
import string
import random
import uuid
from flask import Flask, session, render_template_string, render_template, redirect, url_for, flash, request, current_app
from flask_security import Security, current_user, auth_required, hash_password, SQLAlchemySessionUserDatastore
from flask_login import login_user, logout_user
from flask_font_awesome import FontAwesome
from flask_dance.contrib.google import make_google_blueprint, google
from urllib.parse import urlencode
from flask_mailman import Mail
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.security import generate_password_hash
from .config import Config, ProductionConfig, DevelopmentConfig
from dotenv import load_dotenv
from .database import db_session, init_db
from .models import User, Role
from .views import views_bp
fa = FontAwesome()
# Create app
def create_app():
app = Flask(__name__, instance_relative_config=True)
app.config['MAX_CONTENT_LENGTH'] = 20 * 1024 * 1024 # Set the maximum allowed request size to 100MB
debug_mode = False
print("Creating app")
for i, arg in enumerate(sys.argv):
print("arg: " + str(arg))
if arg == "--debug":
print("debug mode activé")
app.config.from_object(DevelopmentConfig)
debug_mode = True
break
print("end of variable retrieval")
if not debug_mode:
app.config.from_object(ProductionConfig)
# Create Google OAuth blueprint
google_blueprint = make_google_blueprint(
client_id=app.config['GOOGLE_OAUTH_CLIENT_ID'],
client_secret=app.config['GOOGLE_OAUTH_CLIENT_SECRET'],
scope=["https://www.googleapis.com/auth/userinfo.email", "openid"],
redirect_url="/authorized",
)
# Register the Google OAuth blueprint
app.register_blueprint(google_blueprint, url_prefix="/login")
def google_login_url():
google_auth_url = url_for("google.login", _external=True)
params = {
"access_type": "offline",
"prompt": "consent",
}
return f"{google_auth_url}?{urlencode(params)}"
@app.context_processor
def inject_google_login_url():
return dict(google_login_url=google_login_url)
user_datastore = SQLAlchemySessionUserDatastore(db_session, User, Role)
print("user_datastore retrieved in prod")
app.security = Security(app, user_datastore)
print("app.security initialized in prod")
mail = Mail(app)
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
@app.route('/authorized')
def authorized():
if debug_mode:
return "Not available in development mode", 404
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.get("/oauth2/v3/tokeninfo")
if not resp.ok:
flash("Failed to authenticate with Google.", "danger")
return redirect(url_for("views.home"))
idinfo = resp.json()
email = idinfo['email']
if 'name' in idinfo:
name = idinfo['name']
else:
name = email
# Check if the user's email is in the allowed list
if email not in app.config['ALLOWED_USERS']:
flash("Your email is not authorized.", "danger")
return redirect(url_for("views.home"))
# Check if the user exists in the database
user = app.security.datastore.find_user(email=email)
# If the user does not exist, create a new user
if not user:
random_password = generate_random_password()
hashed_password = generate_password_hash(random_password, method='sha256')
#fs_uniquifier = str(uuid.uuid4())
user = app.security.datastore.create_user(email=email, username=name, password=hashed_password)
db_session.commit()
login_user(user)
# Set the session variable
session['logged_in_with_google'] = True
# Redirect to the desired page after login, e.g., the index page
return redirect(url_for("views.home"))
# Register the Google authorized route only in production
app.add_url_rule('/authorized', view_func=authorized)
def generate_random_password(length=12):
characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(random.choice(characters) for i in range(length))
@app.route("/logout")
@auth_required()
def logout():
logout_user()
return redirect(url_for("views.home"))
app.register_blueprint(views_bp)
with app.app_context():
# Create a user to test with
init_db()
for user in app.config['ALLOWED_USERS']:
if not (user.endswith("gmail.com") or user.endswith("thargo.io")) and not app.security.datastore.find_user(email=user):
app.security.datastore.create_user(email=user, password=hash_password(generate_random_password()))
db_session.commit()
#from admin import admin
#admin.init_app(app)
fa.init_app(app)
return app
app = create_app()
if __name__ == "__main__":
app.run()

45
project/config.py Normal file
View File

@ -0,0 +1,45 @@
import os
from dotenv import load_dotenv
import json
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(os.path.dirname(basedir), '.env'))
class Config():
DEBUG = False
TESTING = False
CSRF_ENABLED = True
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'None'
MAX_CONTENT_LENGTH = 100 * 1024 * 1024
MAIL_SERVER = os.environ.get("MAIL_SERVER")
MAIL_PORT = os.environ.get("MAIL_PORT")
MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS")
MAIL_USE_SSL = os.environ.get("MAIL_USE_SSL")
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER")
SECURITY_RECOVERABLE = True
SECURITY_EMAIL_SENDER = os.environ.get("MAIL_SENDER")
ALLOWED_USERS = json.loads(os.environ.get("ALLOWED_USERS"))
class ProductionConfig(Config):
DEBUG = False
FLASK_DEBUG = False
SECRET_KEY = os.environ.get("PRODUCTION_SECRET_KEY")
SECURITY_PASSWORD_SALT = os.environ.get("PRODUCTION_SECURITY_PASSWORD_SALT")
SQLALCHEMY_TRACK_MODIFICATIONS = False
GOOGLE_OAUTH_CLIENT_ID = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
class DevelopmentConfig(Config):
DEVELOPMENT = True
FLASK_DEBUG = True
DEBUG = True
OAUTHLIB_INSECURE_TRANSPORT = True
SECRET_KEY = os.environ.get("DEV_SECRET_KEY")
SECURITY_PASSWORD_SALT = os.environ.get("DEV_SECURITY_PASSWORD_SALT")
SQLALCHEMY_TRACK_MODIFICATIONS = True

25
project/database.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from flask import current_app
import os
engine = None
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
def init_db():
DATABASE_URL = current_app.config['SQLALCHEMY_DATABASE_URI']
print("inside init_db, database_url : " + str(DATABASE_URL))
engine = create_engine(DATABASE_URL, pool_size=10, max_overflow=20)
db_session.configure(bind=engine)
# import all modules here that might define models so that
# they will be registered properly on the metadata. Otherwise
# you will have to import them first before calling init_db()
from . import models
Base.metadata.create_all(bind=engine)

36
project/models.py Normal file
View File

@ -0,0 +1,36 @@
from .database import Base
from flask_security import UserMixin, RoleMixin
from sqlalchemy import create_engine
from sqlalchemy.orm import relationship, backref
from sqlalchemy import Boolean, DateTime, Column, Integer, \
String, ForeignKey, UnicodeText
class RolesUsers(Base):
__tablename__ = 'roles_users'
id = Column(Integer(), primary_key=True)
user_id = Column('user_id', Integer(), ForeignKey('user.id'))
role_id = Column('role_id', Integer(), ForeignKey('role.id'))
class Role(Base, RoleMixin):
__tablename__ = 'role'
id = Column(Integer(), primary_key=True)
name = Column(String(80), unique=True)
description = Column(String(255))
permissions = Column(UnicodeText)
class User(Base, UserMixin):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
email = Column(String(255), unique=True)
username = Column(String(255), unique=True, nullable=True)
password = Column(String(255), nullable=False)
last_login_at = Column(DateTime())
current_login_at = Column(DateTime())
last_login_ip = Column(String(100))
current_login_ip = Column(String(100))
login_count = Column(Integer)
active = Column(Boolean())
fs_uniquifier = Column(String(255), unique=True, nullable=False)
confirmed_at = Column(DateTime())
roles = relationship('Role', secondary='roles_users',
backref=backref('users', lazy='dynamic'))

View File

@ -0,0 +1,43 @@
{% import 'admin/static.html' as admin_static with context %}
{% macro dropdown(actions) -%}
<div class="dropdown is-active is-overlay">
<a class="dropdown-trigger" aria-controls="dropdown-menu" role="button" aria-haspopup="true"
aria-expanded="false">
<span>{{ _gettext('With selected') }}</span>
<span class="icon is-small">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
{% for p in actions %}
<a class="dropdown-item" href="javascript:void(0)"
onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
{% endfor %}
</div>
</div>
</div>
{% endmacro %}
{% macro form(actions, url) %}
{% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
{{ action_form.url(value=return_url) }}
{{ action_form.action() }}
</form>
{% endif %}
{% endmacro %}
{% macro script(message, actions, actions_confirmation) %}
{% if actions %}
<div id="actions-confirmation-data" style="display:none;">{{ actions_confirmation|tojson|safe }}</div>
<div id="message-data" style="display:none;">{{ message|tojson|safe }}</div>
<script src="{{ admin_static.url(filename='admin/js/actions.js', v='1.0.0') }}"></script>
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,9 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% block content %}
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
{% block fa_form %}
{{ lib.render_form(form, dir_url) }}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,191 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/actions.html' as actionslib with context %}
{% block content %}
{% block breadcrums %}
<nav area-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=None) }}">{{ _gettext('Root') }}</a>
</li>
{% for name, path in breadcrumbs[:-1] %}
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=path) }}">{{ name }}</a>
</li>
{% endfor %}
{% if breadcrumbs %}
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=breadcrumbs[-1][1]) }}">{{ breadcrumbs[-1][0] }}</a>
</li>
{% endif %}
</ol>
</nav>
{% endblock %}
{% block file_list_table %}
<div class="table-responsive">
<table class="table table-striped table-bordered model-list">
<thead>
<tr>
{% block list_header scoped %}
{% if actions %}
<th class="list-checkbox-column">
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" />
</th>
{% endif %}
<th class="">&nbsp;</th>
{% for column in admin_view.column_list %}
<th>
{% if admin_view.is_column_sortable(column) %}
{% if sort_column == column %}
<a href="{{ sort_url(column, dir_path, True) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">
{{ admin_view.column_label(column) }}
{% if sort_desc %}
<span class="fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
{% else %}
<span class="fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
{% endif %}
</a>
{% else %}
<a href="{{ sort_url(column, dir_path) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">{{ admin_view.column_label(column) }}</a>
{% endif %}
{% else %}
{{ _gettext(admin_view.column_label(column)) }}
{% endif %}
</th>
{% endfor %}
{% endblock %}
</tr>
</thead>
{% for name, path, is_dir, size, date in items %}
<tr>
{% block list_row scoped %}
{% if actions %}
<td>
{% if not is_dir %}
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ path }}" />
{% endif %}
</td>
{% endif %}
<td>
{% block list_row_actions scoped %}
{% if admin_view.can_rename and path and name != '..' %}
{%- if admin_view.rename_modal -%}
{{ lib.add_modal_button(url=get_url('.rename', path=path, modal=True),
title=_gettext('Rename File'),
content='<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>') }}
{% else %}
<a class="icon" href="{{ get_url('.rename', path=path) }}" title="{{ _gettext('Rename File') }}">
<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>
</a>
{%- endif -%}
{% endif %}
{%- if admin_view.can_delete and path -%}
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="fa fa-times glyphicon glyphicon-remove"></i>
</button>
</form>
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="fa fa-trash glyphicon glyphicon-trash"></i>
</button>
</form>
{% endif %}
{%- endif -%}
{% endblock %}
</td>
{% if is_dir %}
<td colspan="2">
<a href="{{ get_dir_url('.index_view', path)|safe }}">
<i class="fa fa-folder-o glyphicon glyphicon-folder-close"></i> <span>{{ name }}</span>
</a>
</td>
{% else %}
<td>
{% if admin_view.can_download %}
{%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%}
{{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe,
btn_class='', content=name) }}
{% else %}
<a href="{{ get_file_url(path)|safe }}">{{ name }}</a>
{%- endif -%}
{% else %}
{{ name }}
{% endif %}
</td>
{% if admin_view.is_column_visible('size') %}
<td>
{{ size|filesizeformat }}
</td>
{% endif %}
{% endif %}
{% if admin_view.is_column_visible('date') %}
<td>
{{ timestamp_format(date) }}
</td>
{% endif %}
{% endblock %}
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block toolbar %}
<div class="btn-toolbar">
{% if admin_view.can_upload %}
<div class="btn-group">
{%- if admin_view.upload_modal -%}
{{ lib.add_modal_button(url=get_dir_url('.upload', path=dir_path, modal=True),
btn_class="btn btn-secondary",
content=_gettext('Upload File')) }}
{% else %}
<a class="btn btn-secondary" href="{{ get_dir_url('.upload', path=dir_path) }}">{{ _gettext('Upload File') }}</a>
{%- endif -%}
</div>
{% endif %}
{% if admin_view.can_mkdir %}
<div class="mx-1">
{%- if admin_view.mkdir_modal -%}
{{ lib.add_modal_button(url=get_dir_url('.mkdir', path=dir_path, modal=True),
btn_class="btn btn-secondary",
content=_gettext('Create Directory')) }}
{% else %}
<a class="btn btn-secondary" href="{{ get_dir_url('.mkdir', path=dir_path) }}">{{ _gettext('Create Directory') }}</a>
{%- endif -%}
</div>
{% endif %}
{% if actions %}
<div class="mx-1">
{{ actionslib.dropdown(actions, 'dropdown-toggle btn btn-secondary') }}
</div>
{% endif %}
</div>
{% endblock %}
{% block actions %}
{{ actionslib.form(actions, get_url('.action_view')) }}
{% endblock %}
{%- if admin_view.rename_modal or admin_view.mkdir_modal
or admin_view.upload_modal or admin_view.edit_modal -%}
{{ lib.add_modal_window() }}
{%- endif -%}
{% endblock %}
{% block tail %}
{{ super() }}
{{ actionslib.script(_gettext('Please select at least one file.'),
actions,
actions_confirmation) }}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% import 'admin/static.html' as admin_static with context %}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
{# content added to modal-content #}
<div class="modal-header">
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% block fa_form %}
{{ lib.render_form(form, dir_url, action=request.url, is_modal=True) }}
{% endblock %}
</div>
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'admin/master.html' %}
{% block content %}
Bonjour
{% endblock %}

View File

@ -0,0 +1,294 @@
{% import 'admin/static.html' as admin_static with context %}
{# ---------------------- Pager -------------------------- #}
{% macro pager(page, pages, generator) -%}
{% if pages > 1 %}
<ul class="pagination">
{% set min = page - 3 %}
{% set max = page + 3 + 1 %}
{% if min < 0 %}
{% set max = max - min %}
{% endif %}
{% if max >= pages %}
{% set min = min - max + pages %}
{% endif %}
{% if min < 0 %}
{% set min = 0 %}
{% endif %}
{% if max >= pages %}
{% set max = pages %}
{% endif %}
{% if min > 0 %}
<li class="page-item">
<a class="page-link" href="{{ generator(0) }}">&laquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&laquo;</a>
</li>
{% endif %}
{% if page > 0 %}
<li class="page-item">
<a class="page-link" href="{{ generator(page-1) }}">&lt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&lt;</a>
</li>
{% endif %}
{% for p in range(min, max) %}
{% if page == p %}
<li class="page-item active">
<a class="page-link" href="javascript:void(0)">{{ p + 1 }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ generator(p) }}">{{ p + 1 }}</a>
</li>
{% endif %}
{% endfor %}
{% if page + 1 < pages %}
<li class="page-item">
<a class="page-link" href="{{ generator(page + 1) }}">&gt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&gt;</a>
</li>
{% endif %}
{% if max < pages %}
<li class="page-item">
<a class="page-link" href="{{ generator(pages - 1) }}">&raquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&raquo;</a>
</li>
{% endif %}
</ul>
{% endif %}
{%- endmacro %}
{% macro simple_pager(page, have_next, generator) -%}
<ul class="pagination">
{% if page > 0 %}
<li class="page-item">
<a href="{{ generator(page - 1) }}">&lt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="{{ generator(0) }}">&lt;</a>
</li>
{% endif %}
{% if have_next %}
<li class="page-item">
<a href="{{ generator(page + 1) }}">&gt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="{{ generator(page) }}">&gt;</a>
</li>
{% endif %}
</ul>
{%- endmacro %}
{# ---------------------- Modal Window ------------------- #}
{% macro add_modal_window(modal_window_id='fa_modal_window', modal_label_id='fa_modal_label') %}
<div class="modal fade" id="{{ modal_window_id }}" tabindex="-1" role="dialog" aria-labelledby="{{ modal_label_id }}">
<div class="modal-dialog modal-xl" role="document">
{# bootstrap version > 3.1.0 required for this to work #}
<div class="modal-content">
</div>
</div>
</div>
{% endmacro %}
{% macro add_modal_button(url='', title='', content='', modal_window_id='fa_modal_window', btn_class='icon') %}
<a class="{{ btn_class }}" data-target="#{{ modal_window_id }}" title="{{ title }}" href="{{ url }}" data-toggle="modal">
{{ content|safe }}
</a>
{% endmacro %}
{# ---------------------- Forms -------------------------- #}
{% macro render_field(form, field, kwargs={}, caller=None) %}
{% set direct_error = h.is_field_error(field.errors) %}
{% set prepend = kwargs.pop('prepend', None) %}
{% set append = kwargs.pop('append', None) %}
<div class="form-group {{ kwargs.get('column_class', '') }}">
<label for="{{ field.id }}" class="control-label" {% if field.widget.input_type == 'checkbox' %}style="display: block; margin-bottom: 0"{% endif %}>{{ field.label.text }}
{% if h.is_required_form_field(field) %}
<strong style="color: red">&#42;</strong>
{%- else -%}
&nbsp;
{%- endif %}
</label>
{% if prepend or append %}
<div class="input-group">
{%- if prepend -%}
<div class="input-group-prepend">
{{ prepend }}
</div>
{%- endif -%}
{% endif %}
{% if field.widget.input_type == 'checkbox' %}
{% set _class = kwargs.setdefault('class', '') %}
{% elif field.widget.input_type == 'file' %}
{% set _class = kwargs.setdefault('class', 'form-control-file') %}
{% else %}
{% set _class = kwargs.setdefault('class', 'form-control') %}
{% endif %}
{%- if direct_error %} {% set _ = kwargs.update({'class': kwargs['class'] ~ ' is-invalid'}) %} {% endif -%}
{{ field(**kwargs) | safe }}
{%- if append -%}
<div class="input-group-append">
{{ append }}
</div>
{%- endif -%}
{% if direct_error %}
<div class="invalid-feedback">
<ul class="form-text text-muted" {% if field.widget.input_type == 'checkbox' %}style="margin-top: 0"{% endif %}>
{% for e in field.errors if e is string %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% elif field.description %}
<small class="form-text text-muted" {% if field.widget.input_type == 'checkbox' %}style="margin-top: 0"{% endif %}>
{{ field.description|safe }}
</small>
{% endif %}
{% if prepend or append %}
</div>
{% endif %}
{% if caller %}
{{ caller(form, field, direct_error, kwargs) }}
{% endif %}
</div>
{% endmacro %}
{% macro render_header(form, text) %}
<h3>{{ text }}</h3>
{% endmacro %}
{% macro render_form_fields(form, form_opts=None) %}
{% if form.hidden_tag is defined %}
{{ form.hidden_tag() }}
{% else %}
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
{% for f in form if f.widget.input_type == 'hidden' %}
{{ f }}
{% endfor %}
{% endif %}
{% if form_opts and form_opts.form_rules %}
{% for r in form_opts.form_rules %}
{{ r(form, form_opts=form_opts) }}
{% endfor %}
{% else %}
{% for f in form if f.widget.input_type != 'hidden' %}
{% if form_opts %}
{% set kwargs = form_opts.widget_args.get(f.short_name, {}) %}
{% else %}
{% set kwargs = {} %}
{% endif %}
{{ render_field(form, f, kwargs) }}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro form_tag(form=None, action=None) %}
<form action="{{ action or '' }}" method="POST" role="form" class="admin-form" enctype="multipart/form-data">
<fieldset>
{{ caller() }}
</fieldset>
</form>
{% endmacro %}
{% macro render_form_buttons(cancel_url, extra=None, is_modal=False) %}
{% if is_modal %}
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
{% else %}
<hr>
<div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
{% call form_tag(action=action) %}
{{ render_form_fields(form, form_opts=form_opts) }}
{{ render_form_buttons(cancel_url, extra, is_modal) }}
{% endcall %}
{% endmacro %}
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css', v='4.2.1') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/select2/select2-bootstrap4.css', v='1.4.6') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs4.css', v='1.3.22') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css', v='1.0.2') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css', v='0.4.6') }}" rel="stylesheet">
{% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap4-editable.css', v='1.5.1.1') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
{% if config.MAPBOX_ACCESS_TOKEN %}
window.MAPBOX_ACCESS_TOKEN = "{{ config.MAPBOX_ACCESS_TOKEN }}";
{% endif %}
{% if config.DEFAULT_CENTER_LAT and config.DEFAULT_CENTER_LONG %}
window.DEFAULT_CENTER_LAT = "{{ config.DEFAULT_CENTER_LAT }}";
window.DEFAULT_CENTER_LONG = "{{ config.DEFAULT_CENTER_LONG }}";
{% endif %}
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js', v='1.0.2') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js', v='0.4.6') }}"></script>
{% if config.MAPBOX_SEARCH %}
<script>
window.MAPBOX_SEARCH = "{{ config.MAPBOX_SEARCH }}";
</script>
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key={{ config.get('GOOGLE_MAPS_API_KEY') }}"></script>
{% endif %}
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap4-editable.min.js', v='1.5.1.1') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }}"></script>
{% endmacro %}
{% macro extra() %}
{% if admin_view.can_create %}
<input name="_add_another" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Add Another') }}" />
{% endif %}
{% if admin_view.can_edit %}
<input name="_continue_editing" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Continue Editing') }}" />
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,72 @@
{% import 'admin/layout.html' as layout with context -%}
{% import 'admin/static.html' as admin_static with context %}
{% extends "/base.html" %}
{% block head_scripts %}
{% block head_admin_scripts %}
{% endblock %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/swatch/{swatch}/bootstrap.min.css'.format(swatch=config.get('FLASK_ADMIN_SWATCH', 'default')), v='4.2.1') }}"
rel="stylesheet">
{% if config.get('FLASK_ADMIN_SWATCH', 'default') == 'default' %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/bootstrap.min.css', v='4.2.1') }}" rel="stylesheet">
{% endif %}
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/admin.css', v='1.1.1') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/font-awesome.min.css', v='4.7.0') }}" rel="stylesheet">
{% if admin_view.extra_css %}
{% for css_url in admin_view.extra_css %}
<link href="{{ css_url }}" rel="stylesheet">
{% endfor %}
{% endif %}
{{ super() }}
{% endblock %}
{%- block styles %}
{{ super() }}
{% endblock %}
{% block page_body %}
{% block brand %}
{% endblock %}
{% block main_menu %}
{% endblock %}
{% block menu_links %}
{% endblock %}
{% block access_control %}
{% endblock %}
{% block messages %}
{% endblock %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% endblock %}
{% block scripts %}
{{ super() }}
{% block tail_js %}
<script src="{{ admin_static.url(filename='vendor/jquery.min.js', v='3.5.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/popper.min.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/bootstrap.min.js', v='4.2.1') }}"
type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/bootstrap4/util.js', v='4.3.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/bootstrap4/dropdown.js', v='4.3.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='4.2.1') }}"
type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/multi-level-dropdowns-bootstrap/bootstrap4-dropdown-ml-hack.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='admin/js/helpers.js', v='1.0.0') }}" type="text/javascript"></script>
{% if admin_view.extra_js %}
{% for js_url in admin_view.extra_js %}
<script src="{{ js_url }}" type="text/javascript"></script>
{% endfor %}
{% endif %}
{% endblock tail_js %}
{% block tail %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block navlinks %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('Create') }}</a>
</li>
</ul>
</div>
{% endblock %}
{% block create_form %}
{{ lib.render_form(form, return_url, extra(), form_opts) }}
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% block content %}
{% block navlinks %}
<div class="tabs">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
{%- if admin_view.can_create -%}
<li>
<a href="{{ get_url('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
</li>
{%- endif -%}
{%- if admin_view.can_edit -%}
<li>
<a href="{{ get_url('.edit_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Edit') }}</a>
</li>
{%- endif -%}
<li>
<a class="is-active disabled" href="javascript:void(0)">{{ _gettext('Details') }}</a>
</li>
</ul>
</div>
{% endblock %}
{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_table %}
<table class="table table-hover table-bordered searchable">
{% for c, name in details_columns %}
<tr>
<td>
<b>{{ name }}</b>
</td>
<td>
{{ get_value(model, c) }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block navlinks %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
{%- if admin_view.can_create -%}
<li>
<a href="{{ get_url('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
</li>
{%- endif -%}
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('Edit') }}</a>
</li>
{%- if admin_view.can_view_details -%}
<li>
<a href="{{ get_url('.details_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Details') }}</a>
</li>
{%- endif -%}
</ul>
</div>
{% endblock %}
{% block edit_form %}
{{ lib.render_form(form, return_url, extra(), form_opts) }}
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
{% endblock %}

View File

@ -0,0 +1,212 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/model/layout.html' as model_layout with context %}
{% import 'admin/actions.html' as actionlib with context %}
{% import 'admin/model/row_actions.html' as row_actions with context %}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block model_menu_bar %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
</li>
{% if admin_view.can_create %}
<li>
{%- if admin_view.create_modal -%}
{{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), btn_class='nav-link', title=_gettext('Create New Record'), content=_gettext('Create')) }}
{% else %}
<a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create New Record') }}" class="nav-link">{{ _gettext('Create') }}</a>
{%- endif -%}
</li>
{% endif %}
{% if admin_view.can_export %}
{{ model_layout.export_options() }}
{% endif %}
{% block model_menu_bar_before_filters %}{% endblock %}
{% if filters %}
<li class="dropdown">
{{ model_layout.filter_options() }}
</li>
{% endif %}
{% if can_set_page_size %}
<li class="dropdown">
{{ model_layout.page_size_form(page_size_url) }}
</li>
{% endif %}
{% if actions %}
<li>
{{ actionlib.dropdown(actions) }}
</li>
{% endif %}
{% if search_supported %}
<li class="box">
{{ model_layout.search_form() }}
</li>
{% endif %}
{% block model_menu_bar_after_filters %}{% endblock %}
</ul>
</div>
{% endblock %}
{% if filters %}
{{ model_layout.filter_form() }}
<div class=is-clearfix"></div>
{% endif %}
{% block model_list_table %}
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable is-fullwidth model-list">
<thead>
<tr>
{% block list_header scoped %}
{% if actions %}
<th class="has-text-centered">
<input type="checkbox" name="rowtoggle" title="{{ _gettext('Select all records') }}" />
</th>
{% endif %}
{% block list_row_actions_header %}
{% if admin_view.column_display_actions %}
<th class="has-text-centered">&nbsp;</th>
{% endif %}
{% endblock %}
{% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="has-text-centered">
{% if admin_view.is_sortable(c) %}
{% if sort_column == column %}
<a href="{{ sort_url(column, True) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">
{{ name }}
{% if sort_desc %}
<span class="icon fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
{% else %}
<span class="icon fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
{% endif %}
</a>
{% else %}
<a href="{{ sort_url(column) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">{{ name }}</a>
{% endif %}
{% else %}
{{ name }}
{% endif %}
{% if admin_view.column_descriptions.get(c) %}
<a class="icon fa fa-question-circle glyphicon glyphicon-question-sign"
title="{{ admin_view.column_descriptions[c] }}"
href="javascript:void(0)" data-role="tooltip"
></a>
{% endif %}
</th>
{% endfor %}
{% endblock %}
</tr>
</thead>
{% for row in data %}
<tr>
{% block list_row scoped %}
{% if actions %}
<td>
<input type="checkbox" name="rowid" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
</td>
{% endif %}
{% block list_row_actions_column scoped %}
{% if admin_view.column_display_actions %}
<td>
{% block list_row_actions scoped %}
{% for action in list_row_actions %}
{{ action.render_ctx(get_pk_value(row), row) }}
{% endfor %}
{% endblock %}
</td>
{%- endif -%}
{% endblock %}
{% for c, name in list_columns %}
<td>
{% if admin_view.is_editable(c) %}
{% set form = list_forms[get_pk_value(row)] %}
{% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% elif csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
{% else %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %}
{% else %}
{{ get_value(row, c) }}
{% endif %}
</td>
{% endfor %}
{% endblock %}
</tr>
{% else %}
<tr>
<td>
{% block empty_list_message %}
<div>
{{ admin_view.get_empty_list_message() }}
</div>
{% endblock %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% block list_pager %}
{% if num_pages is not none %}
{{ lib.pager(page, num_pages, pager_url) }}
{% else %}
{{ lib.simple_pager(page, data|length == page_size, pager_url) }}
{% endif %}
{% endblock %}
{% endblock %}
{% block actions %}
{{ actionlib.form(actions, get_url('.action_view')) }}
{% endblock %}
{%- if admin_view.edit_modal or admin_view.create_modal or admin_view.details_modal -%}
{{ lib.add_modal_window() }}
{%- endif -%}
{% endblock %}
{% block tail %}
{{ super() }}
{% if filter_groups %}
<div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %}
{{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/bs4_filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
actions_confirmation) }}
<script>
document.addEventListener('DOMContentLoaded', function () {
var dropdown = document.querySelector('.dropdown');
dropdown.addEventListener('click', function(event) {
event.stopPropagation();
dropdown.classList.toggle('is-active');
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% block body %}
<div class="modal-header">
{% block header_text %}<h5 class="modal-title">{{ _gettext('Create New Record') }}</h5>{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
<div class="modal-body">
{{ lib.render_form_fields(form, form_opts=form_opts) }}
</div>
<div class="modal-footer">
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
</div>
{% endcall %}
{# "save and add" button is removed from modal (it won't function properly) #}
{# % block create_form %}
{{ lib.render_form(form, return_url, extra=None, form_opts=form_opts,
action=url_for('.create_view', url=return_url),
is_modal=True) }}
{% endblock % #}
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
<div class="modal-header">
{% block header_text %}
<h3>{{ _gettext('View Record') + ' #' + request.args.get('id') }}</h3>
{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_table %}
<table class="table table-hover table-bordered searchable">
{% for c, name in details_columns %}
<tr>
<td>
<b>{{ name }}</b>
</td>
<td>
{{ get_value(model, c) }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
</div>
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% block body %}
<div class="modal-header">
{% block header_text %}
<h5 class="modal-title">{{ _gettext('Edit Record') + ' #' + request.args.get('id') }}</h5>
{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% call lib.form_tag(action=url_for('.edit_view', id=request.args.get('id'), url=return_url)) %}
<div class="modal-body">
{{ lib.render_form_fields(form, form_opts=form_opts) }}
</div>
<div class="modal-footer">
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
</div>
{% endcall %}
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/static.html' as admin_static with context%}
{%- block head_scripts %}
{{ super() }}
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/rediscli.css', v='1.0.0') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="console">
<div class="console-container">
</div>
<div class="console-line mb-4">
<form action="#">
<input type="text"></input>
</form>
</div>
</div>
{% endblock %}
{% block tail %}
{{ super() }}
<div id="execute-view-data" style="display:none;">{{ admin_view.get_url('.execute_view')|tojson|safe }}</div>
<script src="{{ admin_static.url(filename='admin/js/rediscli.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends "security/base.html" %}
{% block metas %}
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
{% endblock %}
{%- block head_scripts %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css" integrity="sha512-SI0aF82pT58nyOjCNfyeE2Y5/KHId8cLIX/1VYzdjTRs0HPNswsJR+aLQYSWpb88GDJieAgR4g1XWZvUROQv1A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{%- endblock head_scripts %}
{%- block styles %}
<style>
.hide { display: none;}
.fs-center { text-align: center }
.fs-important { font-size: larger; font-weight: bold }
.fs-gap { margin-top: 20px; }
.fs-div { margin: 4px; }
.fs-error-msg { color: darkred; }
</style>
{%- endblock %}
{% block body %}
<section class="hero is-primary is-fullheight">
<div class="hero-head">
{% block navbar %}
{% include "partials/navbar.html" %}
{% endblock navbar %}
</div>
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
{% block content -%}
{%- endblock content %}
</div>
</div>
</div>
<div class="hero-foot">
{% block footer -%}
{%- endblock footer %}
</div>
</section>
{% block scripts %}
{%- endblock scripts %}
{% endblock body %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<div class="column is-half">
<h1 class="title">
Flask Login Example
</h1>
<h2 class="subtitle">
Easy authentication and authorization in Flask.
</h2>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% block navbar %}
<nav class="navbar" role="navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item">
<img src="https://bulma.io/images/bulma-type-white.png" alt="Logo">
</a>
<span class="navbar-burger" data-target="navbarMenuHeroA">
<span></span>
<span></span>
<span></span>
</span>
</div>
<div id="navbarMenuHeroA" class="navbar-menu">
<div class="navbar-end">
<a href="{{ url_for('views.home') }}" class="navbar-item">
Home
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('views.profile') }}" class="navbar-item">
Profile
</a>
<a href="{{ url_for('security.logout') }}" class="navbar-item">
Logout
</a>
{% endif %}
{% if not current_user.is_authenticated %}
<a href="{{ url_for('security.login') }}" class="navbar-item">
Login
</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="hero-body">
<div class="container has-text-centered">
<h1>Hello {{name}}</h1>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% macro render_field_with_errors(field) %}
<div class="field" id="{{ field.id|default('fs-field') }}">
<div class="control">
{% set placeholder = field.label.text %}
{{ field(placeholder=placeholder,**kwargs)|safe }}
</div>
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{% macro render_field(field) %}
<div class="fs-div field" id="{{ field.id|default('fs-field') }}">
{% if field.label.text == "Login" %}
{{ field(**kwargs)|safe }}
{% else %}
{{ field.label }} {{ field(**kwargs)|safe }}
{% endif %}
</div>
{% endmacro %}
{% macro render_field_errors(field) %}
<div class="fs-div" id="{{ field.id if field else 'fs-field' }}">
{% if field and field.errors %}
<ul>
{% for error in field.errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{# render WTForms (>3.0) form level errors #}
{% macro render_form_errors(form) %}
{% if form.form_errors %}
<div class="fs-div field" id="fs-form-errors">
<ul>
{% for error in form.form_errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endmacro %}
{% macro prop_next() -%}
{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}
{%- endmacro %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block content %}
{% include "security/_messages.html" %}
<h1>{{ _fsdomain('Send password reset instructions') }}</h1>
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field(forgot_password_form.submit, class="button") }}
</form>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors, prop_next %}
{% block content %}
<div class="column is-one-third">
{% include "security/_messages.html" %}
<h1 class="title">{{ _fsdomain('Login') }}</h1>
<div class="block">
<form action="{{ url_for_security('login') }}{{ prop_next() }}" method="POST" name="login_user_form">
<div class="box">
{{ login_user_form.hidden_tag() }}
{{ render_form_errors(login_user_form) }}
{% if "email" in identity_attributes %}
{{ render_field_with_errors(login_user_form.email) }}
{% endif %}
{% if login_user_form.username and "username" in identity_attributes %}
{% if "email" in identity_attributes %}
<h3>{{ _fsdomain("or") }}</h3>
{% endif %}
{{ render_field_with_errors(login_user_form.username) }}
{% endif %}
{{ render_field_with_errors(login_user_form.password) }}
</div>
{{ render_field(login_user_form.remember) }}
{{ render_field_errors(login_user_form.csrf_token) }}
{{ render_field(login_user_form.submit, class="button is-rounded is-primary", label="bonjour") }}
</form>
{% if security.webauthn %}
<hr class="fs-gap">
<h2>{{ _fsdomain("Use WebAuthn to Sign In") }}</h2>
<div>
<form method="GET" id="wan-signin-form" name="wan_signin_form">
<input id="wan_signin" name="wan_signin" type="submit" value="{{ _fsdomain('Sign in with WebAuthn') }}"
formaction="{{ url_for_security('wan_signin') }}{{ prop_next() }}">
</form>
</div>
{% endif %}
{% if security.oauthglue %}
<hr class="fs-gap">
<h2>{{ _fsdomain("Use Social Oauth to Sign In") }}</h2>
{% for provider in security.oauthglue.provider_names %}
<div class="fs-gap">
<form method="POST" id={{ provider }}-form name={{ provider }}_form>
<input id={{ provider }} name={{ provider }} type="submit" value="{{ _fsdomain('Sign in with ')~provider }}"
formaction="{{ url_for_security('oauthstart', name=provider) }}{{ prop_next() }}">
</form>
</div>
{% endfor %}
{% endif %}
{% if google_login_url is defined %}
<a class="button is-rounded is-primary" href="{{ google_login_url() }}" class="button is-primary">
<span class="icon">
<i class="fab fa-google"></i>
</span>
<span>Sign in with Google</span>
</a>
{% endif %}
{% if security.recoverable %}
<a class="button is-rounded is-primary" href="{{ url_for_security('forgot_password') }}">{{ _fsdomain('Forgot password') }}</a><br/>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "security/base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %}
{% block content %}
{% include "security/_messages.html" %}
<h1>{{ _fsdomain('Register') }}</h1>
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_form_errors(register_user_form) }}
{{ render_field_with_errors(register_user_form.email) }}
{% if security.username_enable %}
{{ render_field_with_errors(register_user_form.username) }}
{% endif %}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field(register_user_form.submit) }}
</form>
{% include "security/_menu.html" %}
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block content %}
<h2>User Management</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-primary btn-sm">Edit</a>
{% if not user.is_admin %}
{% if user.is_active %}
<a href="{{ url_for('block_user', user_id=user.id) }}" class="btn btn-warning btn-sm">Block</a>
{% else %}
<a href="{{ url_for('unblock_user', user_id=user.id) }}" class="btn btn-success btn-sm">Unblock</a>
{% endif %}
<a href="{{ url_for('delete_user', user_id=user.id) }}" class="btn btn-danger btn-sm">Delete</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col-md-6">
<h3>Create User</h3>
<form method="POST" action="{{ url_for('create_user') }}">
{{ form.csrf_token }}
<div class="form-group">
<label for="username">Username</label>
{{ form.username(class="form-control", placeholder="Enter username") }}
</div>
<div class="form-group">
<label for="email">Email</label>
{{ form.email(class="form-control", placeholder="Enter email address") }}
</div>
<div class="form-group">
<label for="password">Password</label>
{{ form.password(class="form-control", placeholder="Enter password") }}
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
<div class="col-md-6">
<h3>Change Password</h3>
<form method="POST" action="{{ url_for('change_password') }}">
{{ form.csrf_token }}
<div class="form-group">
<label for="email">Email</label>
{{ form.email(class="form-control", placeholder="Enter email address") }}
</div>
<div class="form-group">
<label for="password">New Password</label>
{{ form.password(class="form-control", placeholder="Enter new password") }}
</div>
<button type="submit" class="btn btn-primary">Change</button>
</form>
</div>
</div>
{% endblock %}

37
project/views.py Normal file
View File

@ -0,0 +1,37 @@
# Views
from flask import Blueprint, render_template, redirect, url_for
from flask_security import current_user, auth_required
from .models import User, Role
from .database import db_session
views_bp = Blueprint('views', __name__)
@views_bp.route('/')
def home():
return render_template('index.html')
@views_bp.route('/profile')
@auth_required()
def profile():
if not current_user.is_authenticated:
return redirect(url_for('security.login'))
else:
return render_template('profile.html', name=current_user.email)
''' @app.route('/users', endpoint='users', methods=['GET', 'POST'])
@auth_required
def users():
form = CreateUserForm()
if form.validate_on_submit():
user = User(email=form.email.data, password=form.password.data)
db.session.add(user)
db.session.commit()
flash('User created successfully!', 'success')
return redirect(url_for('users'))
users = User.query.all()
return render_template('users.html', users=users, form=form) '''

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
Flask-Security-Too[common]==5.1.0
SQLAlchemy==2.0.4
python-dotenv
Flask-Admin==1.6.1
Flask-WTF==1.1.1
Font-Awesome-Flask==0.1.1
Flask-Dance==6.2.0
google-auth==2.17.0
google-auth-httplib2==0.1.0
google-auth-oauthlib==1.0.0
gunicorn==20.1.0
oauth2client==4.1.3