diff --git a/.gitignore b/.gitignore index c96f862..22ca0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ inventory/static/uploads/* !inventory/static/uploads/.gitkeep .venv/ -.vscode/ .env *.db* *.db-journal diff --git a/crudkit/api/flask_api.py b/crudkit/api/flask_api.py index 0f31264..39e7c49 100644 --- a/crudkit/api/flask_api.py +++ b/crudkit/api/flask_api.py @@ -60,8 +60,11 @@ def generate_crud_blueprint(model, service, *, base_prefix: str | None = None, r DELETE /api//delete?id=123[&hard=1] """ model_name = model.__name__.lower() + # bikeshed if you want pluralization; this is the least-annoying default + collection = (base_prefix or model_name).lower() + plural = collection if collection.endswith('s') else f"{collection}s" - bp = Blueprint(model_name, __name__, url_prefix=f"/api/{model_name}") + bp = Blueprint(plural, __name__, url_prefix=f"/api/{plural}") @bp.errorhandler(Exception) def _handle_any(e: Exception): @@ -102,7 +105,7 @@ def generate_crud_blueprint(model, service, *, base_prefix: str | None = None, r obj = service.create(payload) resp = jsonify(obj.as_dict()) resp.status_code = 201 - resp.headers["Location"] = url_for(f"{bp.name}.rest_get", obj_id=obj.id, _external=False) + resp.headers["Location"] = url_for(f"{plural}.rest_get", obj_id=obj.id, _external=False) return resp except Exception as e: return _json_error(e) diff --git a/crudkit/core/service.py b/crudkit/core/service.py index d7fabc4..fbad3a1 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -415,8 +415,6 @@ class CRUDService(Generic[T]): opt = opt.load_only(*cols) query = query.options(opt) - # inside CRUDService._apply_firsthop_strategies - # ... # NEW: if a first-hop to-one relationship’s target table is present in filter expressions, # make sure we actually JOIN it (outer) so filters don’t create a cartesian product. if plan.filter_tables: @@ -424,19 +422,14 @@ class CRUDService(Generic[T]): for rel in mapper.relationships: if rel.uselist: continue # only first-hop to-one here - target_cls = rel.mapper.class_ - target_tbl = getattr(target_cls, "__table__", None) + target_tbl = getattr(rel.mapper.class_, "__table__", None) if target_tbl is None: continue if target_tbl in plan.filter_tables: if rel.key in joined_rel_keys: continue # already joined via join_paths - - # alias when joining same-entity relationships (User->User supervisor) - ta = aliased(target_cls) if target_cls is self.model else target_cls - query = query.join(getattr(root_alias, rel.key).of_type(ta), isouter=True) + query = query.join(getattr(root_alias, rel.key), isouter=True) joined_rel_keys.add(rel.key) - if log.isEnabledFor(logging.DEBUG): info = [] for base_alias, rel_attr, target_alias in plan.join_paths: diff --git a/crudkit/ui/fragments.py b/crudkit/ui/fragments.py index 03cc1c0..ab4c3ba 100644 --- a/crudkit/ui/fragments.py +++ b/crudkit/ui/fragments.py @@ -11,8 +11,6 @@ from sqlalchemy.orm.base import NO_VALUE from sqlalchemy.orm.properties import ColumnProperty, RelationshipProperty from typing import Any, Dict, List, Optional, Tuple -import crudkit - _ALLOWED_ATTRS = { "class", "placeholder", "autocomplete", "inputmode", "pattern", "min", "max", "step", "maxlength", "minlength", @@ -109,53 +107,6 @@ def register_template_globals(app=None): app.add_template_global(fn, name) installed.add(name) -def _fields_for_label_params(label_spec, related_model): - """ - Build a 'fields' list suitable for CRUDService.list() so labels render - without triggering lazy loads. Always includes 'id'. - """ - simple_cols, rel_paths = _extract_label_requirements(label_spec, related_model) - fields = set(["id"]) - for c in simple_cols: - fields.add(c) - for rel_name, col_name in rel_paths: - if col_name == "__all__": - # just ensure relationship object is present; ask for rel.id - fields.add(f"{rel_name}.id") - else: - fields.add(f"{rel_name}.{col_name}") - return list(fields) - -def _fk_options_via_service(related_model, label_spec, *, options_params: dict | None = None): - svc = crudkit.crud.get_service(related_model) - - # default to unlimited results for dropdowns - params = {"limit": 0} - if options_params: - params.update(options_params) # caller can override limit if needed - - # ensure fields needed to render the label are present (avoid lazy loads) - fields = _fields_for_label_params(label_spec, related_model) - if fields: - existing = params.get("fields") - if isinstance(existing, str): - existing = [s.strip() for s in existing.split(",") if s.strip()] - if isinstance(existing, (list, tuple)): - params["fields"] = list(dict.fromkeys(list(existing) + fields)) - else: - params["fields"] = fields - - # only set a default sort if caller didn’t supply one - if "sort" not in params: - simple_cols, _ = _extract_label_requirements(label_spec, related_model) - params["sort"] = (simple_cols[0] if simple_cols else "id") - - rows = svc.list(params) - return [ - {"value": str(r.id), "label": _label_from_obj(r, label_spec)} - for r in rows - ] - def expand_projection(model_cls, fields): req = getattr(model_cls, "__crudkit_field_requires__", {}) or {} out = set(fields) @@ -696,12 +647,10 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default): if "label_deps" in spec: field["label_deps"] = spec["label_deps"] - opts_params = spec.get("options_params") or spec.get("options_filter") or spec.get("options_where") - if rel_prop: if field["type"] is None: field["type"] = "select" - if field["type"] == "select" and field.get("options") is None: + if field["type"] == "select" and field.get("options") is None and session is not None: related_model = rel_prop.mapper.class_ label_spec = ( spec.get("label_spec") @@ -709,11 +658,7 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default): or getattr(related_model, "__crud_label__", None) or "id" ) - field["options"] = _fk_options_via_service( - related_model, - label_spec, - options_params=opts_params - ) + field["options"] = _fk_options(session, related_model, label_spec) return field col = mapper.columns.get(name) diff --git a/crudkit/ui/templates/field.html b/crudkit/ui/templates/field.html index 9a9174a..aecb234 100644 --- a/crudkit/ui/templates/field.html +++ b/crudkit/ui/templates/field.html @@ -1,4 +1,4 @@ - +{# show label unless hidden/custom #} {% if field_type != 'hidden' and field_label %}