Skip to content

Commit 98c2b7a

Browse files
committed
Adds flask admin as a feature. Bumps flask admin to 1.6.0
1 parent a9e8702 commit 98c2b7a

File tree

7 files changed

+120
-6
lines changed

7 files changed

+120
-6
lines changed

.env

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ FLASK_DEBUG=1
77
ALGOLIA_APP_ID=search_id
88
ALGOLIA_API_KEY=search_key
99
INDEX_NAME=resources_api
10+
SECRET_KEY=sammy
11+
SECURITY_PASSWORD_SALT=saltedpop
12+
ADMIN_EMAIL=[email protected]
13+
ADMIN_PASSWORD=1234

Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ RUN apt-get update \
1919
&& pip install poetry \
2020
&& poetry config virtualenvs.create false
2121

22+
RUN poetry lock
23+
2224
RUN poetry install --no-dev --no-interaction --no-ansi
2325

2426
COPY . /src

app/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from algoliasearch.search_client import SearchClient
22
from configs import Config
3-
43
from flask import Flask
54
from flask_migrate import Migrate
65
from flask_sqlalchemy import SQLAlchemy

app/models.py

+35
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from sqlalchemy import DateTime
33
from sqlalchemy.sql import func
44
from sqlalchemy_utils import URLType
5+
from flask_security import UserMixin, RoleMixin
56

