├── tests ├── __init__.py ├── test_websocket.py └── test_REST.py ├── general ├── __init__.py ├── database │ ├── __init__.py │ ├── base.py │ ├── reset_db.py │ ├── session_scope.py │ ├── schema.py │ └── setup_notifications.py ├── domains │ ├── __init__.py │ ├── admin │ │ ├── __init__.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ ├── insert_FormSettings.py │ │ │ ├── insert_TableColumnSettings.py │ │ │ ├── insert_TableSettings.py │ │ │ └── insert_Submenus.py │ │ ├── features │ │ │ ├── __init__.py │ │ │ ├── all_feature.py │ │ │ ├── user_feature.py │ │ │ └── admin_feature.py │ │ ├── setup_admin.py │ │ ├── api_views │ │ │ ├── __init__.py │ │ │ ├── home.py │ │ │ ├── menubar.py │ │ │ ├── forms.py │ │ │ ├── form_fields.py │ │ │ ├── datatable_columns.py │ │ │ └── datatables.py │ │ ├── materialized_views │ │ │ ├── __init__.py │ │ │ ├── schemas.py │ │ │ ├── forms.py │ │ │ ├── tables.py │ │ │ ├── table_columns.py │ │ │ ├── form_fields.py │ │ │ └── refresh_trigger.py │ │ ├── models │ │ │ ├── feature_sets.py │ │ │ ├── __init__.py │ │ │ ├── notification_message_settings.py │ │ │ ├── home_settings.py │ │ │ ├── submenus.py │ │ │ ├── feature_sets_users.py │ │ │ ├── context_menu_items.py │ │ │ ├── form_field_settings.py │ │ │ ├── dialog_settings.py │ │ │ ├── form_settings.py │ │ │ ├── table_settings.py │ │ │ ├── mapper_settings.py │ │ │ └── table_column_settings.py │ │ ├── views │ │ │ ├── __init__.py │ │ │ ├── default_home_settings.py │ │ │ ├── default_form_field_settings.py │ │ │ ├── submenu_items.py │ │ │ ├── default_form_settings.py │ │ │ ├── default_datatable_settings.py │ │ │ ├── menubar.py │ │ │ └── default_datatable_column_settings.py │ │ ├── admin_api_schema.py │ │ └── admin_schema.py │ ├── auth │ │ ├── __init__.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ └── insert_User.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── notification_channels.py │ │ │ └── users.py │ │ ├── api_functions │ │ │ ├── __init__.py │ │ │ ├── websocket_login.py │ │ │ ├── logout.py │ │ │ ├── token.py │ │ │ └── login.py │ │ ├── setup_auth.py │ │ ├── functions │ │ │ ├── __init__.py │ │ │ ├── jwt_url_encode.py │ │ │ ├── encrypt_password.py │ │ │ ├── check_if_role_exists.py │ │ │ ├── authenticate_user_email.py │ │ │ ├── jwt_sign.py │ │ │ └── jwt_algorithm_sign.py │ │ ├── auth_api_schema.py │ │ └── auth_schema.py │ └── setup_domains.py ├── utilities.py └── logger.py ├── alembic ├── README ├── script.py.mako └── env.py ├── general_insignia.png ├── requirements.txt ├── postgrest.conf ├── PostgREST_Environment_Vars.md ├── setup.py ├── alembic.ini ├── README.md └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/domains/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/domains/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/domains/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/domains/admin/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /general/domains/auth/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | alembic revision --autogenerate -m "" 2 | -------------------------------------------------------------------------------- /general/domains/auth/models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .users import Users -------------------------------------------------------------------------------- /general/domains/admin/features/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin_feature import insert_admin_feature 2 | -------------------------------------------------------------------------------- /general_insignia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PierreRochard/general/HEAD/general_insignia.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | alembic 3 | psycopg2 4 | PyJWT 5 | requests 6 | structlog 7 | sqlalchemy 8 | websockets -------------------------------------------------------------------------------- /general/database/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /general/utilities.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def get_script_path(): 6 | return os.path.dirname(os.path.realpath(sys.argv[0])) 7 | -------------------------------------------------------------------------------- /general/domains/auth/api_functions/__init__.py: -------------------------------------------------------------------------------- 1 | from .login import create_login_api_trigger 2 | from .logout import create_logout_api_trigger 3 | from .token import create_token_api_trigger 4 | -------------------------------------------------------------------------------- /general/domains/setup_domains.py: -------------------------------------------------------------------------------- 1 | from general.domains.admin.setup_admin import setup_admin 2 | from general.domains.auth.setup_auth import setup_auth 3 | 4 | 5 | def setup_domains(): 6 | setup_auth() 7 | setup_admin() 8 | 9 | 10 | if __name__ == '__main__': 11 | setup_domains() 12 | -------------------------------------------------------------------------------- /general/domains/auth/api_functions/websocket_login.py: -------------------------------------------------------------------------------- 1 | 2 | # Todo: implement login for websocket clients 3 | # session.execute(""" 4 | # CREATE OR REPLACE FUNCTION 5 | # api.websocket_login(channel TEXT) 6 | # RETURNS auth.jwt_token 7 | # LANGUAGE plpgsql 8 | # AS $$ 9 | # DECLARE 10 | # """) 11 | -------------------------------------------------------------------------------- /general/domains/auth/setup_auth.py: -------------------------------------------------------------------------------- 1 | from general.domains.auth.auth_api_schema import AuthApiSchema 2 | from general.domains.auth.auth_schema import AuthSchema 3 | 4 | 5 | def setup_auth(): 6 | auth_schema = AuthSchema() 7 | auth_schema.setup() 8 | 9 | auth_api_schema = AuthApiSchema() 10 | auth_api_schema.setup() 11 | 12 | 13 | if __name__ == '__main__': 14 | setup_auth() 15 | -------------------------------------------------------------------------------- /general/domains/admin/setup_admin.py: -------------------------------------------------------------------------------- 1 | from general.domains.admin.admin_api_schema import AdminApiSchema 2 | from general.domains.admin.admin_schema import AdminSchema 3 | 4 | 5 | def setup_admin(): 6 | admin_schema = AdminSchema() 7 | admin_schema.setup() 8 | 9 | admin_api_schema = AdminApiSchema() 10 | admin_api_schema.setup() 11 | 12 | 13 | if __name__ == '__main__': 14 | setup_admin() 15 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/__init__.py: -------------------------------------------------------------------------------- 1 | from .datatable_columns import ( 2 | create_datatable_columns_trigger, 3 | create_datatable_columns_view 4 | ) 5 | from .datatables import create_datatables_trigger, create_datatables_view 6 | from .form_fields import create_form_fields_view 7 | from .forms import create_forms_view 8 | from .home import create_home_view 9 | from .menubar import create_menubar_view 10 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/__init__.py: -------------------------------------------------------------------------------- 1 | from .form_fields import create_form_fields_materialized_view 2 | from .forms import create_forms_materialized_view 3 | from .refresh_trigger import create_materialized_views_refresh_trigger 4 | from .schemas import create_schemas_materialized_view 5 | from .table_columns import create_table_columns_materialized_view 6 | from .tables import create_tables_materialized_view 7 | -------------------------------------------------------------------------------- /postgrest.conf: -------------------------------------------------------------------------------- 1 | db-uri = "$(PGRST_DB_URI)" 2 | db-schema = "$(PGRST_DB_SCHEMA)" 3 | db-anon-role = "$(PGRST_DB_ANON_ROLE)" 4 | db-pool = "$(PGRST_DB_POOL)" 5 | 6 | server-host = "$(PGRST_SERVER_HOST)" 7 | server-port = "$(PGRST_SERVER_PORT)" 8 | 9 | server-proxy-url = "$(PGRST_SERVER_PROXY_URL)" 10 | jwt-secret = "$(PGRST_JWT_SECRET)" 11 | 12 | max-rows = "$(PGRST_MAX_ROWS)" 13 | pre-request = "$(PGRST_PRE_REQUEST)" 14 | -------------------------------------------------------------------------------- /general/domains/auth/functions/__init__.py: -------------------------------------------------------------------------------- 1 | from .authenticate_user_email import create_authenticate_user_email_function 2 | from .check_if_role_exists import create_check_if_role_exists_function 3 | from .encrypt_password import create_encrypt_password_function 4 | from .jwt_algorithm_sign import create_jwt_algorithm_sign_function 5 | from .jwt_sign import create_jwt_sign_function 6 | from .jwt_url_encode import create_jwt_url_encode_function 7 | -------------------------------------------------------------------------------- /PostgREST_Environment_Vars.md: -------------------------------------------------------------------------------- 1 | ### Environment variables 2 | 3 | export PGRST_DB_URI="postgres://user:pass@host:5432/dbname" 4 | export PGRST_DB_SCHEMA="" 5 | export PGRST_DB_ANON_ROLE="" 6 | export PGRST_DB_POOL="" 7 | export PGRST_SERVER_HOST="" 8 | export PGRST_SERVER_PORT="" 9 | export PGRST_SERVER_PROXY_URL="" 10 | export PGRST_JWT_SECRET="" 11 | export PGRST_SECRET_IS_BASE64="" 12 | export PGRST_MAX_ROWS="" 13 | export PGRST_PRE_REQUEST="" 14 | -------------------------------------------------------------------------------- /general/domains/admin/models/feature_sets.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Column, String, text) 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | 6 | 7 | class FeatureSets(Base): 8 | __tablename__ = 'feature_sets' 9 | __table_args__ = ({'schema': 'admin'}, 10 | ) 11 | 12 | id = Column(UUID, 13 | server_default=text('auth.gen_random_uuid()'), 14 | primary_key=True) 15 | 16 | name = Column(String, unique=True) 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="general", 5 | version="0.0.0", 6 | author="Pierre Rochard", 7 | packages=setuptools.find_packages( 8 | exclude=['googleapis', "*.tests", "*.tests.*", "tests.*", "tests"]), 9 | classifiers=[ 10 | "Programming Language :: Python :: 3", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | ], 14 | install_requires=[ 15 | ], 16 | python_requires='>=3', 17 | ) 18 | -------------------------------------------------------------------------------- /general/domains/admin/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .default_datatable_column_settings import create_default_datatable_column_settings_view 2 | from .default_datatable_settings import create_default_datatable_settings_view 3 | from .default_form_field_settings import create_default_form_field_settings_view 4 | from .default_form_settings import create_default_form_settings_view 5 | from .default_home_settings import create_default_home_settings_view 6 | from .menubar import create_menubar_view 7 | from .submenu_items import create_submenu_items_view 8 | -------------------------------------------------------------------------------- /general/domains/auth/api_functions/logout.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_logout_api_trigger(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | CREATE OR REPLACE FUNCTION 8 | auth_api.logout() 9 | RETURNS VOID 10 | LANGUAGE plpgsql 11 | AS $$ 12 | BEGIN 13 | END; 14 | $$; 15 | """) 16 | 17 | 18 | if __name__ == '__main__': 19 | create_logout_api_trigger() 20 | -------------------------------------------------------------------------------- /general/domains/auth/scripts/insert_User.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from general.database.session_scope import session_scope 4 | 5 | from ..models import Users 6 | 7 | 8 | def insert_user(user_data): 9 | new_user = Users(**user_data) 10 | with session_scope() as session: 11 | session.add(new_user) 12 | 13 | 14 | def insert_admin(): 15 | user = { 16 | 'email': os.environ['REST_EMAIL'], 17 | 'password': os.environ['REST_PASSWORD'], 18 | 'role': os.environ['REST_USER'], 19 | } 20 | insert_user(user) 21 | 22 | if __name__ == '__main__': 23 | insert_admin() 24 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /general/domains/auth/models/notification_channels.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, text 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | 6 | 7 | class NotificationChannels(Base): 8 | __tablename__ = 'notification_channels' 9 | __table_args__ = {'schema': 'auth'} 10 | 11 | id = Column(UUID, 12 | server_default=text('auth.gen_random_uuid()'), 13 | primary_key=True) 14 | channel_name = Column(String, unique=True) 15 | table = Column(String) 16 | schema = Column(String) 17 | jwt = Column(String) 18 | -------------------------------------------------------------------------------- /general/domains/admin/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .context_menu_items import ContextMenuItems 2 | from .dialog_settings import DialogSettings 3 | from .feature_sets import FeatureSets 4 | from .feature_sets_users import FeatureSetsUsers 5 | from .form_field_settings import FormFieldSettings 6 | from .form_settings import FormSettings 7 | from .home_settings import HomeSettings 8 | from .mapper_settings import MapperSettings 9 | from .notification_message_settings import NotificationMessageSettings 10 | from .submenus import Submenus 11 | from .table_column_settings import TableColumnSettings 12 | from .table_settings import TableSettings 13 | -------------------------------------------------------------------------------- /general/domains/auth/functions/jwt_url_encode.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_jwt_url_encode_function(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth.jwt_url_encode(data BYTEA); 8 | """) 9 | 10 | session.execute(""" 11 | CREATE OR REPLACE FUNCTION auth.jwt_url_encode(data BYTEA) 12 | RETURNS TEXT 13 | LANGUAGE SQL 14 | AS $$ 15 | SELECT translate(encode(data, 'base64'), E'+/=\n', '-_'); 16 | $$; 17 | """) 18 | -------------------------------------------------------------------------------- /tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import jwt 4 | import websockets 5 | 6 | 7 | async def hello(): 8 | async with websockets.connect(host + encoded) as websocket: 9 | while True: 10 | greeting = await websocket.recv() 11 | print(greeting) 12 | 13 | if __name__ == '__main__': 14 | 15 | payload = { 16 | 'mode': 'rw', 17 | 'channel': 'messages_table_update' 18 | } 19 | 20 | encoded = jwt.encode(payload, '4S7lR9SnY8g3', algorithm='HS256').decode('utf-8') 21 | print(encoded) 22 | host = 'ws://localhost:4545/' 23 | asyncio.get_event_loop().run_until_complete(hello()) 24 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/home.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_home_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin_api.home CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin_api.home AS 11 | SELECT dhs.body, 12 | dhs.custom_name, 13 | dhs.headline, 14 | dhs.icon, 15 | dhs.sub_headline, 16 | dhs.supporting_image 17 | FROM admin.default_home_settings dhs 18 | WHERE dhs."user" = current_user; 19 | """) 20 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/schemas.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_schemas_materialized_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP MATERIALIZED VIEW IF EXISTS admin.schemas CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE MATERIALIZED VIEW admin.schemas AS 11 | SELECT 12 | replace(schema_name, '_api', '') AS schema_name 13 | FROM information_schema.schemata 14 | WHERE schema_name LIKE '%_api'; 15 | """) 16 | 17 | 18 | if __name__ == '__main__': 19 | create_schemas_materialized_view() 20 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/forms.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_forms_materialized_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP MATERIALIZED VIEW IF EXISTS admin.forms CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE MATERIALIZED VIEW admin.forms AS 11 | SELECT replace(specific_schema, '_api', '') AS schema_name, 12 | routine_name as form_name 13 | FROM information_schema.routines 14 | WHERE specific_schema LIKE '%_api'; 15 | """) 16 | 17 | 18 | if __name__ == '__main__': 19 | create_forms_materialized_view() 20 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/menubar.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_menubar_view(): 5 | """ 6 | Frontend usage: 7 | params.set('select', 'label, icon, routerLink, items'); 8 | return this.restClient.get('/menubar', params); 9 | """ 10 | with session_scope() as session: 11 | session.execute(""" 12 | DROP VIEW IF EXISTS admin_api.menubar CASCADE; 13 | """) 14 | session.execute(""" 15 | CREATE OR REPLACE VIEW admin_api.menubar AS 16 | SELECT m."label", 17 | m.icon, 18 | m."routerLink", 19 | m.items 20 | FROM admin.menubar m 21 | WHERE m."user" = current_user; 22 | """) 23 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/tables.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_tables_materialized_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP MATERIALIZED VIEW IF EXISTS admin.tables CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE MATERIALIZED VIEW admin.tables AS 11 | SELECT 12 | replace(table_schema, '_api', '') AS schema_name, 13 | table_name, 14 | table_type 15 | FROM information_schema.tables 16 | WHERE table_schema LIKE '%_api'; 17 | """) 18 | 19 | 20 | if __name__ == '__main__': 21 | create_tables_materialized_view() 22 | -------------------------------------------------------------------------------- /general/database/reset_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.compiler import compiles 2 | from sqlalchemy.sql.ddl import DropTable 3 | 4 | from general.database.base import Base 5 | from general.database.session_scope import session_scope 6 | 7 | 8 | # Necessary for models to register with Base 9 | import general.domains.admin.models 10 | import general.domains.auth.models 11 | 12 | 13 | @compiles(DropTable, "postgresql") 14 | def _compile_drop_table(element, compiler, **kwargs): 15 | return compiler.visit_drop_table(element) + " CASCADE" 16 | 17 | 18 | def reset_database(): 19 | with session_scope() as session: 20 | Base.metadata.drop_all(session.connection()) 21 | Base.metadata.create_all(session.connection()) 22 | 23 | if __name__ == '__main__': 24 | reset_database() 25 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | script_location = alembic 3 | file_template = %%(rev)s_%%(slug)s_%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d_%%(minute).2d_%%(second).2d 4 | 5 | [loggers] 6 | keys = root,sqlalchemy,alembic 7 | 8 | [handlers] 9 | keys = console 10 | 11 | [formatters] 12 | keys = generic 13 | 14 | [logger_root] 15 | level = WARN 16 | handlers = console 17 | qualname = 18 | 19 | [logger_sqlalchemy] 20 | level = WARN 21 | handlers = 22 | qualname = sqlalchemy.engine 23 | 24 | [logger_alembic] 25 | level = INFO 26 | handlers = 27 | qualname = alembic 28 | 29 | [handler_console] 30 | class = StreamHandler 31 | args = (sys.stderr,) 32 | level = NOTSET 33 | formatter = generic 34 | 35 | [formatter_generic] 36 | format = %(levelname)-5.5s [%(name)s] %(message)s 37 | datefmt = %H:%M:%S 38 | -------------------------------------------------------------------------------- /general/domains/admin/scripts/insert_FormSettings.py: -------------------------------------------------------------------------------- 1 | from general.models import FormSettings, session_scope 2 | 3 | 4 | def insert_form_settings(user): 5 | with session_scope() as session: 6 | for form_name, in session.execute('SELECT form_name FROM admin.forms'): 7 | new_record_data = { 8 | 'user': user, 9 | 'form_name': form_name, 10 | 'custom_name': form_name.replace('_', ' ').title(), 11 | 'icon': 'fa-pencil-square-o', 12 | 'submenu_id': None, 13 | 'order_index': 10, 14 | } 15 | new_record = FormSettings(**new_record_data) 16 | session.add(new_record) 17 | 18 | 19 | 20 | if __name__ == '__main__': 21 | insert_form_settings('anon') 22 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/forms.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_forms_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin_api.forms CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin_api.forms AS 11 | SELECT (row_number() OVER())::INT id, * 12 | FROM ( 13 | SELECT 14 | dfs.custom_button_copy, 15 | dfs.custom_name, 16 | dfs.dialog_settings, 17 | dfs.form_name, 18 | dfs.schema_name, 19 | dfs.user_id 20 | FROM admin.default_form_settings dfs 21 | WHERE dfs."user" = current_user 22 | ) sub; 23 | """) 24 | -------------------------------------------------------------------------------- /general/domains/auth/functions/encrypt_password.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_encrypt_password_function(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth.encrypt_password(_email TEXT, _password TEXT) CASCADE; 8 | """) 9 | 10 | session.execute(""" 11 | CREATE OR REPLACE FUNCTION 12 | auth.encrypt_password() 13 | RETURNS TRIGGER 14 | LANGUAGE plpgsql 15 | AS $$ 16 | BEGIN 17 | IF tg_op = 'INSERT' OR new.password <> old.password 18 | THEN 19 | new.password = auth.crypt(new.password, auth.gen_salt('bf', 8)); 20 | END IF; 21 | RETURN new; 22 | END 23 | $$; 24 | 25 | """) 26 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/form_fields.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_form_fields_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin_api.form_fields CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin_api.form_fields AS 11 | SELECT (row_number() OVER())::INT id, * 12 | FROM ( 13 | SELECT 14 | dffs.custom_name, 15 | dffs.field_name, 16 | dffs.field_type, 17 | dffs.form_name, 18 | dffs.schema_name, 19 | dffs.user_id 20 | FROM admin.default_form_field_settings dffs 21 | WHERE dffs."user" = current_user 22 | ORDER BY dffs.order_index ASC 23 | ) sub; 24 | """) 25 | 26 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/table_columns.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_table_columns_materialized_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP MATERIALIZED VIEW IF EXISTS admin.table_columns CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE MATERIALIZED VIEW admin.table_columns AS 11 | SELECT replace(table_schema, '_api', '') AS schema_name, 12 | table_name, 13 | column_name, 14 | is_nullable, 15 | column_default, 16 | data_type 17 | FROM information_schema.columns 18 | WHERE table_schema LIKE '%_api' 19 | AND column_name NOT LIKE '%_select_items'; 20 | """) 21 | 22 | 23 | if __name__ == '__main__': 24 | create_table_columns_materialized_view() 25 | -------------------------------------------------------------------------------- /general/domains/admin/scripts/insert_TableColumnSettings.py: -------------------------------------------------------------------------------- 1 | from general.models import TableColumnSettings, Users, \ 2 | session_scope 3 | 4 | 5 | def insert_table_column_settings(user): 6 | with session_scope() as session: 7 | columns = session.execute('SELECT table_name, column_name FROM admin.columns') 8 | for table_name, column_name in columns: 9 | new_record_data = { 10 | 'user': user, 11 | 'table_name': table_name, 12 | 'column_name': column_name, 13 | 'custom_name': column_name.replace('_', ' ').title(), 14 | 'is_filterable': False 15 | } 16 | new_record = TableColumnSettings(**new_record_data) 17 | with session_scope() as session: 18 | session.add(new_record) 19 | 20 | 21 | if __name__ == '__main__': 22 | with session_scope() as session: 23 | roles = session.query(Users.role).all() 24 | roles = [r[0] for r in roles] 25 | roles.append('anon') 26 | for role in roles: 27 | insert_table_column_settings(role) 28 | -------------------------------------------------------------------------------- /general/domains/admin/models/notification_message_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, String, text, UniqueConstraint) 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | 6 | 7 | class NotificationMessageSettings(Base): 8 | __tablename__ = 'notification_message_settings' 9 | __table_args__ = (UniqueConstraint('namespace', 10 | 'message_type', 11 | name='notification_message_settings_unique_constraint'), 12 | {'schema': 'admin'}, 13 | ) 14 | 15 | id = Column(UUID, 16 | server_default=text('auth.gen_random_uuid()'), 17 | primary_key=True) 18 | namespace = Column(String, nullable=False) 19 | message_type = Column(String, nullable=False) 20 | 21 | is_visible = Column(Boolean, nullable=False, default=False) 22 | severity = Column(String, nullable=False, default='info') 23 | summary = Column(String) 24 | detail = Column(String) 25 | -------------------------------------------------------------------------------- /general/domains/auth/functions/check_if_role_exists.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_check_if_role_exists_function(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth.check_if_role_exists() CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE FUNCTION 11 | auth.check_if_role_exists() 12 | RETURNS TRIGGER 13 | LANGUAGE plpgsql 14 | AS $$ 15 | BEGIN 16 | IF NOT exists(SELECT 1 17 | FROM pg_roles 18 | WHERE pg_roles.rolname = NEW.role) 19 | THEN 20 | RAISE foreign_key_violation 21 | USING MESSAGE = 'Unknown database role: ' || NEW.role; 22 | RETURN NULL; 23 | END IF; 24 | RETURN NEW; 25 | END 26 | $$; 27 | 28 | """) 29 | -------------------------------------------------------------------------------- /general/domains/auth/functions/authenticate_user_email.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_authenticate_user_email_function(): 5 | """ 6 | Returns the user's role name if the provided email and password are good 7 | """ 8 | with session_scope() as session: 9 | session.execute(""" 10 | DROP FUNCTION IF EXISTS auth.authenticate_user_email(_email TEXT, _password TEXT) CASCADE; 11 | """) 12 | session.execute(""" 13 | CREATE OR REPLACE FUNCTION 14 | auth.authenticate_user_email(_email TEXT, _password TEXT) 15 | RETURNS NAME 16 | LANGUAGE plpgsql 17 | AS $$ 18 | BEGIN 19 | RETURN ( 20 | SELECT role 21 | FROM auth.users 22 | WHERE users.email = _email 23 | AND users.password = auth.crypt(_password, users.password) 24 | ); 25 | END; 26 | $$; 27 | """) 28 | 29 | if __name__ == '__main__': 30 | create_authenticate_user_email_function() 31 | -------------------------------------------------------------------------------- /general/domains/admin/models/home_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | ForeignKey, 4 | String, 5 | text, 6 | UniqueConstraint 7 | ) 8 | from sqlalchemy.dialects.postgresql import UUID 9 | 10 | from general.database.base import Base 11 | 12 | 13 | class HomeSettings(Base): 14 | __tablename__ = 'home_settings' 15 | __table_args__ = (UniqueConstraint('user_id', 16 | name='home_settings_unique_constraint'), 17 | {'schema': 'admin'}, 18 | ) 19 | 20 | id = Column(UUID, 21 | server_default=text('auth.gen_random_uuid()'), 22 | primary_key=True) 23 | 24 | body = Column(String) 25 | custom_name = Column(String) 26 | headline = Column(String) 27 | icon = Column(String) 28 | sub_headline = Column(String) 29 | supporting_image = Column(String) 30 | 31 | user_id = Column(UUID, 32 | ForeignKey('auth.users.id', 33 | onupdate='CASCADE', 34 | ondelete='CASCADE'), 35 | nullable=False) 36 | -------------------------------------------------------------------------------- /general/domains/auth/api_functions/token.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_token_api_trigger(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth_api.token() CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE FUNCTION 11 | auth_api.token() 12 | RETURNS auth.JWT_TOKEN 13 | LANGUAGE plpgsql 14 | AS $$ 15 | DECLARE 16 | result auth.JWT_TOKEN; 17 | BEGIN 18 | 19 | SELECT auth.jwt_sign(row_to_json(r), current_setting('app.jwt_ws_secret')) AS token 20 | FROM ( 21 | SELECT 22 | 'anon' AS role, 23 | 'rw' AS mode, 24 | extract(EPOCH FROM now()) :: INTEGER + current_setting('app.jwt_hours')::INTEGER * 60 * 60 AS exp 25 | ) r 26 | INTO result; 27 | RETURN result; 28 | END; 29 | $$; 30 | """) 31 | -------------------------------------------------------------------------------- /general/domains/admin/views/default_home_settings.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_default_home_settings_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.default_home_settings CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.default_home_settings AS 11 | SELECT coalesce(hs.id, auth.gen_random_uuid()) as id, 12 | u.role as "user", 13 | u.id as user_id, 14 | 15 | coalesce(hs.body, '') AS body, 16 | coalesce(hs.custom_name, 'General') AS custom_name, 17 | coalesce(hs.headline, 'Welcome') as headline, 18 | coalesce(hs.icon, 'fa-home') AS icon, 19 | coalesce(hs.sub_headline, '') as sub_headline, 20 | coalesce(hs.supporting_image, '') as supporting_image 21 | FROM auth.users u 22 | LEFT OUTER JOIN admin.home_settings hs 23 | ON u.id = hs.user_id 24 | ORDER BY u.role; 25 | """) 26 | -------------------------------------------------------------------------------- /general/domains/auth/functions/jwt_sign.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_jwt_sign_function(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth.jwt_sign(payload JSON, secret TEXT) CASCADE; 8 | """) 9 | 10 | session.execute(""" 11 | CREATE OR REPLACE FUNCTION auth.jwt_sign(payload JSON, secret TEXT) 12 | RETURNS TEXT 13 | LANGUAGE SQL 14 | AS $$ 15 | WITH 16 | header AS ( 17 | SELECT auth.jwt_url_encode( 18 | convert_to('{"alg":"HS256","typ":"JWT"}', 19 | 'utf8')) AS data 20 | ), 21 | payload AS ( 22 | SELECT auth.jwt_url_encode(convert_to(payload :: TEXT, 'utf8')) AS data 23 | ), 24 | signables AS ( 25 | SELECT header.data || '.' || payload.data AS data 26 | FROM header, payload 27 | ) 28 | SELECT signables.data || '.' || 29 | auth.jwt_algorithm_sign(signables.data, secret, 'HS256') 30 | FROM signables; 31 | $$; 32 | """) 33 | -------------------------------------------------------------------------------- /general/domains/auth/functions/jwt_algorithm_sign.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_jwt_algorithm_sign_function(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth.jwt_algorithm_sign(signables TEXT, 8 | secret TEXT, 9 | algorithm TEXT); 10 | """) 11 | 12 | session.execute(""" 13 | CREATE OR REPLACE FUNCTION auth.jwt_algorithm_sign(signables TEXT, secret TEXT, 14 | algorithm TEXT) 15 | RETURNS TEXT 16 | LANGUAGE SQL 17 | AS $$ 18 | WITH 19 | alg AS ( 20 | SELECT CASE 21 | WHEN algorithm = 'HS256' 22 | THEN 'sha256' 23 | WHEN algorithm = 'HS384' 24 | THEN 'sha384' 25 | WHEN algorithm = 'HS512' 26 | THEN 'sha512' 27 | ELSE '' END AS id) -- hmac throws error 28 | SELECT auth.jwt_url_encode(auth.hmac(signables, secret, alg.id)) 29 | FROM alg; 30 | $$; 31 | """) 32 | -------------------------------------------------------------------------------- /general/domains/admin/models/submenus.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, ForeignKey, Integer, String, 2 | UniqueConstraint, text) 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from sqlalchemy.orm import relationship 5 | 6 | from general.database.base import Base 7 | 8 | 9 | class Submenus(Base): 10 | __tablename__ = 'submenus' 11 | __table_args__ = (UniqueConstraint('user_id', 12 | 'submenu_name', 13 | name='submenus_unique_constraint'), 14 | {'schema': 'admin'}, 15 | ) 16 | 17 | id = Column(UUID, 18 | server_default=text('auth.gen_random_uuid()'), 19 | primary_key=True) 20 | submenu_name = Column(String, nullable=False) 21 | icon = Column(String) 22 | is_visible = Column(Boolean, default=True) 23 | order_index = Column(Integer, default=2) 24 | 25 | user_id = Column(UUID, 26 | ForeignKey('auth.users.id', 27 | onupdate='CASCADE', 28 | ondelete='CASCADE'), 29 | nullable=False) 30 | 31 | user = relationship('Users') 32 | -------------------------------------------------------------------------------- /general/domains/admin/views/default_form_field_settings.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_default_form_field_settings_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.default_form_field_settings CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.default_form_field_settings AS 11 | SELECT coalesce(ffs.id, auth.gen_random_uuid()) as id, 12 | u.role as "user", 13 | u.id as user_id, 14 | ff.schema_name, 15 | ff.form_name, 16 | ff.field_name, 17 | 18 | ff.field_type, 19 | 20 | coalesce(ffs.order_index, ff.ordinal_position) AS order_index, 21 | coalesce(ffs.custom_name, initcap(replace(ff.field_name, '_', ' '))) as custom_name 22 | FROM auth.users u 23 | LEFT OUTER JOIN admin.form_fields ff 24 | ON TRUE 25 | LEFT OUTER JOIN admin.form_field_settings ffs 26 | ON ff.form_name = ffs.form_name 27 | """) 28 | -------------------------------------------------------------------------------- /general/domains/admin/models/feature_sets_users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Column, ForeignKey, text, UniqueConstraint) 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | from general.domains.auth.models.users import Users 6 | 7 | 8 | class FeatureSetsUsers(Base): 9 | __tablename__ = 'feature_sets_users' 10 | __table_args__ = (UniqueConstraint('feature_set_id', 11 | 'user_id', 12 | name='feature_sets_users_unique_constraint'), 13 | {'schema': 'admin'}, 14 | ) 15 | 16 | id = Column(UUID, 17 | server_default=text('auth.gen_random_uuid()'), 18 | primary_key=True) 19 | 20 | feature_set_id = Column(UUID, 21 | ForeignKey('admin.feature_sets.id', 22 | onupdate='CASCADE', 23 | ondelete='CASCADE'), 24 | nullable=False) 25 | 26 | user_id = Column(UUID, 27 | ForeignKey(Users.id, 28 | onupdate='CASCADE', 29 | ondelete='CASCADE'), 30 | nullable=False) 31 | -------------------------------------------------------------------------------- /general/domains/admin/models/context_menu_items.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | ForeignKey, 4 | String, 5 | text, 6 | UniqueConstraint 7 | ) 8 | from sqlalchemy.dialects.postgresql import UUID 9 | 10 | from general.database.base import Base 11 | 12 | 13 | class ContextMenuItems(Base): 14 | __tablename__ = 'context_menu_items' 15 | __table_args__ = (UniqueConstraint('user_id', 16 | 'schema_name', 17 | 'table_name', 18 | 'label', 19 | name='context_menu_items_unique_constraint'), 20 | {'schema': 'admin'}, 21 | ) 22 | 23 | id = Column(UUID, 24 | server_default=text('auth.gen_random_uuid()'), 25 | primary_key=True) 26 | schema_name = Column(String, nullable=False) 27 | table_name = Column(String, nullable=False) 28 | 29 | label = Column(String, nullable=False) 30 | icon = Column(String, nullable=False) 31 | command = Column(String) 32 | 33 | user_id = Column(UUID, 34 | ForeignKey('auth.users.id', 35 | onupdate='CASCADE', 36 | ondelete='CASCADE'), 37 | nullable=False) 38 | -------------------------------------------------------------------------------- /general/domains/admin/views/submenu_items.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_submenu_items_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.submenu_items CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.submenu_items AS 11 | SELECT 12 | dts.user, 13 | dts.id, 14 | dts.custom_name AS "label", 15 | dts.icon, 16 | ARRAY['/', dts.schema_name, dts.table_name] AS "routerLink", 17 | dts.order_index, 18 | NULL as "items", 19 | dts.submenu_id 20 | FROM admin.default_datatable_settings dts 21 | WHERE dts.submenu_id IS NOT NULL 22 | UNION 23 | SELECT 24 | dfs.user, 25 | dfs.id, 26 | dfs.custom_name AS "label", 27 | dfs.icon, 28 | ARRAY['/', dfs.schema_name, 'rpc', dfs.form_name] AS "routerLink", 29 | dfs.order_index, 30 | NULL as "items", 31 | dfs.submenu_id 32 | FROM admin.default_form_settings dfs 33 | WHERE dfs.submenu_id IS NOT NULL 34 | 35 | ORDER BY order_index ASC NULLS LAST, "label" ASC NULLS LAST; 36 | """) 37 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/form_fields.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_form_fields_materialized_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP MATERIALIZED VIEW IF EXISTS admin.form_fields CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE MATERIALIZED VIEW admin.form_fields AS 11 | SELECT 12 | replace(params.specific_schema, '_api', '') AS schema_name, 13 | routines.routine_name as form_name, 14 | params.ordinal_position, 15 | params.parameter_mode, 16 | params.parameter_name as field_name, 17 | params.data_type as field_type 18 | FROM information_schema.parameters params 19 | LEFT JOIN information_schema.routines routines 20 | ON routines.specific_name = params.specific_name 21 | WHERE params.specific_schema LIKE '%_api' 22 | AND params.parameter_mode = 'IN'; 23 | """) 24 | 25 | if __name__ == '__main__': 26 | create_form_fields_materialized_view() 27 | 28 | # SELECT (row_number() OVER())::INT id, sub1.* FROM 29 | # (SELECT pg_proc.proname as form_name, 30 | # unnest(pg_proc.proargnames) as name 31 | # FROM pg_catalog.pg_proc) sub1 32 | # ; -------------------------------------------------------------------------------- /general/domains/admin/models/form_field_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Column, ForeignKey, Integer, String, text, 2 | UniqueConstraint) 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from sqlalchemy.orm import relationship 5 | 6 | from general.database.base import Base 7 | 8 | 9 | class FormFieldSettings(Base): 10 | __tablename__ = 'form_field_settings' 11 | __table_args__ = (UniqueConstraint('user_id', 12 | 'schema_name', 13 | 'form_name', 14 | 'form_field_name', 15 | name='form_field_settings_unique_constraint'), 16 | {'schema': 'admin'}, 17 | ) 18 | 19 | id = Column(UUID, 20 | server_default=text('auth.gen_random_uuid()'), 21 | primary_key=True) 22 | 23 | schema_name = Column(String, nullable=False) 24 | form_name = Column(String, nullable=False) 25 | form_field_name = Column(String, nullable=False) 26 | custom_name = Column(String) 27 | order_index = Column(Integer) 28 | 29 | user_id = Column(UUID, 30 | ForeignKey('auth.users.id', 31 | onupdate='CASCADE', 32 | ondelete='CASCADE'), 33 | nullable=False) 34 | 35 | user = relationship('Users') 36 | -------------------------------------------------------------------------------- /general/domains/admin/models/dialog_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Boolean, 3 | Column, 4 | ForeignKey, 5 | String, 6 | text, 7 | UniqueConstraint 8 | ) 9 | from sqlalchemy.dialects.postgresql import UUID 10 | 11 | from general.database.base import Base 12 | 13 | 14 | class DialogSettings(Base): 15 | __tablename__ = 'dialog_settings' 16 | __table_args__ = (UniqueConstraint('user_id', 17 | 'name', 18 | name='dialog_settings_unique_constraint'), 19 | {'schema': 'admin'}, 20 | ) 21 | 22 | id = Column(UUID, 23 | server_default=text('auth.gen_random_uuid()'), 24 | primary_key=True) 25 | 26 | name = Column(String, nullable=False) 27 | 28 | header = Column(String, default='Dialog') 29 | 30 | is_draggable = Column(Boolean, default=False) 31 | is_resizable = Column(Boolean, default=False) 32 | # Defines if background should be blocked when dialog is displayed. 33 | is_modal = Column(Boolean, default=True) 34 | # Specifies if clicking the modal background should hide the dialog. 35 | is_dismissible = Column(Boolean, default=True) 36 | 37 | user_id = Column(UUID, 38 | ForeignKey('auth.users.id', 39 | onupdate='CASCADE', 40 | ondelete='CASCADE'), 41 | nullable=False) 42 | -------------------------------------------------------------------------------- /general/database/session_scope.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import os 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.engine.url import URL 6 | from sqlalchemy.exc import IntegrityError, ProgrammingError 7 | from sqlalchemy.orm import sessionmaker 8 | 9 | 10 | @contextmanager 11 | def session_scope(echo=False, 12 | raise_integrity_error=True, 13 | raise_programming_error=True, 14 | engine=None): 15 | """Provide a transactional scope around a series of operations.""" 16 | if engine is None: 17 | pg_url = URL.create( 18 | drivername='postgresql+psycopg2', 19 | username=os.environ['PGUSER'], 20 | password=os.environ['PGPASSWORD'], 21 | host=os.environ['PGHOST'], 22 | port=int(os.environ['PGPORT']), 23 | query={}, 24 | database=os.environ['PGDB'] 25 | ) 26 | engine = create_engine(pg_url, echo=echo) 27 | session_maker = sessionmaker(bind=engine) 28 | session = session_maker() 29 | 30 | try: 31 | yield session 32 | session.commit() 33 | except IntegrityError: 34 | session.rollback() 35 | if raise_integrity_error: 36 | raise 37 | except ProgrammingError: 38 | session.rollback() 39 | if raise_programming_error: 40 | raise 41 | except: 42 | session.rollback() 43 | raise 44 | finally: 45 | session.close() 46 | -------------------------------------------------------------------------------- /general/domains/admin/features/all_feature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm.exc import NoResultFound 2 | 3 | from general.database.session_scope import session_scope 4 | from general.domains.admin.models import TableSettings 5 | from general.domains.auth.models import Users 6 | 7 | 8 | def insert_all_feature(): 9 | menubar_view_names = ['menubar'] 10 | schema_name = 'admin' 11 | with session_scope() as session: 12 | for user in session.query(Users).all(): 13 | for menubar_view_name in menubar_view_names: 14 | try: 15 | menubar_view_setting = ( 16 | session.query(TableSettings) 17 | .filter(TableSettings.user_id == user.id) 18 | .filter( 19 | TableSettings.table_name == menubar_view_name) 20 | .filter(TableSettings.schema_name == schema_name) 21 | .one() 22 | ) 23 | except NoResultFound: 24 | menubar_view_setting_data = { 25 | 'schema_name': schema_name, 26 | 'table_name': menubar_view_name, 27 | 'user_id': user.id 28 | } 29 | menubar_view_setting = TableSettings( 30 | **menubar_view_setting_data) 31 | session.add(menubar_view_setting) 32 | session.commit() 33 | menubar_view_setting.is_visible = False 34 | -------------------------------------------------------------------------------- /general/domains/admin/models/form_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, ForeignKey, Integer, String, 2 | UniqueConstraint, text) 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from sqlalchemy.orm import relationship 5 | 6 | from general.database.base import Base 7 | 8 | 9 | class FormSettings(Base): 10 | __tablename__ = 'form_settings' 11 | __table_args__ = (UniqueConstraint('user_id', 12 | 'schema_name', 13 | 'form_name', 14 | name='form_settings_unique_constraint'), 15 | {'schema': 'admin'}, 16 | ) 17 | 18 | id = Column(UUID, 19 | server_default=text('auth.gen_random_uuid()'), 20 | primary_key=True) 21 | schema_name = Column(String, nullable=False) 22 | form_name = Column(String, nullable=False) 23 | 24 | custom_button_copy = Column(String, default='Submit') 25 | custom_name = Column(String) 26 | dialog_settings_id = Column(UUID, ForeignKey('admin.dialog_settings.id')) 27 | icon = Column(String) 28 | is_visible = Column(Boolean, default=True) 29 | order_index = Column(Integer) 30 | submenu_id = Column(UUID, ForeignKey('admin.submenus.id')) 31 | 32 | user_id = Column(UUID, 33 | ForeignKey('auth.users.id', 34 | onupdate='CASCADE', 35 | ondelete='CASCADE'), 36 | nullable=False) 37 | 38 | user = relationship('Users') 39 | -------------------------------------------------------------------------------- /general/domains/auth/api_functions/login.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_login_api_trigger(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP FUNCTION IF EXISTS auth_api.login(email TEXT, password TEXT) CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE FUNCTION 11 | auth_api.login(email TEXT, password TEXT) 12 | RETURNS auth.JWT_TOKEN 13 | LANGUAGE plpgsql 14 | AS $$ 15 | DECLARE 16 | _role NAME; 17 | result auth.JWT_TOKEN; 18 | BEGIN 19 | SELECT auth.authenticate_user_email(email, password) 20 | INTO _role; 21 | 22 | IF _role IS NULL 23 | THEN 24 | RAISE invalid_password 25 | USING MESSAGE = 'Invalid email or password'; 26 | END IF; 27 | 28 | SELECT auth.jwt_sign(row_to_json(r), current_setting('app.jwt_secret')) AS token 29 | FROM ( 30 | SELECT 31 | _role AS role, 32 | email AS email, 33 | extract(EPOCH FROM now()) :: INTEGER + current_setting('app.jwt_hours')::INTEGER * 60 * 60 AS exp 34 | ) r 35 | INTO result; 36 | RETURN result; 37 | END; 38 | $$; 39 | """) 40 | -------------------------------------------------------------------------------- /general/domains/auth/auth_api_schema.py: -------------------------------------------------------------------------------- 1 | from general.database.schema import Schema 2 | from general.database.session_scope import session_scope 3 | from general.domains.auth.models import Users 4 | 5 | from general.domains.auth.api_functions import ( 6 | create_login_api_trigger, 7 | create_logout_api_trigger, 8 | create_token_api_trigger 9 | ) 10 | 11 | 12 | class AuthApiSchema(Schema): 13 | 14 | def __init__(self): 15 | super(AuthApiSchema, self).__init__(name='auth_api') 16 | 17 | @staticmethod 18 | def create_functions(): 19 | create_login_api_trigger() 20 | create_logout_api_trigger() 21 | create_token_api_trigger() 22 | 23 | def grant_auth_privileges(self): 24 | with session_scope() as session: 25 | privileges = { 26 | 'SCHEMA': { 27 | 'auth_api': { 28 | 'USAGE': [u.role for u in session.query(Users).all()] 29 | } 30 | }, 31 | 'FUNCTION': { 32 | 'login(TEXT, TEXT)': { 33 | 'EXECUTE': ['anon'] 34 | }, 35 | 'logout()': { 36 | 'EXECUTE': [u.role for u in session.query(Users).all()] 37 | }, 38 | 'token()': { 39 | 'EXECUTE': ['anon'] 40 | }, 41 | }, 42 | } 43 | self.grant_privileges(self.name, privileges) 44 | 45 | def setup(self): 46 | self.create_schema() 47 | self.create_functions() 48 | self.grant_auth_privileges() 49 | -------------------------------------------------------------------------------- /general/domains/admin/models/table_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, ForeignKey, Integer, String, text, 2 | UniqueConstraint) 3 | from sqlalchemy.dialects.postgresql import UUID 4 | 5 | from general.database.base import Base 6 | 7 | 8 | class TableSettings(Base): 9 | __tablename__ = 'table_settings' 10 | __table_args__ = (UniqueConstraint('user_id', 11 | 'schema_name', 12 | 'table_name', 13 | name='table_settings_unique_constraint'), 14 | {'schema': 'admin'}, 15 | ) 16 | 17 | id = Column(UUID, 18 | server_default=text('auth.gen_random_uuid()'), 19 | primary_key=True) 20 | schema_name = Column(String, nullable=False) 21 | table_name = Column(String, nullable=False) 22 | 23 | can_archive = Column(Boolean, default=False) 24 | can_delete = Column(Boolean, default=False) 25 | can_insert = Column(Boolean, default=False) 26 | can_update = Column(Boolean, default=False) 27 | custom_name = Column(String) 28 | submenu_id = Column(UUID, ForeignKey('admin.submenus.id')) 29 | icon = Column(String) 30 | is_visible = Column(Boolean, default=True) 31 | order_index = Column(Integer) 32 | row_limit = Column(Integer, default=10) 33 | row_offset = Column(Integer, default=0) 34 | sort_column = Column(String) 35 | sort_order = Column(Integer) 36 | 37 | user_id = Column(UUID, 38 | ForeignKey('auth.users.id', 39 | onupdate='CASCADE', 40 | ondelete='CASCADE'), 41 | nullable=False) 42 | -------------------------------------------------------------------------------- /general/domains/admin/views/default_form_settings.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_default_form_settings_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.default_form_settings CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.default_form_settings AS 11 | SELECT coalesce(fs.id, auth.gen_random_uuid()) as id, 12 | u.role as "user", 13 | u.id as user_id, 14 | f.form_name, 15 | f.schema_name, 16 | 17 | coalesce(fs.custom_button_copy, 'Submit') AS custom_button_copy, 18 | coalesce(fs.custom_name, initcap(replace(f.form_name, '_', ' '))) as custom_name, 19 | fs.submenu_id, 20 | coalesce(fs.icon, 'fa-pencil-square-o') AS icon, 21 | coalesce(fs.is_visible, TRUE) AS is_visible, 22 | coalesce(fs.order_index, 99) AS order_index, 23 | row_to_json(ds)::JSONB AS dialog_settings 24 | 25 | FROM auth.users u 26 | LEFT OUTER JOIN admin.forms f 27 | ON TRUE 28 | LEFT OUTER JOIN admin.form_settings fs 29 | ON f.form_name = fs.form_name 30 | AND f.schema_name = fs.schema_name 31 | AND u.id = fs.user_id 32 | LEFT OUTER JOIN admin.dialog_settings ds 33 | ON fs.dialog_settings_id = ds.id 34 | AND fs.user_id = ds.user_id 35 | ORDER BY u.role, f.schema_name, f.form_name; 36 | """) 37 | -------------------------------------------------------------------------------- /general/domains/auth/models/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, DateTime, Integer, String, text 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | from general.database.session_scope import session_scope 6 | 7 | 8 | class Users(Base): 9 | __tablename__ = 'users' 10 | __table_args__ = {'schema': 'auth'} 11 | 12 | id = Column(UUID, 13 | server_default=text('auth.gen_random_uuid()'), 14 | primary_key=True) 15 | email = Column(String, unique=True) 16 | password = Column(String) 17 | role = Column(String, unique=True, nullable=False) 18 | first_name = Column(String) 19 | last_name = Column(String) 20 | active = Column(Boolean, nullable=False, default=True) 21 | confirmed_at = Column(DateTime(timezone=True)) 22 | last_login_at = Column(DateTime(timezone=True)) 23 | current_login_at = Column(DateTime(timezone=True)) 24 | last_login_ip = Column(String) 25 | current_login_ip = Column(String) 26 | login_count = Column(Integer) 27 | 28 | @staticmethod 29 | def create_constraint_triggers_on_users(): 30 | with session_scope() as session: 31 | session.execute(""" 32 | DROP TRIGGER IF EXISTS ensure_user_role_exists 33 | ON auth.users; 34 | CREATE CONSTRAINT TRIGGER ensure_user_role_exists 35 | AFTER INSERT OR UPDATE ON auth.users 36 | FOR EACH ROW 37 | EXECUTE PROCEDURE auth.check_if_role_exists(); 38 | """) 39 | 40 | @staticmethod 41 | def create_triggers_on_users(): 42 | with session_scope() as session: 43 | session.execute(""" 44 | DROP TRIGGER IF EXISTS encrypt_password 45 | ON auth.users; 46 | CREATE TRIGGER encrypt_password 47 | BEFORE INSERT OR UPDATE ON auth.users 48 | FOR EACH ROW 49 | EXECUTE PROCEDURE auth.encrypt_password(); 50 | """) 51 | -------------------------------------------------------------------------------- /general/domains/admin/materialized_views/refresh_trigger.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_materialized_views_refresh_trigger(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | CREATE OR REPLACE FUNCTION admin.refresh_admin_views() 8 | RETURNS event_trigger AS 9 | $BODY$ 10 | BEGIN 11 | REFRESH MATERIALIZED VIEW admin.tables; 12 | REFRESH MATERIALIZED VIEW admin.table_columns; 13 | REFRESH MATERIALIZED VIEW admin.forms; 14 | REFRESH MATERIALIZED VIEW admin.form_fields; 15 | END; 16 | $BODY$ 17 | LANGUAGE plpgsql VOLATILE 18 | COST 100; 19 | """) 20 | 21 | session.execute(""" 22 | DROP EVENT TRIGGER IF EXISTS refresh_admin_views_trigger; 23 | """) 24 | 25 | session.execute(""" 26 | CREATE EVENT TRIGGER refresh_admin_views_trigger 27 | ON ddl_command_end 28 | WHEN tag IN ( 29 | 'ALTER FUNCTION', 30 | 'CREATE FUNCTION', 31 | 'DROP FUNCTION', 32 | 'ALTER VIEW', 33 | 'CREATE VIEW', 34 | 'DROP VIEW', 35 | 'ALTER TABLE', 36 | 'CREATE TABLE', 37 | 'CREATE TABLE AS', 38 | 'DROP TABLE') 39 | EXECUTE PROCEDURE admin.refresh_admin_views(); 40 | """) 41 | 42 | session.execute(""" 43 | REFRESH MATERIALIZED VIEW admin.tables; 44 | REFRESH MATERIALIZED VIEW admin.table_columns; 45 | REFRESH MATERIALIZED VIEW admin.forms; 46 | REFRESH MATERIALIZED VIEW admin.form_fields; 47 | """) 48 | 49 | if __name__ == '__main__': 50 | create_materialized_views_refresh_trigger() 51 | -------------------------------------------------------------------------------- /general/domains/admin/models/mapper_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, ForeignKey, String, UniqueConstraint, text 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from general.database.base import Base 5 | 6 | 7 | class MapperSettings(Base): 8 | __tablename__ = 'mapper_settings' 9 | __table_args__ = (UniqueConstraint('user_id', 10 | 'table_settings_id', 11 | name='mapper_settings_unique_constraint'), 12 | {'schema': 'admin'}, 13 | ) 14 | 15 | id = Column(UUID, 16 | server_default=text('auth.gen_random_uuid()'), 17 | primary_key=True) 18 | table_settings_id = Column(UUID, 19 | ForeignKey('admin.table_settings.id'), 20 | nullable=False) 21 | 22 | title = Column(String) 23 | 24 | # The keyword that the user is currently filtering for 25 | keyword = Column(String) 26 | 27 | # The column that we are filtering with a keyword, it belongs to the same table 28 | # as the table_settings_id above 29 | filter_column_settings_id = Column(UUID, ForeignKey( 30 | 'admin.table_column_settings.id')) 31 | 32 | # The column that has all the items we can map to 33 | mapping_column_settings_id = Column(UUID, ForeignKey( 34 | 'admin.table_column_settings.id')) 35 | 36 | # The column that will store the saved mapping's keyword 37 | saved_keyword_column_settings_id = Column(UUID, ForeignKey( 38 | 'admin.table_column_settings.id')) 39 | 40 | # The column that will store the saved mapping's value 41 | saved_mapping_column_settings_id = Column(UUID, ForeignKey( 42 | 'admin.table_column_settings.id')) 43 | 44 | user_id = Column(UUID, 45 | ForeignKey('auth.users.id', 46 | onupdate='CASCADE', 47 | ondelete='CASCADE'), 48 | nullable=False) 49 | -------------------------------------------------------------------------------- /general/logger.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import os 3 | 4 | import structlog 5 | 6 | 7 | timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S') 8 | pre_chain = [ 9 | # Add the log level and a timestamp to the event_dict if the log entry 10 | # is not from structlog. 11 | structlog.stdlib.add_log_level, 12 | timestamper, 13 | ] 14 | 15 | logging.config.dictConfig({ 16 | 'version': 1, 17 | 'disable_existing_loggers': False, 18 | 'formatters': { 19 | 'plain': { 20 | '()': structlog.stdlib.ProcessorFormatter, 21 | 'processor': structlog.dev.ConsoleRenderer(colors=False), 22 | 'foreign_pre_chain': pre_chain, 23 | }, 24 | 'colored': { 25 | '()': structlog.stdlib.ProcessorFormatter, 26 | 'processor': structlog.dev.ConsoleRenderer(colors=True), 27 | 'foreign_pre_chain': pre_chain, 28 | }, 29 | }, 30 | 'handlers': { 31 | 'default': { 32 | 'level': 'DEBUG', 33 | 'class': 'logging.StreamHandler', 34 | 'formatter': 'colored', 35 | }, 36 | }, 37 | 'loggers': { 38 | '': { 39 | 'handlers': ['default'], 40 | 'level': 'DEBUG', 41 | 'propagate': True, 42 | }, 43 | } 44 | }) 45 | 46 | 47 | def dropper(logger, method_name, event_dict): 48 | for key in event_dict[0][0].keys(): 49 | if 'rpcpass' in key: 50 | event_dict[0][0][key] = 'masked_password' 51 | return event_dict 52 | 53 | 54 | structlog.configure( 55 | processors=[ 56 | structlog.stdlib.add_log_level, 57 | structlog.stdlib.PositionalArgumentsFormatter(), 58 | timestamper, 59 | structlog.processors.StackInfoRenderer(), 60 | structlog.processors.format_exc_info, 61 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 62 | dropper 63 | ], 64 | context_class=dict, 65 | logger_factory=structlog.stdlib.LoggerFactory(), 66 | wrapper_class=structlog.stdlib.BoundLogger, 67 | cache_logger_on_first_use=True, 68 | ) 69 | 70 | log = structlog.get_logger() 71 | -------------------------------------------------------------------------------- /general/domains/admin/scripts/insert_TableSettings.py: -------------------------------------------------------------------------------- 1 | from general.models import Submenus, TableSettings, Users, \ 2 | session_scope 3 | 4 | custom_name_mappings = { 5 | 'form_field_settings': 'Form Fields', 6 | 'form_settings': 'Forms', 7 | 'table_column_settings': 'Table Columns', 8 | 'table_settings': 'Tables', 9 | } 10 | system_table_names = [ 11 | 'datatable', 12 | 'datatable_columns', 13 | 'items', 14 | 'menubar' 15 | ] 16 | 17 | 18 | def insert_table_setting(user: str, name: str, icon: str, ): 19 | with session_scope() as session: 20 | table_names = session.execute('SELECT table_name FROM admin.tables') 21 | 22 | for table_name, in table_names: 23 | submenu_id = None 24 | 25 | if table_name.endswith('settings'): 26 | submenu_name = 'Settings' 27 | submenu_id = (session.query(Submenus.id) 28 | .filter(Submenus.submenu_name == submenu_name) 29 | .filter(Submenus.user == user) 30 | .scalar()) 31 | 32 | if table_name in system_table_names: 33 | is_visible = False 34 | else: 35 | is_visible = True 36 | 37 | custom_name = custom_name_mappings.get(table_name, None) 38 | if custom_name is None: 39 | custom_name = table_name.replace('_', ' ').title() 40 | 41 | new_record_data = { 42 | 'user': user, 43 | 'table_name': table_name, 44 | 'custom_name': custom_name, 45 | 'icon': 'fa-table', 46 | 'submenu_id': str(submenu_id) if submenu_id else None, 47 | 'is_visible': is_visible, 48 | 'order_index': 1, 49 | } 50 | new_record = TableSettings(**new_record_data) 51 | 52 | with session_scope() as session: 53 | session.add(new_record) 54 | 55 | 56 | if __name__ == '__main__': 57 | with session_scope() as session: 58 | roles = session.query(Users.role).all() 59 | roles = [r[0] for r in roles] 60 | roles.append('anon') 61 | for role in roles: 62 | insert_table_settings(role) 63 | -------------------------------------------------------------------------------- /general/domains/admin/scripts/insert_Submenus.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm.exc import NoResultFound 2 | 3 | from general.models import Submenus, TableSettings, Users, \ 4 | session_scope 5 | 6 | 7 | def insert_submenu(name: str, icon: str, order_index: int, items: list): 8 | 9 | with session_scope() as session: 10 | users = session.query(Users.role).all() 11 | users = [r[0] for r in users] 12 | users.append('anon') 13 | 14 | for user in users: 15 | insert_user_submenu(user=user, 16 | name=name, 17 | icon=icon, 18 | order_index=order_index, 19 | items=items) 20 | 21 | 22 | def insert_user_submenu(user: str, name: str, icon: str, order_index: int, 23 | items: list): 24 | submenu_name = name 25 | new_record = Submenus() 26 | new_record.user = user 27 | new_record.submenu_name = submenu_name 28 | new_record.icon = icon 29 | new_record.order_index = order_index 30 | with session_scope() as session: 31 | session.add(new_record) 32 | for item in items: 33 | with session_scope() as session: 34 | submenu_id = ( 35 | session 36 | .query(Submenus.id) 37 | .filter(Submenus.submenu_name == submenu_name) 38 | .filter(Submenus.user == user) 39 | .scalar() 40 | ) 41 | try: 42 | setting = ( 43 | session 44 | .query(TableSettings) 45 | .filter(TableSettings.table_name == item) 46 | .filter(TableSettings.user == user) 47 | .one() 48 | ) 49 | except NoResultFound: 50 | setting = TableSettings() 51 | setting.user = user 52 | setting.table_name = item 53 | session.add(setting) 54 | setting.submenu_id = str(submenu_id) 55 | 56 | 57 | if __name__ == '__main__': 58 | insert_submenu(name='Settings', icon='fa-cogs', order_index=3, 59 | items=[]) 60 | -------------------------------------------------------------------------------- /general/domains/admin/features/user_feature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm.exc import NoResultFound 2 | 3 | from general.database.session_scope import session_scope 4 | from general.domains.admin.models.feature_sets import FeatureSets 5 | from general.domains.admin.models.feature_sets_users import \ 6 | FeatureSetsUsers 7 | from general.domains.admin.models.table_settings import TableSettings 8 | from general.domains.auth.models import Users 9 | 10 | 11 | def insert_user_feature(): 12 | """ 13 | The user feature set is the opposite of the admin feature set. 14 | Hide the admin tables. 15 | """ 16 | schema_name = 'admin' 17 | api_view_names = ['datatable_columns', 18 | 'datatables', 19 | 'form_fields', 20 | 'forms', 21 | 'home'] 22 | with session_scope() as session: 23 | users = ( 24 | session 25 | .query(Users) 26 | .outerjoin(FeatureSetsUsers, 27 | FeatureSetsUsers.user_id == Users.id) 28 | .outerjoin(FeatureSets, 29 | FeatureSetsUsers.feature_set_id == FeatureSets.id) 30 | .filter(FeatureSets.name.is_(None)) 31 | .all() 32 | ) 33 | user_ids = [user.id for user in users] 34 | for user in users: 35 | for api_view_name in api_view_names: 36 | try: 37 | menubar_view_setting = ( 38 | session.query(TableSettings) 39 | .filter(TableSettings.user_id == user.id) 40 | .filter(TableSettings.table_name == api_view_name) 41 | .filter(TableSettings.schema_name == schema_name) 42 | .one() 43 | ) 44 | except NoResultFound: 45 | menubar_view_setting_data = { 46 | 'schema_name': schema_name, 47 | 'table_name': api_view_name, 48 | 'user_id': user.id 49 | } 50 | menubar_view_setting = TableSettings( 51 | **menubar_view_setting_data) 52 | session.add(menubar_view_setting) 53 | session.commit() 54 | menubar_view_setting.is_visible = False 55 | -------------------------------------------------------------------------------- /general/database/schema.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | class Schema(object): 5 | 6 | def __init__(self, name): 7 | self.name = name 8 | 9 | def create_schema(self): 10 | with session_scope(raise_programming_error=True) as session: 11 | session.execute(f""" 12 | CREATE SCHEMA IF NOT EXISTS {self.name}; 13 | """) 14 | 15 | def create_extension(self, extension_name): 16 | with session_scope(raise_programming_error=True) as session: 17 | session.execute(f""" 18 | CREATE EXTENSION IF NOT EXISTS {extension_name} SCHEMA {self.name}; 19 | """) 20 | 21 | @staticmethod 22 | def drop_extension(extension_name): 23 | with session_scope(raise_programming_error=True) as session: 24 | session.execute(f""" 25 | DROP EXTENSION IF EXISTS {extension_name} CASCADE; 26 | """) 27 | 28 | @staticmethod 29 | def grant_privileges(schema_name: str, privilege_definitions: dict): 30 | for db_object_type, objects in privilege_definitions.items(): 31 | for object_name, privileges in objects.items(): 32 | for privilege_name, users in privileges.items(): 33 | for user in users: 34 | with session_scope(raise_programming_error=True) as session: 35 | if db_object_type in ('FUNCTION', 'TABLE'): 36 | session.execute(f'GRANT {privilege_name} ' 37 | f'ON {db_object_type} ' 38 | f'{schema_name}.{object_name}' 39 | f' TO "{user}";') 40 | elif db_object_type == 'VIEW': 41 | session.execute(f'GRANT {privilege_name} ' 42 | f'ON ' 43 | f'{schema_name}.{object_name}' 44 | f' TO "{user}";') 45 | else: 46 | session.execute(f'GRANT {privilege_name} ' 47 | f'ON {db_object_type} ' 48 | f'{object_name}' 49 | f' TO "{user}";') 50 | -------------------------------------------------------------------------------- /general/domains/admin/views/default_datatable_settings.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_default_datatable_settings_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.default_datatable_settings CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.default_datatable_settings AS 11 | SELECT coalesce(ts.id, auth.gen_random_uuid()) as id, 12 | u.role as "user", 13 | u.id as user_id, 14 | t.schema_name, 15 | t.table_name, 16 | 17 | coalesce(ts.can_archive, FALSE) AS can_archive, 18 | coalesce(ts.can_delete, FALSE) AS can_delete, 19 | coalesce(ts.can_insert, FALSE) AS can_insert, 20 | coalesce(ts.can_update, TRUE) AS can_update, 21 | coalesce(ts.custom_name, initcap(replace(t.table_name, '_', ' '))) as custom_name, 22 | ts.submenu_id, 23 | coalesce(ts.icon, 'fa-table') AS icon, 24 | coalesce(ts.is_visible, FALSE) AS is_visible, 25 | coalesce(ts.order_index, 99) AS order_index, 26 | coalesce(ts.row_limit, 10) AS row_limit, 27 | coalesce(ts.row_offset, 0) as row_offset, 28 | coalesce(ts.sort_column, 'id') as sort_column, 29 | coalesce(ts.sort_order, 1) as sort_order, 30 | coalesce(cmi.context_menu_items, '[]') AS context_menu_items 31 | FROM auth.users u 32 | LEFT OUTER JOIN admin.tables t 33 | ON TRUE 34 | LEFT OUTER JOIN admin.table_settings ts 35 | ON t.schema_name = ts.schema_name 36 | AND t.table_name = ts.table_name 37 | AND u.id = ts.user_id 38 | LEFT OUTER JOIN ( 39 | SELECT 40 | cmi.user_id, 41 | cmi.table_name, 42 | cmi.schema_name, 43 | array_to_json(array_agg(row_to_json(cmi)))::JSONB as "context_menu_items" 44 | FROM admin.context_menu_items cmi 45 | GROUP BY 1, 2, 3 46 | ) cmi 47 | ON t.schema_name = cmi.schema_name 48 | AND t.table_name = cmi.table_name 49 | AND u.id = cmi.user_id 50 | ORDER BY u.role, t.schema_name, t.table_name; 51 | """) 52 | -------------------------------------------------------------------------------- /general/domains/admin/views/menubar.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_menubar_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.menubar CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.menubar AS 11 | SELECT 12 | dhs.user, 13 | dhs.id, 14 | dhs.custom_name AS "label", 15 | dhs.icon, 16 | ARRAY['/'] AS "routerLink", 17 | -1 AS order_index, 18 | NULL AS "items" 19 | FROM admin.default_home_settings dhs 20 | UNION 21 | SELECT u.role AS "user", 22 | s.id, 23 | s.submenu_name AS "label", 24 | s.icon, 25 | NULL as "routerLink", 26 | s.order_index, 27 | si."items" as "items" 28 | FROM admin.submenus s 29 | LEFT JOIN auth.users u 30 | ON u.id = s.user_id 31 | LEFT OUTER JOIN ( 32 | SELECT 33 | si.user, 34 | si.submenu_id, 35 | array_to_json(array_agg(row_to_json(si)))::JSONB as "items" 36 | FROM admin.submenu_items si 37 | GROUP BY si.submenu_id, si.user 38 | ) si 39 | ON s.id = si.submenu_id AND si.user = u.role 40 | WHERE s.is_visible 41 | UNION 42 | SELECT 43 | dts.user, 44 | dts.id, 45 | dts.custom_name AS "label", 46 | dts.icon, 47 | ARRAY['/', dts.schema_name, dts.table_name] AS "routerLink", 48 | dts.order_index, 49 | NULL as "items" 50 | FROM admin.default_datatable_settings dts 51 | WHERE dts.submenu_id IS NULL AND dts.is_visible 52 | UNION 53 | SELECT 54 | dfs.user, 55 | dfs.id, 56 | dfs.custom_name AS "label", 57 | dfs.icon, 58 | ARRAY['/', dfs.schema_name, 'rpc', dfs.form_name] AS "routerLink", 59 | dfs.order_index, 60 | NULL as "items" 61 | FROM admin.default_form_settings dfs 62 | WHERE dfs.submenu_id IS NULL AND dfs.is_visible 63 | AND ((dfs.user != 'anon' AND dfs.form_name != 'login') 64 | OR (dfs.user = 'anon' AND dfs.form_name != 'logout')) 65 | 66 | ORDER BY "user" ASC, order_index ASC NULLS LAST, "label" ASC NULLS LAST; 67 | """) 68 | 69 | 70 | if __name__ == '__main__': 71 | create_menubar_view() 72 | -------------------------------------------------------------------------------- /tests/test_REST.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | from pprint import pprint 4 | 5 | import requests 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | 9 | from general.scripts import get_pg_url 10 | 11 | api_path = 'https://api.rochard.org' 12 | test_user_email = 'testing@localhost' 13 | test_user_password = str(uuid.uuid4()).replace('-', '') 14 | 15 | 16 | class TestRestAuth(unittest.TestCase): 17 | session = None 18 | 19 | def setUp(self): 20 | engine = create_engine(get_pg_url(), echo=True) 21 | self.session = scoped_session(sessionmaker(bind=engine, autocommit=False))() 22 | 23 | def test_unauthenticated(self): 24 | response = requests.get(api_path + '/') 25 | pprint(response) 26 | data = dict(label='Test') 27 | new_submenu = requests.post(api_path + '/submenus', data=data) 28 | print(new_submenu.json()) 29 | 30 | test_submenu = requests.get(api_path + '/submenus').json() 31 | test_submenu = [s for s in test_submenu if s['label'] == 'Test'][0] 32 | 33 | data = dict(table_name='messages', 34 | user='anon', 35 | submenu_id=test_submenu['id'], 36 | ) 37 | new_table_setting = requests.post(api_path + '/table_settings', 38 | data=data) 39 | 40 | data = dict(table_name='column_settings', 41 | user='anon', 42 | submenu_id=test_submenu['id'], 43 | ) 44 | new_table_setting = requests.post(api_path + '/table_settings', 45 | data=data) 46 | 47 | params = dict(select="label,items{label, icon, routerLink}") 48 | response = requests.get(api_path + '/submenus', params=params).json() 49 | pprint(response) 50 | 51 | # def test_invalid_login(self): 52 | # data = dict(email='wrong@localhost', password='wrongpassword') 53 | # response = requests.post(api_path + '/rpc/login', data=data).json() 54 | # assert response['code'] == '28P01' 55 | # assert response['message'] == 'Invalid email or password' 56 | 57 | # def test_valid_login(self): 58 | # data = dict(email=testing_email, password=testing_password) 59 | # response = requests.post(api_path + '/rpc/login', data=data).json() 60 | # pprint(response) 61 | # token = response[0]['token'] 62 | # pprint(token) 63 | # headers = {'Authorization': 'Bearer ' + token} 64 | # response = requests.get(api_path + '/', headers=headers).json() 65 | # pprint(response) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /general/domains/admin/models/table_column_settings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import (Boolean, Column, ForeignKey, Numeric, Integer, String, 2 | UniqueConstraint, text) 3 | 4 | from sqlalchemy.dialects.postgresql import UUID 5 | from sqlalchemy.orm import relationship 6 | 7 | from general.database.base import Base 8 | 9 | 10 | class TableColumnSettings(Base): 11 | __tablename__ = 'table_column_settings' 12 | __table_args__ = (UniqueConstraint('user_id', 13 | 'schema_name', 14 | 'table_name', 15 | 'column_name', 16 | name='table_column_settings_unique_constraint'), 17 | {'schema': 'admin'}, 18 | ) 19 | 20 | id = Column(UUID, 21 | server_default=text('auth.gen_random_uuid()'), 22 | primary_key=True) 23 | schema_name = Column(String, nullable=False) 24 | table_name = Column(String, nullable=False) 25 | column_name = Column(String, nullable=False) 26 | 27 | can_update = Column(Boolean) 28 | custom_name = Column(String) 29 | filter_match_mode = Column(String, default='contains') 30 | filter_value = Column(String) 31 | format_pattern = Column(String) 32 | input_type = Column(String, default='text') 33 | is_filterable = Column(Boolean, default=False) 34 | is_multiple = Column(Boolean, default=False) 35 | is_select_item = Column(Boolean, default=False) 36 | is_sortable = Column(Boolean, default=True) 37 | is_visible = Column(Boolean, default=True) 38 | order_index = Column(Integer) 39 | slice_start = Column(Integer, default=None) 40 | slice_end = Column(Integer, default=None) 41 | 42 | # Utility Columns 43 | select_item_label_column_name = Column(String) 44 | select_item_schema_name = Column(String) 45 | select_item_table_name = Column(String) 46 | select_item_value_column_name = Column(String) 47 | 48 | suggestion_column_name = Column(String) 49 | suggestion_schema_name = Column(String) 50 | suggestion_table_name = Column(String) 51 | 52 | # CSS Properties, in pixels 53 | height = Column(Numeric) 54 | overflow = Column(String) 55 | padding_bottom = Column(Numeric) 56 | padding_left = Column(Numeric) 57 | padding_right = Column(Numeric) 58 | padding_top = Column(Numeric) 59 | width = Column(Numeric) 60 | 61 | user_id = Column(UUID, 62 | ForeignKey('auth.users.id', 63 | onupdate='CASCADE', 64 | ondelete='CASCADE'), 65 | nullable=False) 66 | 67 | user = relationship('Users') 68 | -------------------------------------------------------------------------------- /general/domains/auth/auth_schema.py: -------------------------------------------------------------------------------- 1 | from general.database.base import Base 2 | from general.database.schema import Schema 3 | from general.database.session_scope import session_scope 4 | 5 | from general.domains.auth.functions import ( 6 | create_authenticate_user_email_function, 7 | create_check_if_role_exists_function, 8 | create_encrypt_password_function, 9 | create_jwt_algorithm_sign_function, 10 | create_jwt_sign_function, 11 | create_jwt_url_encode_function 12 | ) 13 | from general.domains.auth.models.users import Users 14 | 15 | 16 | class AuthSchema(Schema): 17 | 18 | def __init__(self): 19 | super(AuthSchema, self).__init__(name='auth') 20 | 21 | def create_extensions(self): 22 | self.create_extension('pgcrypto') 23 | 24 | @staticmethod 25 | def create_types(): 26 | with session_scope(raise_programming_error=False) as session: 27 | session.execute(""" 28 | CREATE TYPE auth.jwt_token AS ( 29 | token TEXT 30 | ); 31 | """) 32 | 33 | @staticmethod 34 | def create_tables(): 35 | with session_scope() as session: 36 | Base.metadata.create_all(session.connection()) 37 | 38 | @staticmethod 39 | def create_table_triggers(): 40 | Users.create_triggers_on_users() 41 | Users.create_constraint_triggers_on_users() 42 | 43 | @staticmethod 44 | def create_functions(): 45 | create_authenticate_user_email_function() 46 | create_check_if_role_exists_function() 47 | create_encrypt_password_function() 48 | 49 | create_jwt_url_encode_function() 50 | create_jwt_algorithm_sign_function() 51 | create_jwt_sign_function() 52 | 53 | def grant_auth_privileges(self): 54 | with session_scope(raise_programming_error=True) as session: 55 | privileges = { 56 | 'ALL TABLES IN SCHEMA': { 57 | 'auth': { 58 | 'SELECT, UPDATE, INSERT': [u.role for u in 59 | session.query(Users).all()] 60 | } 61 | }, 62 | 'SCHEMA': { 63 | 'auth': { 64 | 'USAGE': [u.role for u in session.query(Users).all()] 65 | } 66 | }, 67 | 'TABLE': { 68 | 'users': { 69 | 'SELECT': ['anon'] 70 | } 71 | } 72 | } 73 | self.grant_privileges(self.name, privileges) 74 | 75 | def setup(self): 76 | self.create_schema() 77 | self.create_extensions() 78 | self.create_types() 79 | self.create_tables() 80 | self.create_functions() 81 | self.create_table_triggers() 82 | self.grant_auth_privileges() 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![General Insignia][insignia] 2 | 3 | [insignia]: https://github.com/PierreRochard/general/blob/master/general_insignia.png "General Insignia" 4 | 5 | # General 6 | 7 | General is a framework that integrates mature open source libraries to 8 | deliver modern web applications with astounding effectiveness. 9 | 10 | General solves the repetitive problem of building CRUD interfaces 11 | on top of a rich data model. 12 | 13 | General revolves around PostgreSQL, the most powerful and mature relational 14 | database. 15 | 16 | General is a set of API endpoints that provide configuration parameters to 17 | a front end application. 18 | 19 | Configuration parameters are stored in normalized database tables. 20 | 21 | Configuration parameters can be applied to users or groups. 22 | 23 | Currently, configuration parameters encompass the following 24 | front end components: 25 | * Menubar 26 | * Tables 27 | * Forms 28 | 29 | Additional components will be supported as General evolves. 30 | 31 | Examples of configuration parameters include: 32 | * Which tables or forms appear in the menubar and its submenus 33 | * What icon is associated with each table or form 34 | * Custom names for tables and forms 35 | * Which columns or fields are visible/editable 36 | * Which columns the user can filter on 37 | 38 | 39 | ## The stack 40 | 41 | ### SQLAlchemy 42 | 43 | The models for General are developed as Python classes with the SQLAlchemy ORM. 44 | SQLAlchemy is a highly flexible and mature ORM. 45 | 46 | Your application models can use any ORM in any language, 47 | as long as it's compatible with PostgreSQL. 48 | You can also just use raw SQL scripts to develop your data model. 49 | 50 | ### Alembic 51 | 52 | Migrations for General are performed with Alembic as it is built for 53 | SQLAlchemy and performs as advertised. 54 | 55 | For changes to your application schema you can use your ORM's migration manager 56 | or an independent one like Flyway. 57 | 58 | ### PostgreSQL 59 | 60 | PostgreSQL is the most reliable and feature-rich relational database. 61 | 62 | Both General's tables/functions as well as your application's are automatically 63 | detected using the PostgreSQL's system catalogs. This avoids the issue of 64 | repeating yourself, the data model is hard-coded only in one place: your ORM or SQL scripts. 65 | 66 | ### PostgREST 67 | 68 | PostgREST is the service layer of General. It automatically generates 69 | the REST endpoints based on the tables, views, and functions defined inside 70 | the API schema(s). All of the relevant HTTP verbs are implemented and a 71 | Swagger/OpenAPI specification is automatically generated as well. 72 | This avoids the issue of repeating yourself, 73 | the data model is hard-coded only in one place: 74 | your ORM or SQL scripts. (seeing a pattern?) 75 | 76 | ### Nginx 77 | 78 | It is recommended that you place nginx in front of PostgREST configured 79 | as a reverse proxy. -------------------------------------------------------------------------------- /general/domains/admin/features/admin_feature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm.exc import NoResultFound 2 | 3 | from general.database.session_scope import session_scope 4 | from general.domains.admin.models.feature_sets import FeatureSets 5 | from general.domains.admin.models.feature_sets_users import \ 6 | FeatureSetsUsers 7 | from general.domains.admin.models.submenus import Submenus 8 | from general.domains.admin.models.table_settings import TableSettings 9 | from general.domains.auth.models import Users 10 | 11 | 12 | def insert_admin_feature(): 13 | schema_name = 'admin' 14 | submenu_name = 'Settings' 15 | submenu_icon = 'fa-cogs' 16 | api_view_names = [ 17 | 'datatable_columns', 18 | 'datatables', 19 | 'form_fields', 20 | 'forms', 21 | 'home' 22 | ] 23 | 24 | with session_scope() as session: 25 | users = ( 26 | session 27 | .query(Users) 28 | .outerjoin(FeatureSetsUsers, 29 | FeatureSetsUsers.user_id == Users.id) 30 | .outerjoin(FeatureSets, 31 | FeatureSetsUsers.feature_set_id == FeatureSets.id) 32 | .filter(FeatureSets.name == 'admin') 33 | .all() 34 | ) 35 | for user in users: 36 | try: 37 | submenu = ( 38 | session.query(Submenus) 39 | .filter(Submenus.submenu_name == submenu_name) 40 | .filter(Submenus.user_id == user.id) 41 | .one() 42 | ) 43 | except NoResultFound: 44 | submenu = Submenus() 45 | submenu.user_id = user.id 46 | submenu.submenu_name = submenu_name 47 | session.add(submenu) 48 | session.commit() 49 | submenu.icon = submenu_icon 50 | 51 | for api_view_name in api_view_names: 52 | try: 53 | menubar_view_setting = ( 54 | session.query(TableSettings) 55 | .filter(TableSettings.user_id == user.id) 56 | .filter(TableSettings.table_name == api_view_name) 57 | .filter(TableSettings.schema_name == schema_name) 58 | .one() 59 | ) 60 | except NoResultFound: 61 | menubar_view_setting_data = { 62 | 'schema_name': schema_name, 63 | 'table_name': api_view_name, 64 | 'user_id': user.id 65 | } 66 | menubar_view_setting = TableSettings( 67 | **menubar_view_setting_data) 68 | session.add(menubar_view_setting) 69 | session.commit() 70 | menubar_view_setting.submenu_id = submenu.id 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | pyvenv.cfg 12 | .venv 13 | pip-selfcheck.json 14 | ### JetBrains template 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 17 | 18 | # User-specific stuff: 19 | .idea/workspace.xml 20 | .idea/tasks.xml 21 | 22 | # Sensitive or high-churn files: 23 | .idea/dataSources/ 24 | .idea/dataSources.ids 25 | .idea/dataSources.xml 26 | .idea/dataSources.local.xml 27 | .idea/sqlDataSources.xml 28 | .idea/dynamic.xml 29 | .idea/uiDesigner.xml 30 | 31 | # Gradle: 32 | .idea/gradle.xml 33 | .idea/libraries 34 | 35 | # Mongo Explorer plugin: 36 | .idea/mongoSettings.xml 37 | 38 | ## File-based project format: 39 | *.iws 40 | 41 | ## Plugin-specific files: 42 | 43 | # IntelliJ 44 | /out/ 45 | 46 | # mpeltonen/sbt-idea plugin 47 | .idea_modules/ 48 | 49 | # JIRA plugin 50 | atlassian-ide-plugin.xml 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | ### Python template 58 | # Byte-compiled / optimized / DLL files 59 | __pycache__/ 60 | *.py[cod] 61 | *$py.class 62 | 63 | # C extensions 64 | *.so 65 | 66 | # Distribution / packaging 67 | env/ 68 | build/ 69 | develop-eggs/ 70 | dist/ 71 | downloads/ 72 | eggs/ 73 | .eggs/ 74 | lib/ 75 | lib64/ 76 | parts/ 77 | sdist/ 78 | var/ 79 | *.egg-info/ 80 | .installed.cfg 81 | *.egg 82 | 83 | # PyInstaller 84 | # Usually these files are written by a python script from a template 85 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 86 | *.manifest 87 | *.spec 88 | 89 | # Installer logs 90 | pip-log.txt 91 | pip-delete-this-directory.txt 92 | 93 | # Unit test / coverage reports 94 | htmlcov/ 95 | .tox/ 96 | .coverage 97 | .coverage.* 98 | .cache 99 | nosetests.xml 100 | coverage.xml 101 | *,cover 102 | .hypothesis/ 103 | 104 | # Translations 105 | *.mo 106 | *.pot 107 | 108 | # Django stuff: 109 | *.log 110 | local_settings.py 111 | 112 | # Flask stuff: 113 | instance/ 114 | .webassets-cache 115 | 116 | # Scrapy stuff: 117 | .scrapy 118 | 119 | # Sphinx documentation 120 | docs/_build/ 121 | 122 | # PyBuilder 123 | target/ 124 | 125 | # Jupyter Notebook 126 | .ipynb_checkpoints 127 | 128 | # pyenv 129 | .python-version 130 | 131 | # celery beat schedule file 132 | celerybeat-schedule 133 | 134 | # dotenv 135 | .env 136 | 137 | # virtualenv 138 | .venv/ 139 | venv/ 140 | ENV/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | 145 | # Rope project settings 146 | .ropeproject 147 | 148 | .idea 149 | libraries 150 | 151 | alembic/versions 152 | !/general/domains/admin/admin_api_schema.py 153 | !/general/domains/ 154 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from logging.config import fileConfig 4 | from pprint import pformat 5 | 6 | from alembic import context 7 | from sqlalchemy import create_engine 8 | from sqlalchemy.event import listen 9 | from sqlalchemy.engine import Engine 10 | from sqlalchemy.engine.url import URL 11 | from sqlalchemy.sql.schema import SchemaItem, Table 12 | 13 | sys.path.append('.') 14 | 15 | from general.database.util import Base 16 | import general.domains.admin.models 17 | import general.domains.auth.models 18 | 19 | config = context.config 20 | fileConfig(config.config_file_name) 21 | 22 | target_metadata = Base.metadata 23 | 24 | 25 | def before_cursor_execute(conn, cursor, statement, parameters, context, 26 | executemany): 27 | # schema_names = ['admin', 'auth', ] 28 | # schema_names = ','.join(schema_names) 29 | # statement = f'SET search_path TO {schema_names}; {statement}' 30 | # print(statement) 31 | return statement, parameters 32 | 33 | 34 | def include_object(object_, name, type_, reflected, compare_to): 35 | schemas = ['admin', 'auth'] 36 | if str(object_).split('.')[0] in schemas: 37 | return True 38 | else: 39 | if isinstance(object_, Table): 40 | return False 41 | if object_.table.schema in schemas: 42 | return True 43 | else: 44 | print('-----') 45 | print(type(object_)) 46 | print(object_) 47 | print(object_.table.schema) 48 | print(pformat(dir(object_))) 49 | print(pformat(dir(object_.table))) 50 | print(name) 51 | print(type_) 52 | print(reflected) 53 | print(compare_to) 54 | print('-----') 55 | raise Exception() 56 | 57 | 58 | def get_url(): 59 | return URL(drivername='postgresql+psycopg2', 60 | username=os.environ['PGUSER'], 61 | password=os.environ['PGPASSWORD'], 62 | host=os.environ['PGHOST'], 63 | port=os.environ['PGPORT'], 64 | database=os.environ['PGDATABASE'], 65 | query=None) 66 | 67 | 68 | def run_migrations_offline(): 69 | url = get_url() 70 | context.configure(url=url, 71 | target_metadata=target_metadata, 72 | literal_binds=True) 73 | 74 | with context.begin_transaction(): 75 | context.run_migrations() 76 | 77 | 78 | listen(Engine, 'before_cursor_execute', before_cursor_execute, retval=True) 79 | 80 | 81 | def run_migrations_online(): 82 | engine = create_engine(get_url()) 83 | 84 | with engine.connect() as connection: 85 | context.configure(connection=connection, 86 | include_schemas=True, 87 | include_object=include_object, 88 | target_metadata=target_metadata) 89 | 90 | with context.begin_transaction(): 91 | context.run_migrations() 92 | 93 | 94 | if context.is_offline_mode(): 95 | run_migrations_offline() 96 | else: 97 | run_migrations_online() 98 | -------------------------------------------------------------------------------- /general/database/setup_notifications.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def setup_table_notifications(schema, table): 5 | with session_scope() as session: 6 | session.execute(''' 7 | CREATE OR REPLACE FUNCTION table_notify() RETURNS TRIGGER AS $$ 8 | DECLARE 9 | id UUID; 10 | payload TEXT; 11 | json_record JSON; 12 | payload_size INT; 13 | BEGIN 14 | IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN 15 | id = NEW.id; 16 | json_record = row_to_json(NEW); 17 | 18 | -- Creates a DIFF from the OLD row to NEW row on updates, and create a change feed 19 | -- ELSEIF TG_OP = 'UPDATE' THEN 20 | -- id = NEW.id; 21 | -- json_record = jsonb_diff_val(row_to_json(NEW)::JSONB, row_to_json(OLD)::JSONB); 22 | 23 | ELSE 24 | id = OLD.id; 25 | json_record = row_to_json(OLD); 26 | END IF; 27 | payload = json_build_object('table_name', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, 'id', id, 'type', TG_OP, 'row', json_record)::TEXT; 28 | payload_size = octet_length(payload); 29 | IF payload_size >= 8000 THEN 30 | payload = json_build_object('table_name', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, 'id', id, 'type', TG_OP)::TEXT; 31 | END IF; 32 | PERFORM pg_notify('{schema}_{table}', payload); 33 | RETURN NEW; 34 | END; 35 | $$ LANGUAGE plpgsql; 36 | '''.format(schema=schema, table=table)) 37 | 38 | session.execute(''' 39 | DROP TRIGGER IF EXISTS messages_notify_update ON {schema}.{table}; 40 | CREATE TRIGGER messages_notify_update 41 | AFTER UPDATE ON {schema}.{table} 42 | FOR EACH ROW EXECUTE PROCEDURE table_notify(); 43 | 44 | DROP TRIGGER IF EXISTS messages_notify_insert ON {schema}.{table}; 45 | CREATE TRIGGER messages_notify_insert 46 | AFTER INSERT ON {schema}.{table} 47 | FOR EACH ROW EXECUTE PROCEDURE table_notify(); 48 | 49 | DROP TRIGGER IF EXISTS messages_notify_delete ON {schema}.{table}; 50 | CREATE TRIGGER messages_notify_delete 51 | AFTER DELETE ON {schema}.{table} 52 | FOR EACH ROW EXECUTE PROCEDURE table_notify(); 53 | '''.format(schema=schema, table=table)) 54 | 55 | 56 | def install_json_diff_function(): 57 | with session_scope() as session: 58 | session.execute(""" 59 | CREATE OR REPLACE FUNCTION jsonb_diff_val(val1 JSONB,val2 JSONB) 60 | RETURNS JSONB AS $$ 61 | DECLARE 62 | result JSONB; 63 | v RECORD; 64 | BEGIN 65 | result = val1; 66 | FOR v IN SELECT * FROM jsonb_each(val2) LOOP 67 | IF result @> jsonb_build_object(v.key,v.value) 68 | THEN result = result - v.key; 69 | ELSIF result ? v.key THEN CONTINUE; 70 | ELSE 71 | result = result || jsonb_build_object(v.key,'null'); 72 | END IF; 73 | END LOOP; 74 | RETURN result; 75 | END; 76 | $$ LANGUAGE plpgsql; 77 | """) 78 | -------------------------------------------------------------------------------- /general/domains/admin/views/default_datatable_column_settings.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_default_datatable_column_settings_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin.default_datatable_column_settings CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin.default_datatable_column_settings AS 11 | SELECT coalesce(tcs.id, auth.gen_random_uuid()) as id, 12 | u.role as "user", 13 | u.id as user_id, 14 | tc.schema_name, 15 | tc.table_name, 16 | tc.column_name, 17 | 18 | tc.is_nullable, 19 | tc.column_default, 20 | tc.data_type, 21 | 22 | coalesce(tcs.can_update, FALSE) as can_update, 23 | coalesce(tcs.custom_name, initcap(replace(tc.column_name, '_', ' '))) as custom_name, 24 | coalesce(tcs.filter_match_mode, 'contains') as filter_match_mode, 25 | tcs.filter_value, 26 | coalesce(tcs.format_pattern, 27 | CASE WHEN tc.data_type = 'timestamp without time zone' THEN 'shortDate' 28 | WHEN tc.data_type = 'timestamp with time zone' THEN 'short' 29 | WHEN tc.data_type = 'numeric' THEN '1.2-2' 30 | ELSE NULL 31 | END) as format_pattern, 32 | coalesce(tcs.input_type, 'text') as input_type, 33 | coalesce(tcs.is_filterable, FALSE) as is_filterable, 34 | coalesce(tcs.is_multiple, FALSE) as is_multiple, 35 | coalesce(tcs.is_select_item, FALSE) as is_select_item, 36 | coalesce(tcs.is_sortable, TRUE) as is_sortable, 37 | coalesce(tcs.is_visible, TRUE) as is_visible, 38 | tcs.slice_start, 39 | tcs.slice_end, 40 | tcs.select_item_label_column_name, 41 | tcs.select_item_schema_name, 42 | tcs.select_item_table_name, 43 | tcs.select_item_value_column_name, 44 | tcs.suggestion_column_name, 45 | tcs.suggestion_schema_name, 46 | tcs.suggestion_table_name, 47 | coalesce(tcs.order_index, 99) as order_index, 48 | CASE WHEN tcs.height IS NULL THEN 'auto' ELSE concat(tcs.height, 'px') END as height, 49 | coalesce(tcs.overflow, 'visible') as overflow, 50 | CASE WHEN tcs.padding_bottom IS NULL THEN 'auto' ELSE concat(tcs.padding_bottom, 'px') END as padding_bottom, 51 | CASE WHEN tcs.padding_left IS NULL THEN 'auto' ELSE concat(tcs.padding_left, 'px') END as padding_left, 52 | CASE WHEN tcs.padding_right IS NULL THEN 'auto' ELSE concat(tcs.padding_right, 'px') END as padding_right, 53 | CASE WHEN tcs.padding_top IS NULL THEN 'auto' ELSE concat(tcs.padding_top, 'px') END as padding_top, 54 | concat(coalesce(tcs.width, 200), 'px') AS width 55 | FROM auth.users u 56 | LEFT OUTER JOIN admin.table_columns tc 57 | ON TRUE 58 | LEFT OUTER JOIN admin.table_column_settings tcs 59 | ON tc.schema_name = tcs.schema_name 60 | AND tc.table_name = tcs.table_name 61 | AND tc.column_name = tcs.column_name 62 | AND u.id = tcs.user_id 63 | ORDER BY u.role, tc.schema_name, tc.table_name 64 | """) 65 | -------------------------------------------------------------------------------- /general/domains/admin/admin_api_schema.py: -------------------------------------------------------------------------------- 1 | from general.database.schema import Schema 2 | from general.database.session_scope import session_scope 3 | 4 | from general.domains.admin.api_views import ( 5 | create_datatable_columns_trigger, 6 | create_datatable_columns_view, 7 | create_datatables_trigger, 8 | create_datatables_view, 9 | create_form_fields_view, 10 | create_forms_view, 11 | create_home_view, 12 | create_menubar_view 13 | ) 14 | from general.domains.admin.features import insert_admin_feature 15 | from general.domains.admin.features.all_feature import insert_all_feature 16 | from general.domains.admin.features.user_feature import insert_user_feature 17 | 18 | 19 | class AdminApiSchema(Schema): 20 | def __init__(self): 21 | super(AdminApiSchema, self).__init__(name='admin_api') 22 | 23 | @staticmethod 24 | def create_admin_api_views(): 25 | """ 26 | Create API views that are specifically designed to be consumed by 27 | frontend components 28 | """ 29 | 30 | # The frontend consumes the menubar endpoint to parameterize the 31 | # PrimeNG menubar component 32 | create_menubar_view() 33 | 34 | # The frontend consumes the datatable_columns endpoint to parameterize the 35 | # PrimeNG datatable component's columns 36 | create_datatable_columns_view() 37 | create_datatable_columns_trigger() 38 | 39 | # The frontend consumes the datatable endpoint to parameterize the 40 | # PrimeNG datatable component 41 | create_datatables_view() 42 | create_datatables_trigger() 43 | 44 | create_forms_view() 45 | create_form_fields_view() 46 | 47 | create_home_view() 48 | 49 | def grant_admin_privileges(self): 50 | from general.domains.auth.models import Users 51 | with session_scope() as session: 52 | privileges = { 53 | 'SCHEMA': { 54 | 'admin_api': { 55 | 'USAGE': [u.role for u in session.query(Users).all()] 56 | } 57 | }, 58 | 'VIEW': { 59 | 'menubar': { 60 | 'SELECT': [u.role for u in session.query(Users).all()] 61 | }, 62 | 'forms': { 63 | 'SELECT': [u.role for u in session.query(Users).all()], 64 | 'UPDATE': [u.role for u in session.query(Users).all() 65 | if u.role != 'anon'] 66 | }, 67 | 'form_fields': { 68 | 'SELECT': [u.role for u in session.query(Users).all()], 69 | 'UPDATE': [u.role for u in session.query(Users).all() 70 | if u.role != 'anon'] 71 | }, 72 | 'datatables': { 73 | 'SELECT': [u.role for u in session.query(Users).all()], 74 | 'UPDATE': [u.role for u in session.query(Users).all() 75 | # if u.role != 'anon' 76 | ] 77 | }, 78 | 'datatable_columns': { 79 | 'SELECT': [u.role for u in session.query(Users).all()], 80 | 'UPDATE': [u.role for u in session.query(Users).all() 81 | # if u.role != 'anon' 82 | ] 83 | }, 84 | 'home': { 85 | 'SELECT': [u.role for u in session.query(Users).all()], 86 | 'UPDATE': [u.role for u in session.query(Users).all() 87 | if u.role != 'anon'] 88 | }, 89 | } 90 | } 91 | self.grant_privileges(self.name, privileges) 92 | 93 | @staticmethod 94 | def insert_feature_records(): 95 | insert_admin_feature() 96 | insert_all_feature() 97 | insert_user_feature() 98 | 99 | def setup(self): 100 | self.create_schema() 101 | self.create_admin_api_views() 102 | self.grant_admin_privileges() 103 | self.insert_feature_records() 104 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/datatable_columns.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_datatable_columns_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin_api.datatable_columns CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin_api.datatable_columns AS 11 | SELECT (row_number() OVER())::INT id, * 12 | FROM ( 13 | SELECT 14 | dtcs.can_update, 15 | dtcs.column_name, 16 | dtcs.custom_name, 17 | dtcs.data_type, 18 | dtcs.filter_match_mode, 19 | dtcs.filter_value, 20 | dtcs.format_pattern, 21 | dtcs.input_type, 22 | dtcs.is_filterable, 23 | dtcs.is_multiple, 24 | dtcs.is_select_item, 25 | dtcs.is_sortable, 26 | dtcs.is_visible, 27 | dtcs.schema_name, 28 | dtcs.select_item_schema_name, 29 | dtcs.select_item_table_name, 30 | dtcs.select_item_label_column_name, 31 | dtcs.select_item_value_column_name, 32 | dtcs.slice_end, 33 | dtcs.slice_start, 34 | dtcs.suggestion_column_name, 35 | dtcs.suggestion_schema_name, 36 | dtcs.suggestion_table_name, 37 | dtcs.table_name, 38 | dtcs.user_id, 39 | json_build_object( 40 | 'height', dtcs.height, 41 | 'overflow', dtcs.overflow, 42 | 'padding-bottom', dtcs.padding_bottom, 43 | 'padding-left', dtcs.padding_left, 44 | 'padding-right', dtcs.padding_right, 45 | 'padding-top', dtcs.padding_top, 46 | 'width', dtcs.width 47 | ) AS styles 48 | 49 | FROM admin.default_datatable_column_settings dtcs 50 | WHERE dtcs.user = current_user 51 | ORDER BY dtcs.order_index ASC 52 | ) sub; 53 | """) 54 | 55 | 56 | def create_datatable_columns_trigger(): 57 | with session_scope() as session: 58 | session.execute(""" 59 | DROP FUNCTION IF EXISTS admin.datatable_columns_function() CASCADE; 60 | """) 61 | session.execute(""" 62 | CREATE OR REPLACE FUNCTION admin.datatable_columns_function() 63 | RETURNS TRIGGER AS 64 | $BODY$ 65 | BEGIN 66 | IF TG_OP = 'UPDATE' THEN 67 | INSERT INTO admin.table_column_settings 68 | (table_name, 69 | column_name, 70 | is_visible, 71 | can_update) 72 | VALUES 73 | (NEW.table_name, 74 | NEW.value, 75 | NEW.is_visible, 76 | NEW.editable) 77 | ON CONFLICT ("user", table_name, column_name) 78 | DO UPDATE SET is_visible = NEW.is_visible, 79 | can_update = NEW.editable 80 | WHERE admin.table_column_settings.user = current_user 81 | AND admin.table_column_settings.table_name = NEW.table_name 82 | AND admin.table_column_settings.column_name = NEW.value; 83 | RETURN NEW; 84 | END IF; 85 | RETURN NEW; 86 | END; 87 | $BODY$ 88 | LANGUAGE plpgsql VOLATILE 89 | COST 100; 90 | """) 91 | 92 | session.execute(""" 93 | DROP TRIGGER IF EXISTS datatable_columns_trigger ON admin_api.datatable_columns CASCADE; 94 | """) 95 | session.execute(""" 96 | CREATE TRIGGER datatable_columns_trigger 97 | INSTEAD OF INSERT OR UPDATE OR DELETE 98 | ON admin_api.datatable_columns 99 | FOR EACH ROW 100 | EXECUTE PROCEDURE admin.datatable_columns_function(); 101 | """) 102 | 103 | if __name__ == '__main__': 104 | create_datatable_columns_view() 105 | create_datatable_columns_trigger() 106 | -------------------------------------------------------------------------------- /general/domains/admin/api_views/datatables.py: -------------------------------------------------------------------------------- 1 | from general.database.session_scope import session_scope 2 | 3 | 4 | def create_datatables_view(): 5 | with session_scope() as session: 6 | session.execute(""" 7 | DROP VIEW IF EXISTS admin_api.datatables CASCADE; 8 | """) 9 | session.execute(""" 10 | CREATE OR REPLACE VIEW admin_api.datatables AS 11 | SELECT (row_number() OVER())::INT id, * 12 | FROM ( 13 | SELECT 14 | dts.can_archive, 15 | dts.can_delete, 16 | dts.custom_name, 17 | dts.order_index, 18 | dts.row_limit, 19 | dts.row_offset, 20 | dts.schema_name, 21 | dts.sort_column, 22 | dts.sort_order, 23 | dts.table_name, 24 | dts.user_id, 25 | dts.context_menu_items, 26 | map.mapper_settings 27 | FROM admin.default_datatable_settings dts 28 | LEFT OUTER JOIN ( 29 | SELECT 30 | mq.table_settings_id, 31 | row_to_json(mq)::JSONB AS "mapper_settings" 32 | FROM 33 | ( 34 | SELECT 35 | ms.table_settings_id, 36 | row_to_json(fcdc) AS filter_column, 37 | row_to_json(mcdc) AS mapping_column, 38 | row_to_json(smcdc) AS saved_keyword_column, 39 | row_to_json(skdc) AS saved_mapping_column 40 | FROM ADMIN.mapper_settings MS 41 | LEFT JOIN ADMIN.default_datatable_column_settings fcdc 42 | ON fcdc.id = MS.filter_column_settings_id 43 | LEFT JOIN ADMIN.default_datatable_column_settings mcdc 44 | ON mcdc.id = MS.mapping_column_settings_id 45 | LEFT JOIN ADMIN.default_datatable_column_settings smcdc 46 | ON smcdc.id = MS.saved_mapping_column_settings_id 47 | LEFT JOIN ADMIN.default_datatable_column_settings skdc 48 | ON skdc.id = MS.saved_keyword_column_settings_id 49 | ) mq 50 | ) map 51 | ON dts.id = map.table_settings_id 52 | WHERE dts.user = current_user 53 | ) sub; 54 | """) 55 | 56 | 57 | def create_datatables_trigger(): 58 | with session_scope() as session: 59 | session.execute(""" 60 | DROP FUNCTION IF EXISTS admin.datatables_function() CASCADE; 61 | """) 62 | 63 | session.execute(""" 64 | CREATE OR REPLACE FUNCTION admin.datatables_function() 65 | RETURNS TRIGGER AS 66 | $BODY$ 67 | BEGIN 68 | IF TG_OP = 'UPDATE' THEN 69 | INSERT INTO admin.table_settings ( 70 | custom_name, 71 | order_index, 72 | row_limit, 73 | row_offset, 74 | schema_name, 75 | sort_column, 76 | sort_order, 77 | table_name, 78 | user_id 79 | ) 80 | VALUES ( 81 | NEW.custom_name, 82 | NEW.order_index, 83 | NEW.row_limit, 84 | NEW.row_offset, 85 | NEW.schema_name, 86 | NEW.sort_column, 87 | NEW.sort_order, 88 | NEW.table_name, 89 | NEW.user_id 90 | ) 91 | ON CONFLICT (user_id, schema_name, table_name) 92 | DO UPDATE SET 93 | row_offset=NEW.row_offset, 94 | sort_column=NEW.sort_column, 95 | sort_order=NEW.sort_order 96 | WHERE admin.table_settings.user_id = NEW.user_id 97 | AND admin.table_settings.table_name = NEW.table_name 98 | AND admin.table_settings.schema_name = NEW.schema_name; 99 | RETURN NEW; 100 | END IF; 101 | RETURN NEW; 102 | END; 103 | $BODY$ 104 | LANGUAGE plpgsql VOLATILE 105 | COST 100; 106 | """) 107 | 108 | session.execute(""" 109 | DROP TRIGGER IF EXISTS datatables_trigger ON admin_api.datatables; 110 | """) 111 | 112 | session.execute(""" 113 | CREATE TRIGGER datatables_trigger 114 | INSTEAD OF INSERT OR UPDATE OR DELETE 115 | ON admin_api.datatables 116 | FOR EACH ROW 117 | EXECUTE PROCEDURE admin.datatables_function(); 118 | """) 119 | -------------------------------------------------------------------------------- /general/domains/admin/admin_schema.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy.exc import ProgrammingError 4 | from sqlalchemy.orm.exc import NoResultFound 5 | 6 | from general.database.base import Base 7 | from general.database.schema import Schema 8 | from general.database.session_scope import session_scope 9 | 10 | from general.domains.admin.materialized_views import ( 11 | create_form_fields_materialized_view, 12 | create_forms_materialized_view, 13 | create_materialized_views_refresh_trigger, 14 | create_schemas_materialized_view, 15 | create_table_columns_materialized_view, 16 | create_tables_materialized_view 17 | ) 18 | from general.domains.admin.views import ( 19 | create_default_datatable_column_settings_view, 20 | create_default_datatable_settings_view, 21 | create_default_form_field_settings_view, 22 | create_default_form_settings_view, 23 | create_default_home_settings_view, 24 | create_menubar_view, 25 | create_submenu_items_view 26 | ) 27 | 28 | 29 | class AdminSchema(Schema): 30 | 31 | def __init__(self): 32 | super(AdminSchema, self).__init__(name='admin') 33 | 34 | @staticmethod 35 | def create_tables(): 36 | import general.domains.admin.models 37 | with session_scope() as session: 38 | Base.metadata.create_all(session.connection()) 39 | 40 | @staticmethod 41 | def create_materialized_views(): 42 | """ 43 | Materialized views that pull from system tables and a 44 | refresh trigger for to keep the data fresh 45 | """ 46 | create_form_fields_materialized_view() 47 | create_forms_materialized_view() 48 | create_schemas_materialized_view() 49 | create_table_columns_materialized_view() 50 | create_tables_materialized_view() 51 | create_materialized_views_refresh_trigger() 52 | 53 | @staticmethod 54 | def create_admin_views(): 55 | """ 56 | Base views introduce sensible defaults 57 | and limit access to the current user 58 | """ 59 | create_default_datatable_column_settings_view() 60 | create_default_datatable_settings_view() 61 | create_default_form_field_settings_view() 62 | create_default_form_settings_view() 63 | create_default_home_settings_view() 64 | create_submenu_items_view() 65 | create_menubar_view() 66 | 67 | @staticmethod 68 | def insert_anon(): 69 | from general.domains.auth.models import Users 70 | with session_scope() as session: 71 | try: 72 | session.execute(""" 73 | CREATE ROLE anon noinherit; 74 | """) 75 | except ProgrammingError: 76 | pass 77 | with session_scope() as session: 78 | try: 79 | user = ( 80 | session.query(Users) 81 | .filter(Users.role == 'anon') 82 | .one() 83 | ) 84 | except NoResultFound: 85 | user = Users() 86 | user.role = 'anon' 87 | user.active = True 88 | session.add(user) 89 | 90 | @staticmethod 91 | def insert_admin_feature(): 92 | from general.domains.admin.models.feature_sets import FeatureSets 93 | from general.domains.admin.models.feature_sets_users import FeatureSetsUsers 94 | from general.domains.auth.models.users import Users 95 | with session_scope(raise_integrity_error=False) as session: 96 | new_feature_set = FeatureSets() 97 | new_feature_set.name = 'admin' 98 | session.add(new_feature_set) 99 | 100 | with session_scope(raise_integrity_error=False) as session: 101 | admin_feature_set = ( 102 | session.query(FeatureSets) 103 | .filter(FeatureSets.name == 'admin') 104 | .one() 105 | ) 106 | user_role = os.environ['PGUSER'] 107 | try: 108 | user = ( 109 | session.query(Users) 110 | .filter(Users.role == user_role) 111 | .one() 112 | ) 113 | except NoResultFound: 114 | user = Users() 115 | user.role = user_role 116 | user.active = True 117 | session.add(user) 118 | session.commit() 119 | new_feature_sets_users = FeatureSetsUsers() 120 | new_feature_sets_users.user_id = user.id 121 | new_feature_sets_users.feature_set_id = admin_feature_set.id 122 | session.add(new_feature_sets_users) 123 | 124 | def grant_admin_privileges(self): 125 | from general.domains.auth.models import Users 126 | with session_scope() as session: 127 | privileges = { 128 | 'ALL TABLES IN SCHEMA': { 129 | 'admin': { 130 | 'SELECT, UPDATE, INSERT': [u.role for u in 131 | session.query(Users).all()] 132 | } 133 | }, 134 | 'SCHEMA': { 135 | 'admin': { 136 | 'USAGE': [u.role for u in session.query(Users).all()] 137 | } 138 | }, 139 | } 140 | self.grant_privileges(self.name, privileges) 141 | 142 | def setup(self): 143 | self.create_schema() 144 | self.create_tables() 145 | self.create_materialized_views() 146 | self.create_admin_views() 147 | self.insert_anon() 148 | self.insert_admin_feature() 149 | self.grant_admin_privileges() 150 | --------------------------------------------------------------------------------