Redesign1 #1
9 changed files with 149 additions and 11 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,5 +9,4 @@ inventory/static/uploads/*
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
alembic.ini
|
alembic.ini
|
||||||
alembic/
|
alembic/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
test_app/
|
|
||||||
|
|
@ -5,7 +5,7 @@ def generate_crud_blueprint(model, service):
|
||||||
|
|
||||||
@bp.get('/')
|
@bp.get('/')
|
||||||
def list_items():
|
def list_items():
|
||||||
items = service.list()
|
items = service.list(request.args)
|
||||||
return jsonify([item.as_dict() for item in items])
|
return jsonify([item.as_dict() for item in items])
|
||||||
|
|
||||||
@bp.get('/<int:id>')
|
@bp.get('/<int:id>')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Type, TypeVar, Generic
|
from typing import Type, TypeVar, Generic
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from crudkit.core.spec import CRUDSpec
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
@ -11,8 +12,20 @@ class CRUDService(Generic[T]):
|
||||||
def get(self, id: int) -> T:
|
def get(self, id: int) -> T:
|
||||||
return self.session.get(self.model, id)
|
return self.session.get(self.model, id)
|
||||||
|
|
||||||
def list(self, limit=100, offset=0) -> list[T]:
|
def list(self, params=None) -> list[T]:
|
||||||
return self.session.query(self.model).offset(offset).limit(limit).all()
|
query = self.session.query(self.model)
|
||||||
|
if params:
|
||||||
|
spec = CRUDSpec(self.model, params)
|
||||||
|
filters = spec.parse_filters()
|
||||||
|
order_by = spec.parse_sort()
|
||||||
|
limit, offset = spec.parse_pagination()
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
query = query.filter(*filters)
|
||||||
|
if order_by:
|
||||||
|
query = query.order_by(*order_by)
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
def create(self, data: dict) -> T:
|
def create(self, data: dict) -> T:
|
||||||
obj = self.model(**data)
|
obj = self.model(**data)
|
||||||
|
|
|
||||||
53
crudkit/core/spec.py
Normal file
53
crudkit/core/spec.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import List, Tuple
|
||||||
|
from sqlalchemy import asc, desc, or_, and_
|
||||||
|
|
||||||
|
OPERATORS = {
|
||||||
|
'eq': lambda col, val: col == val,
|
||||||
|
'lt': lambda col, val: col < val,
|
||||||
|
'lte': lambda col, val: col <= val,
|
||||||
|
'gt': lambda col, val: col > val,
|
||||||
|
'gte': lambda col, val: col >= val,
|
||||||
|
'ne': lambda col, val: col != val,
|
||||||
|
'icontains': lambda col, val: col.ilike(f"%{val}%"),
|
||||||
|
}
|
||||||
|
|
||||||
|
class CRUDSpec:
|
||||||
|
def __init__(self, model, params):
|
||||||
|
self.model = model
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def parse_filters(self):
|
||||||
|
filters = []
|
||||||
|
for key, value in self.params.items():
|
||||||
|
if key in ('sort', 'limit', 'offset'):
|
||||||
|
continue
|
||||||
|
if '__' in key:
|
||||||
|
field, op = key.split('__', 1)
|
||||||
|
else:
|
||||||
|
field, op = key, 'eq'
|
||||||
|
if hasattr(self.model, field):
|
||||||
|
col = getattr(self.model, field)
|
||||||
|
filters.append(OPERATORS[op](col, value))
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def parse_sort(self):
|
||||||
|
sort_args = self.params.get('sort', '')
|
||||||
|
result = []
|
||||||
|
for part in sort_args.split(','):
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
if part.startswith('-'):
|
||||||
|
field = part[1:]
|
||||||
|
order = desc
|
||||||
|
else:
|
||||||
|
field = part
|
||||||
|
order = asc
|
||||||
|
if hasattr(self.model, field):
|
||||||
|
result.append(order(getattr(self.model, field)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def parse_pagination(self):
|
||||||
|
limit = int(self.params.get('limit', 100))
|
||||||
|
offset = int(self.params.get('offset', 0))
|
||||||
|
return limit, offset
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
{% if objects %}
|
||||||
{% for field in objects[0].__table__.columns %}<th>{{ field.name }}</th>{% endfor %}
|
<tr>
|
||||||
</tr>
|
{% for field in objects[0].__table__.columns %}<th>{{ field.name }}</th>{% endfor %}
|
||||||
{% for obj in objects %}
|
</tr>
|
||||||
<tr>{% for field in obj.__table__.columns %}<td>{{ obj[field.name] }}</td>{% endfor %}</tr>
|
{% for obj in objects %}
|
||||||
{% endfor %}
|
<tr>{% for field in obj.__table__.columns %}<td>{{ obj[field.name] }}</td>{% endfor %}</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><th>No data.</th></tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
31
test_app/app.py
Normal file
31
test_app/app.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for
|
||||||
|
from test_app.models import Device, User
|
||||||
|
from test_app.db import Base, engine, SessionLocal
|
||||||
|
from crudkit.core.service import CRUDService
|
||||||
|
from crudkit.api.flask_api import generate_crud_blueprint
|
||||||
|
from crudkit.ui.fragments import render_table, render_form
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
session = SessionLocal()
|
||||||
|
device_service = CRUDService(Device, session)
|
||||||
|
user_service = CRUDService(User, session)
|
||||||
|
|
||||||
|
app.register_blueprint(generate_crud_blueprint(Device, device_service), url_prefix='/api/devices')
|
||||||
|
app.register_blueprint(generate_crud_blueprint(User, user_service), url_prefix='/api/users')
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def index():
|
||||||
|
if request.method == 'POST':
|
||||||
|
device_service.create(request.form.to_dict())
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
devices = device_service.list()
|
||||||
|
table = render_table(devices)
|
||||||
|
form = render_form(Device, {})
|
||||||
|
return render_template('index.html', table=table, form=form)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='127.0.0.1', port=5050)
|
||||||
6
test_app/db.py
Normal file
6
test_app/db.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
|
||||||
|
engine = create_engine('sqlite:///test.db', echo=True)
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
18
test_app/models.py
Normal file
18
test_app/models.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from sqlalchemy import Column, String, Integer, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from crudkit.core.base import CRUDMixin
|
||||||
|
from test_app.db import Base
|
||||||
|
|
||||||
|
class User(CRUDMixin, Base):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
name = Column(String)
|
||||||
|
email = Column(String)
|
||||||
|
supervisor_id = Column(Integer, ForeignKey('users.id'))
|
||||||
|
supervisor = relationship('User', remote_side='User.id')
|
||||||
|
|
||||||
|
class Device(CRUDMixin, Base):
|
||||||
|
__tablename__ = 'devices'
|
||||||
|
name = Column(String)
|
||||||
|
serial = Column(String)
|
||||||
|
assigned_to_id = Column(Integer, ForeignKey('users.id'))
|
||||||
|
assigned_to = relationship('User')
|
||||||
14
test_app/templates/index.html
Normal file
14
test_app/templates/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Device List</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Devices</h1>
|
||||||
|
{{ table|safe }}
|
||||||
|
|
||||||
|
<!-- RENDERING FORM START -->
|
||||||
|
<h2>Add Device</h2>
|
||||||
|
{{ form|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue