Skip to content

Commit 423bf2f

Browse files
Arfeyjettify
authored andcommitted
Feat add react admin (#358)
* started to implement integration with rest-on-admin * added support json and boolean fields * fixed flake8 * feat: added logout handler * feat: added pre-commit to build new bundle in production mode for each commit * feat: added pre-commit to build new bundle in production mode for each commit * feat: added new bundle
1 parent f294f15 commit 423bf2f

47 files changed

Lines changed: 7916 additions & 521 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.babelrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"presets":[
3+
"react", "es2015", "stage-0"
4+
]
5+
}

.eslintrc

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
{
2-
"env": {
3-
"browser": true,
4-
"es6": true
5-
},
6-
"extends": "eslint:recommended",
7-
"rules": {
8-
"indent": [
9-
"error",
10-
4
11-
],
12-
"linebreak-style": [
13-
"error",
14-
"unix"
15-
],
16-
"quotes": [
17-
"error",
18-
"single"
19-
],
20-
"semi": [
21-
"error",
22-
"always"
23-
]
2+
"env": {
3+
"browser": true,
4+
"es6": true
5+
},
6+
"parserOptions": {
7+
"sourceType": "module",
8+
"ecmaFeatures": {
9+
"jsx": true,
10+
"modules": true
2411
}
25-
}
12+
},
13+
"extends": [
14+
"eslint:recommended",
15+
"plugin:react/recommended"
16+
],
17+
"plugins": [
18+
"react"
19+
],
20+
"rules": {
21+
"indent": [
22+
"error",
23+
2
24+
],
25+
"linebreak-style": [
26+
"error",
27+
"unix"
28+
],
29+
"quotes": [
30+
"error",
31+
"single"
32+
],
33+
"semi": [
34+
"error",
35+
"always"
36+
]
37+
}
38+
}

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ env/
1111
pyvenv/
1212
build/
1313
develop-eggs/
14-
dist/
1514
downloads/
1615
eggs/
1716
lib/

aiohttp_admin/__init__.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
import jinja2
33
from aiohttp import web
44

5-
6-
from .admin import AdminHandler, setup_admin_handlers
7-
from .consts import PROJ_ROOT, TEMPLATE_APP_KEY, APP_KEY
5+
from .admin import (
6+
AdminHandler,
7+
setup_admin_handlers,
8+
setup_admin_on_rest_handlers,
9+
AdminOnRestHandler,
10+
)
11+
from .consts import PROJ_ROOT, TEMPLATE_APP_KEY, APP_KEY, TEMPLATES_ROOT
812
from .security import Permissions, require, authorize
913
from .utils import gather_template_folders
1014

1115

1216
__all__ = ['AdminHandler', 'setup', 'get_admin', 'Permissions', 'require',
13-
'authorize']
17+
'authorize', '_setup', ]
1418
__version__ = '0.0.2'
1519

1620

