-
-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds flask admin as a feature. Bumps flask admin to 1.6.0 #551
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,7 @@ FLASK_DEBUG=1 | |
ALGOLIA_APP_ID=search_id | ||
ALGOLIA_API_KEY=search_key | ||
INDEX_NAME=resources_api | ||
SECRET_KEY=sammy | ||
SECURITY_PASSWORD_SALT=saltedpop | ||
[email protected] | ||
ADMIN_PASSWORD=1234 | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,8 @@ RUN apt-get update \ | |
&& pip install poetry \ | ||
&& poetry config virtualenvs.create false | ||
|
||
RUN poetry lock | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I manually added the dependency versions to the pyproject.toml file. Below link to solution for resolving the "....which doesn't match any versions, version solving failed." error [Known issue]python-poetry/poetry#1281 (comment) |
||
|
||
RUN poetry install --no-dev --no-interaction --no-ansi | ||
|
||
COPY . /src | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from flask_admin import Admin, AdminIndexView | ||
from flask_admin.contrib.sqla import ModelView | ||
from flask import url_for, redirect, request, abort | ||
from app import db | ||
from .models import Resource, Category, Language, User, Role | ||
from flask_security import current_user | ||
|
||
|
||
class AdminView(ModelView): | ||
def is_accessible(self): | ||
return (current_user.is_active and | ||
current_user.is_authenticated and current_user.has_role('admin')) | ||
|
||
def _handle_view(self, name, **kwargs): | ||
""" Override builtin _handle_view in order to redirect users when a view | ||
is not accessible. | ||
""" | ||
if not self.is_accessible(): | ||
if current_user.is_authenticated: | ||
# permission denied | ||
abort(403) | ||
else: | ||
# login | ||
return redirect(url_for('security.login', next=request.url)) | ||
|
||
|
||
class HomeAdminView(AdminIndexView): | ||
def is_accessible(self): | ||
return current_user.has_role('admin') | ||
|
||
def inaccessible_callback(self, name): | ||
return redirect(url_for('security.login', next=request.url)) | ||
|
||
|
||
def run_flask_admin(app): | ||
"""Creates the admin object and defines which views will be visible""" | ||
admin_obj = Admin(app, name='Resources_api', url='/', | ||
base_template='my_master.html', | ||
index_view=HomeAdminView(name='Home')) | ||
admin_obj.add_view(AdminView(Role, db.session)) | ||
admin_obj.add_view(AdminView(User, db.session)) | ||
admin_obj.add_view(AdminView(Resource, db.session)) | ||
admin_obj.add_view(AdminView(Category, db.session)) | ||
admin_obj.add_view(AdminView(Language, db.session)) | ||
return admin_obj |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{% extends 'admin/master.html' %} | ||
|
||
{% block body %} | ||
{{ super() }} | ||
<div class='container'> | ||
<div> | ||
<div> | ||
<h1>Welcome!</h1> | ||
{% if not current_user.is_authenticated %} | ||
<h3>Please log in to continue</h3> | ||
<br> | ||
<a class='btn btn-primary' href="{{ url_for('security.login') }}">login</a> | ||
</p> | ||
<br> | ||
{% endif %} | ||
{% if current_user.is_authenticated %} | ||
<h3>You have successfully logged in.</h3> | ||
<p>You now have access to the administrator view.</p> | ||
<a href="{{ url_for('security.logout') }}">Log out</a> | ||
{% endif %} | ||
</div> | ||
</div> | ||
</div> | ||
{% endblock body %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{% extends 'admin/base.html' %} | ||
|
||
{% block access_control %} | ||
{% if current_user.is_authenticated==True %} | ||
<div class='navbar-text btn-group pull-right'> | ||
<a data-toggle='dropdown' role='button'> | ||
<i class="glyphicon glyphicon-user"></i> | ||
{{ current_user.email }} | ||
<span class="caret"></span> | ||
</a> | ||
<ul class="dropdown-menu"> | ||
<li><a href="{{ url_for('security.logout') }}">Log out</a></li> | ||
</ul> | ||
</div> | ||
{% endif %} | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{% extends 'admin/master.html' %} | ||
{% from "security/_macros.html" import render_field_with_errors, render_field %} | ||
|
||
{% block body %} | ||
{{ super() }} | ||
<div class="container"> | ||
<div> | ||
<h1>Login</h1> | ||
<div class="well"> | ||
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> | ||
{{ login_user_form.hidden_tag() }} | ||
{{ render_field_with_errors(login_user_form.email) }} | ||
{{ render_field_with_errors(login_user_form.password) }} | ||
{{ render_field(login_user_form.next) }} | ||
{{ render_field(login_user_form.submit, class="btn btn-primary") }} | ||
</form> | ||
</div> | ||
</div> | ||
</div> | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{% extends 'admin/master.html' %} | ||
{% from "security/_macros.html" import render_field_with_errors, render_field %} | ||
|
||
{% block body %} | ||
{{ super() }} | ||
<div class="container"> | ||
<div> | ||
<h1>Register</h1> | ||
<div class="well"> | ||
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form"> | ||
{{ register_user_form.hidden_tag() }} | ||
{{ render_field_with_errors(register_user_form.email) }} | ||
{{ 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, class="btn btn-primary") }} | ||
</form> | ||
<p>Already signed up? Please <a href="{{ url_for('security.login') }}">log in</a>.</p> | ||
</div> | ||
</div> | ||
</div> | ||
{% endblock body %} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -37,6 +37,13 @@ def get_sys_exec_root_or_drive(): | |||||
if not all([algolia_app_id, algolia_api_key]): | ||||||
print("Application requires 'ALGOLIA_APP_ID' and 'ALGOLIA_API_KEY' for search") | ||||||
|
||||||
secret_key = os.environ.get('SECRET_KEY', None) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uses bracket notation instead of .get() |
||||||
security_password_hash = 'pbkdf2_sha512' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this an environment variable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SECURITY_PASSWORD_HASH added to .env |
||||||
security_password_salt = os.environ.get('SECURITY_PASSWORD_SALT', None) | ||||||
|
||||||
if not all([secret_key, security_password_salt]): | ||||||
print('Application requires "SECRET_KEY" and "SECURITY_HASH"') | ||||||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can remove this if we use the bracket syntax instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uses bracket notation instead of .get() |
||||||
|
||||||
index_name = os.environ.get("INDEX_NAME") | ||||||
|
||||||
|
||||||
|
@@ -49,6 +56,16 @@ class Config: | |||||
ALGOLIA_API_KEY = algolia_api_key | ||||||
INDEX_NAME = index_name | ||||||
|
||||||
SECRET_KEY = secret_key | ||||||
SECURITY_URL_PREFIX = "/admin" | ||||||
SECURITY_PASSWORD_HASH = security_password_hash | ||||||
SECURITY_PASSWORD_SALT = security_password_salt | ||||||
SECURITY_LOGIN_URL = "/login/" | ||||||
SECURITY_LOGOUT_URL = "/logout/" | ||||||
SECURITY_POST_LOGIN_VIEW = "/admin/" | ||||||
SECURITY_POST_LOGOUT_VIEW = "/admin/" | ||||||
SECURITY_REGISTERABLE = False | ||||||
SECURITY_SEND_REGISTER_EMAIL = False | ||||||
# Can pass in changes to defaults, such as PaginatorConfig(per_page=40) | ||||||
RESOURCE_PAGINATOR = PaginatorConfig() | ||||||
LANGUAGE_PAGINATOR = PaginatorConfig() | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,18 @@ | ||
from app import app, cli | ||
from app.models import Category, Language, Resource, db | ||
from app.admin import run_flask_admin | ||
from app.models import Category, Language, Resource, db, Role, User | ||
import os | ||
from flask_security import Security, SQLAlchemyUserDatastore, utils | ||
from flask import url_for | ||
from flask_admin import helpers as admin_helpers | ||
from werkzeug.middleware.dispatcher import DispatcherMiddleware | ||
from prometheus_client import make_wsgi_app | ||
from sqlalchemy import event | ||
|
||
admin = run_flask_admin(app) | ||
|
||
user_datastore = SQLAlchemyUserDatastore(db, User, Role) | ||
security = Security(app, user_datastore) | ||
|
||
if __name__ == "__main__": | ||
app.run() | ||
|
@@ -15,6 +25,49 @@ | |
}) | ||
|
||
|
||
# @event.listens_for(User.password, 'set', retval=True) | ||
# def hash_user_password(target, value, oldvalue, initiator): | ||
# """Encrypts password when new admin created in User View""" | ||
# if value != oldvalue: | ||
# return utils.encrypt_password(value) | ||
# return value | ||
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This listens for when a new admin password is added in the admin view and encrypts with same method as before_first_request(). Removed. |
||
|
||
|
||
@security.context_processor | ||
def security_context_processor(): | ||
return dict( | ||
admin_base_template=admin.base_template, | ||
admin_view=admin.index_view, | ||
h=admin_helpers, | ||
get_url=url_for | ||
) | ||
|
||
|
||
@app.shell_context_processor | ||
def make_shell_context(): | ||
return {'db': db, 'Resource': Resource, 'Category': Category, 'Language': Language} | ||
return {'db': db, 'Resource': Resource, 'Category': Category, 'Language': Language, | ||
'User': User, 'Role': Role} | ||
|
||
|
||
@app.before_first_request | ||
def before_first_request(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename this so we don't accidentally shadow or get confused. Choose a name that is descriptive of what the function is doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed to add_admin_role
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the admin user already exists when this function runs, what happens? Are there side effects? |
||
""" Adds admin/user roles and default admin account and password if none exists""" | ||
db.create_all() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this idempotent? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only way to check would be to delete the table from the database I think? May need help with this. |
||
user_datastore.find_or_create_role(name='admin', description='Administrator') | ||
user_datastore.find_or_create_role(name='user', description='End User') | ||
|
||
admin_email = os.environ.get('ADMIN_EMAIL', "[email protected]") | ||
admin_password = os.environ.get('ADMIN_PASSWORD', 'password') | ||
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want this to blow up. Please use bracket syntax here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to bracket from |
||
|
||
encrypted_password = utils.encrypt_password(admin_password) | ||
|
||
if not user_datastore.get_user(admin_email): | ||
user_datastore.create_user(email=admin_email, password=encrypted_password) | ||
db.session.commit() | ||
|
||
user_datastore.add_role_to_user(admin_email, 'admin') | ||
db.session.commit() | ||
|
||
|
||
if __name__ == "__main__": | ||
app.run() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please add a newline to the end of this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added