Skip to content
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

Feature/implement flask admin #403

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ FLASK_DEBUG=1
ALGOLIA_APP_ID=search_id
ALGOLIA_API_KEY=search_key
INDEX_NAME=resources_api
SECRET_KEY=change_secret_key
SECURITY_PASSWORD_SALT=!@#$!!@$%@
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ If you encounter any errors, please open an issue or contact us on slack in #oc-
}'
```

## Admin Panel Access

This project has an admin panel which can be used to view and manually edit categories and languages at a higher level than the API allows.
In order to create admin user, set following environment variables before starting the application -
`ADMIN_EMAIL` and `ADMIN_PASSWORD`

## Development Notes

If you make changes to the models.py or other schemas, you need to run a migration and upgrade again:
Expand Down
33 changes: 33 additions & 0 deletions app/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from flask_admin import Admin, AdminIndexView
from flask_admin.contrib.sqla import ModelView
from flask import url_for, redirect, request
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.has_role("admin")

def inaccessible_callback(self):
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):
admin = Admin(app, name="Resources_api", url='/',
index_view=HomeAdminView(name="Home"))
admin.add_view(AdminView(Role, db.session))
admin.add_view(AdminView(User, db.session))
admin.add_view(AdminView(Resource, db.session))
admin.add_view(AdminView(Category, db.session))
admin.add_view(AdminView(Language, db.session))
return admin
43 changes: 43 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sqlalchemy import DateTime
from sqlalchemy.sql import func
from sqlalchemy_utils import URLType
from flask_security import RoleMixin, UserMixin

language_identifier = db.Table('language_identifier',
db.Column(
Expand Down Expand Up @@ -206,3 +207,45 @@ class VoteInformation(db.Model):
current_direction = db.Column(db.String, nullable=True)
resource = db.relationship('Resource', back_populates='voters')
voter = db.relationship('Key', back_populates='voted_resources')


roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
)


# Role class
class Role(db.Model, RoleMixin):
# Our Role has three fields, ID, name and description
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))

def __str__(self):
return self.name

# __hash__ is required to avoid the exception
# TypeError: unhashable type: 'Role' when saving a User
def __hash__(self):
return hash(self.name)


# User class
class User(db.Model, UserMixin):

# Our User has six fields: ID, email, password, active, confirmed_at
# and roles. The roles field represents a many-to-many relationship
# using the roles_users table. Each user may have no role, one role,
# or multiple roles.
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship(
'Role',
secondary=roles_users,
backref=db.backref('users', lazy='dynamic')
)
5 changes: 5 additions & 0 deletions app/templates/admin/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends 'admin/master.html' %}

{% block body %}
<p>Admin Home Page</p>
{% endblock %}
15 changes: 15 additions & 0 deletions configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ 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)

security_password_hash = 'pbkdf2_sha512'
# Replace this with your own salt.
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'")


index_name = os.environ.get("INDEX_NAME")


Expand All @@ -45,6 +56,10 @@ class Config:

SQLALCHEMY_DATABASE_URI = f"postgresql://{pg_user}:{pg_pw}@{pg_host}:5432/{pg_db}"

SECRET_KEY = secret_key
SECURITY_PASSWORD_HASH = security_password_hash
SECURITY_PASSWORD_SALT = security_password_salt

ALGOLIA_APP_ID = algolia_app_id
ALGOLIA_API_KEY = algolia_api_key
INDEX_NAME = index_name
Expand Down
52 changes: 52 additions & 0 deletions migrations/versions/824f1576e904_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""empty message

Revision ID: 824f1576e904
Revises: 205742d3b3f5
Create Date: 2020-10-20 10:36:16.978231

"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils


# revision identifiers, used by Alembic.
revision = '824f1576e904'
down_revision = 'fc34137ad3ba'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('role',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('password', sa.String(length=255), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('roles_users',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('roles_users')
op.drop_table('user')
op.drop_table('role')
# ### end Alembic commands ###