@@ -37,5 +41,34 @@ def setup(app, admin_conf_path, *, resources, static_folder=None,
3741
return admin
3842

3943

44+
def _setup(app, *, schema, title=None, app_key=APP_KEY, db=None):
45+
"""Initialize the admin-on-rest admin"""
46+
47+
admin = web.Application(loop=app.loop)
48+
app[app_key] = admin
49+
loader = jinja2.FileSystemLoader([TEMPLATES_ROOT, ])
50+
aiohttp_jinja2.setup(admin, loader=loader, app_key=TEMPLATE_APP_KEY)
51+
52+
if title:
53+
schema.title = title
54+
55+
resources = [
56+
init(db, info['table'], url=info['url'])
57+
for init, info in schema.resources
58+
]
59+
60+
admin_handler = AdminOnRestHandler(
61+
admin,
62+
resources=resources,
63+
loop=app.loop,
64+
schema=schema,
65+
)
66+
67+
admin['admin_handler'] = admin_handler
68+
setup_admin_on_rest_handlers(admin, admin_handler)
69+
70+
return admin
71+
72+
4073
def get_admin(app, *, app_key=APP_KEY):
4174
return app.get(app_key)

aiohttp_admin/admin.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
from aiohttp_security import remember, forget
33
from yarl import URL
44

5-
from .consts import TEMPLATE_APP_KEY
5+
from .consts import TEMPLATE_APP_KEY, PROJ_ROOT
66
from .exceptions import JsonValidaitonError
77
from .security import authorize
88
from .utils import json_response, validate_payload, LoginForm
99

1010

11-
__all__ = ['AdminHandler', 'setup_admin_handlers']
11+
__all__ = [
12+
'AdminHandler', 'setup_admin_handlers', 'setup_admin_on_rest_handlers',
13+
'AdminOnRestHandler',
14+
]
1215

1316

1417
class AdminHandler:
@@ -80,3 +83,78 @@ def setup_admin_handlers(admin, admin_handler, static_folder, admin_conf_path):
8083
add_route('DELETE', '/logout', a.logout, name='admin.logout')
8184
add_static('/static', path=static_folder, name='admin.static')
8285
add_static('/config', path=admin_conf_path, name='admin.config')
86+
87+
88+
class AdminOnRestHandler:
89+
90+
template = 'admin_on_rest.jinja2'
91+
92+
def __init__(self, admin, *, resources, loop, schema):
93+
self._admin = admin
94+
self._loop = loop
95+
self.schema = schema
96+
97+
for r in resources:
98+
r.setup(self._admin, URL('/'))
99+
self._resources = tuple(resources)
100+
101+
@property
102+
def resources(self):
103+
return self._resources
104+
105+
async def index_page(self, request):
106+
"""
107+
Return index page with initial state for admin
108+
"""
109+
context = {"initial_state": self.schema.to_json()}
110+
111+
return render_template(
112+
self.template,
113+
request,
114+
context,
115+
app_key=TEMPLATE_APP_KEY,
116+
)
117+
118+
async def token(self, request):
119+
"""
120+
Validation of user data and generate auth token
121+
"""
122+
raw_payload = await request.read()
123+
data = validate_payload(raw_payload, LoginForm)
124+
await authorize(request, data['username'], data['password'])
125+
126+
router = request.app.router
127+
location = router["admin.index"].url_for().human_repr()
128+
payload = {"location": location}
129+
response = json_response(payload)
130+
await remember(request, response, data['username'])
131+
132+
return response
133+
134+
async def logout(self, request):
135+
"""
136+
Simple handler for logout
137+
"""
138+
if "Authorization" not in request.headers:
139+
msg = "Auth header is not present, can not destroy token"
140+
raise JsonValidaitonError(msg)
141+
142+
response = json_response()
143+
await forget(request, response)
144+
145+
return response
146+
147+
148+
def setup_admin_on_rest_handlers(admin, admin_handler):
149+
"""
150+
Initialize routes.
151+
"""
152+
add_route = admin.router.add_route
153+
add_static = admin.router.add_static
154+
static_folder = str(PROJ_ROOT / 'static')
155+
a = admin_handler
156+
157+
add_route('GET', '', a.index_page, name='admin.index')
158+
add_route('POST', '/token', a.token, name='admin.token')
159+
add_static('/static', path=static_folder, name='admin.static')
160+
add_route('DELETE', '/logout', a.logout, name='admin.logout')

aiohttp_admin/backends/sa.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sqlalchemy as sa
2+
from sqlalchemy.dialects import postgresql
23

34
from ..resource import AbstractResource
45
from ..exceptions import ObjectNotFound
@@ -11,6 +12,16 @@
1112
__all__ = ['PGResource', 'MySQLResource']
1213

1314

15+
DATA_TYPES = {
16+
sa.Integer: 'integer',
17+
sa.Text: 'string',
18+
sa.Float: 'number',
19+
sa.Date: 'date',
20+
sa.Boolean: 'bool',
21+
postgresql.JSON: 'json',
22+
}
23+
24+
1425
class PGResource(AbstractResource):
1526

1627
def __init__(self, db, table, primary_key='id', url=None):
@@ -33,6 +44,27 @@ def pool(self):
3344
def table(self):
3445
return self._table
3546

47+
@staticmethod
48+
def get_type_of_fields(fields, table):
49+
"""
50+
Return data types of `fields` that are in `table`.
51+
52+
:param fields: list - list of fields that need to be returned
53+
:param table: sa.Table - the current table
54+
:return: list - list of the tuples `(field_name, fields_type)`
55+
"""
56+
57+
actual_fields = [
58+
field for field in table.c.items() if field[0] in fields
59+
]
60+
61+
data_type_fields = {
62+
name: DATA_TYPES.get(type(field_type.type), 'string')
63+
for name, field_type in actual_fields
64+
}
65+
66+
return data_type_fields
67+
3668
async def list(self, request):
3769
await require(request, Permissions.view)
3870
columns_names = list(self._table.c.keys())

aiohttp_admin/contrib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .admin import Schema # noqa

aiohttp_admin/contrib/admin.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import json
2+
3+
4+
__all__ = ['Schema', ]
5+
6+
7+
class Schema:
8+
"""
9+
The main abstraction for registering tables and presenting data in
10+
admin-on-rest format.
11+
"""
12+
13+
def __init__(self, title='Admin'):
14+
self.title = title
15+
self.endpoints = []
16+
17+
def register(self, Endpoint):
18+
"""
19+
Register a wrapped `ModelAdmin` class as the endpoint for admin page.
20+
21+
@schema.register
22+
class User(admin.ModelAdmin):
23+
pass
24+
25+
"""
26+
self.endpoints.append(Endpoint())
27+
28+
return Endpoint
29+
30+
def to_json(self):
31+
"""
32+
Prepare data for the initial state of the admin-on-rest
33+
"""
34+
endpoints = []
35+
for endpoint in self.endpoints:
36+
list_fields = endpoint.fields
37+
resource_type = endpoint.Meta.resource_type
38+
table = endpoint.Meta.table
39+
40+
data = endpoint.to_dict()
41+
data['fields'] = resource_type.get_type_of_fields(
42+
list_fields,
43+
table,
44+
)
45+
endpoints.append(data)
46+
47+
data = {
48+
'title': self.title,
49+
'endpoints': sorted(endpoints, key=lambda x: x['name']),
50+
}
51+
52+
return json.dumps(data)
53+
54+
@property
55+
def resources(self):
56+
"""
57+
Return list of all registered resources.
58+
"""
59+
resources = []
60+
61+
for endpoint in self.endpoints:
62+
resource_type = endpoint.Meta.resource_type
63+
table = endpoint.Meta.table
64+
url = endpoint.name
65+
66+
resources.append((resource_type, {'table': table, 'url': url}))
67+
68+
return resources

aiohttp_admin/contrib/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class ModelAdmin:
2+
"""
3+
The class provides the possibility of declarative describe of information
4+
about the table and describe all things related to viewing this table on
5+
the administrator's page.
6+
7+
8+
class Users(models.ModelAdmin):
9+
fields = ('id', 'username', )
10+
11+
class Meta:
12+
resource_type = PGResource
13+
table = users
14+
15+
"""
16+
17+
def __init__(self):
18+
self.name = self.__class__.__name__.lower()
19+
20+
def to_dict(self):
21+
"""
22+
Return dict with the all base information about the instance.
23+
"""
24+
25+
data = {"name": self.name}
26+
27+
return data

aiohttp_admin/static/react-admin/dist/bundle.js

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)