Some additional work done with CRUDKit. May start this over with a better design for CRUDKit.
This commit is contained in:
parent
f47fb6b505
commit
7738f1c9c2
5 changed files with 67 additions and 53 deletions
|
|
@ -56,7 +56,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" hx-get="{{ _rows_url(model, page, per_page, sort, filters, fields_csv) }}" hx-target="#rows"
|
<a class="page-link state-modifier" hx-get="{{ _rows_url(model, page, per_page, sort, filters, fields_csv) }}" hx-target="#rows"
|
||||||
hx-swap="innerHTML" data-page="{{ page }}" {{ attrs|safe }}>
|
hx-swap="innerHTML" data-page="{{ page }}" {{ attrs|safe }}>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -103,18 +103,6 @@
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{# keep hidden pager-state in sync with one handler instead of inline click spam #}
|
|
||||||
<script>
|
|
||||||
document.currentScript?.previousElementSibling?.addEventListener?.('click', function (e) {
|
|
||||||
const a = e.target.closest('a.page-link');
|
|
||||||
if (!a) return;
|
|
||||||
const targetPage = a.getAttribute('data-page');
|
|
||||||
if (!targetPage) return;
|
|
||||||
const inp = document.querySelector('#pager-state input[name=page]');
|
|
||||||
if (inp) inp.value = targetPage;
|
|
||||||
}, { capture: true });
|
|
||||||
</script>
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro form(schema, action, method="POST", obj_id=None, hx=False, csrf_token=None) -%}
|
{% macro form(schema, action, method="POST", obj_id=None, hx=False, csrf_token=None) -%}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ def _apply_dotted_ordering(stmt, Model, sort_tokens):
|
||||||
rel = current_mapper.relationships.get(rel_name)
|
rel = current_mapper.relationships.get(rel_name)
|
||||||
if rel is None:
|
if rel is None:
|
||||||
# invalid sort key; skip quietly or raise
|
# invalid sort key; skip quietly or raise
|
||||||
# raise ValueError(f"Unknown relationship {current_mapper.class_.__name__}.{rel_name}")
|
print(f"Unknown relationship {current_mapper.class_.__name__}.{rel_name}")
|
||||||
entity = None
|
entity = None
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -78,13 +78,20 @@ def _apply_dotted_ordering(stmt, Model, sort_tokens):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
col_name = parts[-1]
|
col_name = parts[-1]
|
||||||
# Validate final column
|
# # Validate final column
|
||||||
if col_name not in current_mapper.columns:
|
# if col_name not in current_mapper.columns:
|
||||||
# raise ValueError(f"Unknown column {current_mapper.class_.__name__}.{col_name}")
|
# print(f"Unknown column {current_mapper.class_.__name__}.{col_name}")
|
||||||
continue
|
# continue
|
||||||
|
|
||||||
col = getattr(entity, col_name) if entity is not Model else getattr(Model, col_name)
|
# col = getattr(entity, col_name) if entity is not Model else getattr(Model, col_name)
|
||||||
stmt = stmt.order_by(col.desc() if direction == "desc" else col.asc())
|
|
||||||
|
attr = getattr(entity, col_name, None)
|
||||||
|
if attr is None:
|
||||||
|
attr = getattr(current_mapper.class_, col_name, None)
|
||||||
|
if attr is None:
|
||||||
|
print(f"Unknown column {current_mapper.class_.__name__}.{col_name}")
|
||||||
|
continue
|
||||||
|
stmt = stmt.order_by(attr.desc() if direction == "desc" else attr.asc())
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,9 @@ worklog_images = image_links.worklog_images
|
||||||
User = users.User
|
User = users.User
|
||||||
|
|
||||||
# Now it’s safe to configure mappers and set global eagerloads
|
# Now it’s safe to configure mappers and set global eagerloads
|
||||||
from sqlalchemy.orm import configure_mappers, joinedload, selectinload
|
from sqlalchemy.orm import configure_mappers
|
||||||
configure_mappers()
|
configure_mappers()
|
||||||
|
|
||||||
User.ui_eagerload = (
|
|
||||||
joinedload(User.supervisor),
|
|
||||||
joinedload(User.location).joinedload(Room.room_function),
|
|
||||||
)
|
|
||||||
|
|
||||||
Room.ui_eagerload = (
|
|
||||||
joinedload(Room.area),
|
|
||||||
joinedload(Room.room_function),
|
|
||||||
selectinload(Room.inventory),
|
|
||||||
selectinload(Room.users)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
registry = {
|
registry = {
|
||||||
"area": Area,
|
"area": Area,
|
||||||
"brand": Brand,
|
"brand": Brand,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
from typing import Optional, TYPE_CHECKING, List
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .areas import Area
|
from .areas import Area
|
||||||
from .room_functions import RoomFunction
|
|
||||||
from .inventory import Inventory
|
from .inventory import Inventory
|
||||||
from .users import User
|
from .users import User
|
||||||
|
|
||||||
|
from .room_functions import RoomFunction
|
||||||
from crudkit import CrudMixin
|
from crudkit import CrudMixin
|
||||||
from sqlalchemy import ForeignKey, Identity, Integer, Unicode
|
from sqlalchemy import ForeignKey, Identity, Integer, Unicode, func, select, literal
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
|
@ -27,18 +28,6 @@ class Room(db.Model, CrudMixin):
|
||||||
ui_eagerload = tuple()
|
ui_eagerload = tuple()
|
||||||
ui_extra_attrs = ('area_id', 'function_id')
|
ui_extra_attrs = ('area_id', 'function_id')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ui_update(cls, session, id_, payload):
|
|
||||||
print(payload)
|
|
||||||
obj = session.get(cls, id_)
|
|
||||||
if not obj:
|
|
||||||
return None
|
|
||||||
obj.name = payload.get("name", obj.name)
|
|
||||||
obj.area_id = payload.get("area_id", obj.area_id)
|
|
||||||
obj.function_id = payload.get("function_id", obj.function_id)
|
|
||||||
session.commit()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, name: Optional[str] = None, area_id: Optional[int] = None, function_id: Optional[int] = None):
|
def __init__(self, name: Optional[str] = None, area_id: Optional[int] = None, function_id: Optional[int] = None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.area_id = area_id
|
self.area_id = area_id
|
||||||
|
|
@ -47,8 +36,22 @@ class Room(db.Model, CrudMixin):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>"
|
return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>"
|
||||||
|
|
||||||
@property
|
@hybrid_property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
name = self.name or ""
|
name = self.name or ""
|
||||||
func = self.room_function.description if self.room_function else ""
|
function = self.room_function.description if self.room_function else ""
|
||||||
return f"{name} - {func}".strip(" -")
|
return f"{name} - {function}".strip(" -")
|
||||||
|
|
||||||
|
@identifier.expression
|
||||||
|
def identifier(cls):
|
||||||
|
rf_desc = (
|
||||||
|
select(RoomFunction.description)
|
||||||
|
.where(RoomFunction.id == cls.function_id)
|
||||||
|
.correlate(cls)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
return func.concat(
|
||||||
|
func.coalesce(cls.name, ''), # left part
|
||||||
|
literal(' - '), # separator
|
||||||
|
func.coalesce(rf_desc, '') # right part via correlated subquery
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,9 @@ refresh_url=none, model=none, sort=none) %}
|
||||||
<thead class="sticky-top">
|
<thead class="sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
{% for h in headers %}
|
{% for h in headers %}
|
||||||
<th class="text-nowrap">{{ h }}</th>
|
<th class="text-nowrap state-modifier" {% if fields %}
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page={{ page }}&per_page={{ per_page }}&fields_csv={{ fields|join(',') }}&sort={{ fields[loop.index0] }}"
|
||||||
|
hx-target="#rows" hx-swap="innerHTML" hx-trigger="click" data-sort={{ fields[loop.index0] }}{% endif %}>{{ h }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -82,7 +84,7 @@ refresh_url=none, model=none, sort=none) %}
|
||||||
<input type="hidden" name="page" value="{{ page }}">
|
<input type="hidden" name="page" value="{{ page }}">
|
||||||
<input type="hidden" name="per_page" value="{{ per_page }}">
|
<input type="hidden" name="per_page" value="{{ per_page }}">
|
||||||
<input type="hidden" name="fields_csv" value="{{ fields|join(',') }}">
|
<input type="hidden" name="fields_csv" value="{{ fields|join(',') }}">
|
||||||
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
|
<input type="hidden" name="sort" value="{{ sort }}">
|
||||||
{% for k,v in (filters or {}).items() %}
|
{% for k,v in (filters or {}).items() %}
|
||||||
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -90,4 +92,31 @@ refresh_url=none, model=none, sort=none) %}
|
||||||
</div>
|
</div>
|
||||||
<div id="pager" hx-get="/ui/{{ model }}/frag/pager" hx-include="#pager-state"
|
<div id="pager" hx-get="/ui/{{ model }}/frag/pager" hx-include="#pager-state"
|
||||||
hx-trigger="load, htmx:afterSwap from:#rows" hx-target="#pager" hx-swap="innerHTML"></div>
|
hx-trigger="load, htmx:afterSwap from:#rows" hx-target="#pager" hx-swap="innerHTML"></div>
|
||||||
|
|
||||||
|
{# keep hidden pager-state in sync with one handler instead of inline click spam #}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const a = e.target.closest('a.page-link.state-modifier');
|
||||||
|
const th = e.target.closest('th.state-modifier');
|
||||||
|
if (!a && !th) return;
|
||||||
|
|
||||||
|
// run BEFORE htmx handles the click
|
||||||
|
if (a) {
|
||||||
|
const targetPage = a.dataset.page;
|
||||||
|
if (targetPage) {
|
||||||
|
const inp = document.querySelector('#pager-state input[name=page]');
|
||||||
|
if (inp) inp.value = targetPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (th) {
|
||||||
|
const targetSort = th.dataset.sort;
|
||||||
|
if (targetSort) {
|
||||||
|
const inp = document.querySelector('#pager-state input[name=sort]');
|
||||||
|
if (inp) inp.value = targetSort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { capture: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue