From c22ecf44ec0cdbae0465c34d83f1f270c941b4e4 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Tue, 2 Sep 2025 12:27:51 -0500 Subject: [PATCH] More changes brought through testing. --- .gitignore | 3 +- crudkit/api/flask_api.py | 2 +- crudkit/core/service.py | 17 +++++++++-- crudkit/core/spec.py | 53 +++++++++++++++++++++++++++++++++ crudkit/ui/templates/table.html | 16 ++++++---- test_app/app.py | 31 +++++++++++++++++++ test_app/db.py | 6 ++++ test_app/models.py | 18 +++++++++++ test_app/templates/index.html | 14 +++++++++ 9 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 crudkit/core/spec.py create mode 100644 test_app/app.py create mode 100644 test_app/db.py create mode 100644 test_app/models.py create mode 100644 test_app/templates/index.html diff --git a/.gitignore b/.gitignore index 09da556..3bae2c6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,4 @@ inventory/static/uploads/* *.sqlite3 alembic.ini alembic/ -*.egg-info/ -test_app/ \ No newline at end of file +*.egg-info/ \ No newline at end of file diff --git a/crudkit/api/flask_api.py b/crudkit/api/flask_api.py index 611c8c0..ddb77a9 100644 --- a/crudkit/api/flask_api.py +++ b/crudkit/api/flask_api.py @@ -5,7 +5,7 @@ def generate_crud_blueprint(model, service): @bp.get('/') def list_items(): - items = service.list() + items = service.list(request.args) return jsonify([item.as_dict() for item in items]) @bp.get('/') diff --git a/crudkit/core/service.py b/crudkit/core/service.py index ca58dcf..b9d10ac 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -1,5 +1,6 @@ from typing import Type, TypeVar, Generic from sqlalchemy.orm import Session +from crudkit.core.spec import CRUDSpec T = TypeVar("T") @@ -11,8 +12,20 @@ class CRUDService(Generic[T]): def get(self, id: int) -> T: return self.session.get(self.model, id) - def list(self, limit=100, offset=0) -> list[T]: - return self.session.query(self.model).offset(offset).limit(limit).all() + def list(self, params=None) -> list[T]: + 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: obj = self.model(**data) diff --git a/crudkit/core/spec.py b/crudkit/core/spec.py new file mode 100644 index 0000000..a1f8292 --- /dev/null +++ b/crudkit/core/spec.py @@ -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 diff --git a/crudkit/ui/templates/table.html b/crudkit/ui/templates/table.html index 5b65d61..b4abd80 100644 --- a/crudkit/ui/templates/table.html +++ b/crudkit/ui/templates/table.html @@ -1,8 +1,12 @@ - - {% for field in objects[0].__table__.columns %}{% endfor %} - - {% for obj in objects %} - {% for field in obj.__table__.columns %}{% endfor %} - {% endfor %} + {% if objects %} + + {% for field in objects[0].__table__.columns %}{% endfor %} + + {% for obj in objects %} + {% for field in obj.__table__.columns %}{% endfor %} + {% endfor %} + {% else %} + + {% endif %}
{{ field.name }}
{{ obj[field.name] }}
{{ field.name }}
{{ obj[field.name] }}
No data.
\ No newline at end of file diff --git a/test_app/app.py b/test_app/app.py new file mode 100644 index 0000000..8d3f8bd --- /dev/null +++ b/test_app/app.py @@ -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) diff --git a/test_app/db.py b/test_app/db.py new file mode 100644 index 0000000..8bd38fd --- /dev/null +++ b/test_app/db.py @@ -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() diff --git a/test_app/models.py b/test_app/models.py new file mode 100644 index 0000000..79df3e1 --- /dev/null +++ b/test_app/models.py @@ -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') diff --git a/test_app/templates/index.html b/test_app/templates/index.html new file mode 100644 index 0000000..a690a2b --- /dev/null +++ b/test_app/templates/index.html @@ -0,0 +1,14 @@ + + + + Device List + + +

Devices

+ {{ table|safe }} + + +

Add Device

+ {{ form|safe }} + +