67
language_identifier = db.Table('language_identifier',
78
db.Column(
@@ -206,3 +207,37 @@ class VoteInformation(db.Model):
206207
current_direction = db.Column(db.String, nullable=True)
207208
resource = db.relationship('Resource', back_populates='voters')
208209
voter = db.relationship('Key', back_populates='voted_resources')
210+
211+
212+
roles_users = db.Table(
213+
'roles_users',
214+
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
215+
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
216+
217+
218+
class Role(db.Model, RoleMixin):
219+
'''Role has three fields, ID, name and description'''
220+
id = db.Column(db.Integer(), primary_key=True)
221+
name = db.Column(db.String(80), unique=True)
222+
description = db.Column(db.String(255))
223+
224+
def __str__(self):
225+
return self.name
226+
227+
# __hash__ method avoids the exception, returns attribute that does not change
228+
# TypeError:unhashable type:'Role' when saving a User
229+
def __hash__(self):
230+
return self.name
231+
232+
233+
class User(db.Model, UserMixin):
234+
id = db.Column(db.Integer(), primary_key=True)
235+
email = db.Column(db.String(255), unique=True)
236+
password = db.Column(db.String(255))
237+
active = db.Column(db.Boolean())
238+
confirmed_at = db.Column(db.DateTime())
239+
roles = db.relationship(
240+
'Role',
241+
secondary=roles_users,
242+
backref=db.backref('users', lazy='dynamic')
243+
)

configs.py

+17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ def get_sys_exec_root_or_drive():
3737
if not all([algolia_app_id, algolia_api_key]):
3838
print("Application requires 'ALGOLIA_APP_ID' and 'ALGOLIA_API_KEY' for search")
3939

40+
secret_key = os.environ.get('SECRET_KEY', None)
41+
security_password_hash = 'pbkdf2_sha512'
42+
security_password_salt = os.environ.get('SECURITY_PASSWORD_SALT', None)
43+
44+
if not all([secret_key, security_password_salt]):
45+
print('Application requires "SECRET_KEY" and "SECURITY_HASH"')
46+
4047
index_name = os.environ.get("INDEX_NAME")
4148

4249

@@ -49,6 +56,16 @@ class Config:
4956
ALGOLIA_API_KEY = algolia_api_key
5057
INDEX_NAME = index_name
5158

59+
SECRET_KEY = secret_key
60+
SECURITY_URL_PREFIX = "/admin"
61+
SECURITY_PASSWORD_HASH = security_password_hash
62+
SECURITY_PASSWORD_SALT = security_password_salt
63+
SECURITY_LOGIN_URL = "/login/"
64+
SECURITY_LOGOUT_URL = "/logout/"
65+
SECURITY_POST_LOGIN_VIEW = "/admin/"
66+
SECURITY_POST_LOGOUT_VIEW = "/admin/"
67+
SECURITY_REGISTERABLE = False
68+
SECURITY_SEND_REGISTER_EMAIL = False
5269
# Can pass in changes to defaults, such as PaginatorConfig(per_page=40)
5370
RESOURCE_PAGINATOR = PaginatorConfig()
5471
LANGUAGE_PAGINATOR = PaginatorConfig()

pyproject.toml

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ python = "^3.7"
1010
algoliasearch = ">=2.0,<3.0"
1111
alembic = "1.5.8"
1212
bandit = "1.5.1"
13-
click = "7.1.2"
13+
click = "8.1.3"
1414
flake8 = "3.9.0"
15-
flask = "1.1.2"
15+
flask = "2.1.2"
1616
Flask-Cors = "3.0.10"
1717
Flask-Migrate = "2.7.0"
1818
prometheus_client = "0.9.0"
@@ -27,9 +27,13 @@ requests = "2.25.1"
2727
sqlalchemy = "1.3.22"
2828
SQLAlchemy-Utils = "0.36.8"
2929
uWSGI = "2.0.19.1"
30-
Werkzeug = "1.0.1"
30+
Werkzeug = "2.1.2"
3131
pyjwt = "^2.0.1"
3232
cryptography = "^3.4"
33+
flask-admin = "^1.6.0"
34+
Flask-Login = "^0.6.1"
35+
Flask-Security = "^3.0.0"
36+
email-validator = "^1.2.1"
3337

3438
[tool.poetry.dev-dependencies]
3539

run.py

+55-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
from app import app, cli
2-
from app.models import Category, Language, Resource, db
2+
from app.admin import run_flask_admin
3+
from app.models import Category, Language, Resource, db, Role, User
4+
import os
5+
from flask_security import Security, SQLAlchemyUserDatastore, utils
6+
from flask import url_for
7+
from flask_admin import helpers as admin_helpers
38
from werkzeug.middleware.dispatcher import DispatcherMiddleware
49
from prometheus_client import make_wsgi_app
10+
from sqlalchemy import event
511

12+
admin = run_flask_admin(app)
13+
14+
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
15+
security = Security(app, user_datastore)
616

717
if __name__ == "__main__":
818
app.run()
@@ -15,6 +25,49 @@
1525
})
1626

1727

28+
# @event.listens_for(User.password, 'set', retval=True)
29+
# def hash_user_password(target, value, oldvalue, initiator):
30+
# """Encrypts password when new admin created in User View"""
31+
# if value != oldvalue:
32+
# return utils.encrypt_password(value)
33+
# return value
34+
35+
36+
@security.context_processor
37+
def security_context_processor():
38+
return dict(
39+
admin_base_template=admin.base_template,
40+
admin_view=admin.index_view,
41+
h=admin_helpers,
42+
get_url=url_for
43+
)
44+
45+
1846
@app.shell_context_processor
1947
def make_shell_context():
20-
return {'db': db, 'Resource': Resource, 'Category': Category, 'Language': Language}
48+
return {'db': db, 'Resource': Resource, 'Category': Category, 'Language': Language,
49+
'User': User, 'Role': Role}
50+
51+
52+
@app.before_first_request
53+
def before_first_request():
54+
""" Adds admin/user roles and default admin account and password if none exists"""
55+
db.create_all()
56+
user_datastore.find_or_create_role(name='admin', description='Administrator')
57+
user_datastore.find_or_create_role(name='user', description='End User')
58+
59+
admin_email = os.environ.get('ADMIN_EMAIL', "[email protected]")
60+
admin_password = os.environ.get('ADMIN_PASSWORD', 'password')
61+
62+
encrypted_password = utils.encrypt_password(admin_password)
63+
64+
if not user_datastore.get_user(admin_email):
65+
user_datastore.create_user(email=admin_email, password=encrypted_password)
66+
db.session.commit()
67+
68+
user_datastore.add_role_to_user(admin_email, 'admin')
69+
db.session.commit()
70+
71+
72+
if __name__ == "__main__":
73+
app.run()

0 commit comments

Comments
 (0)