Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bde89c47e | ||
|
|
afd3dce493 | ||
|
|
89248407e7 | ||
|
|
aa6db94179 |
49
CHANGELOG.md
49
CHANGELOG.md
@@ -10,6 +10,55 @@ ADR-007 and ADR-002.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.11.2] - 2026-08-17
|
||||
|
||||
Bug fixes for the buildings-and-levels work in 0.11.0, all found by using it.
|
||||
Anyone on 0.11.0 or 0.11.1 with more than one level should take this: until now
|
||||
the map drew markers from every level on whichever floor plan was showing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Markers from other levels were drawn on the displayed level.** The map
|
||||
component filtered out positions with no coordinates and drew everything else,
|
||||
never checking which drawing a position belonged to - the exact failure ADR-017
|
||||
exists to prevent, in the one component that draws the map. Markers are now
|
||||
filtered to the level being shown, and a position with no level is omitted
|
||||
rather than approximated onto the default.
|
||||
- **The map page had no way to choose a level.** It read the current level only
|
||||
to title a PDF export, so a second floor was unreachable from the viewer most
|
||||
people use. A level selector appears when a site has more than one, and
|
||||
switching moves the drawing, the bounds, the coordinate space and the markers
|
||||
together.
|
||||
- **The map editor never accepted a click.** Its click handler was bound only if
|
||||
the map was already in picker mode when it mounted, and the editor opens with
|
||||
nothing selected, so selecting an asset and clicking did nothing at all for the
|
||||
life of the page. The asset forms were unaffected, because their picker is
|
||||
mounted inside a dialog that is already in picker mode.
|
||||
- Existing markers are now drawn while placing one. Positioning an asset relative
|
||||
to the machines already on the floor is the whole task.
|
||||
- Searching the map follows results across levels: a search whose matches are all
|
||||
on another floor showed an empty map while the filter counted them.
|
||||
- **A level's native size could not be set.** The settings page had a width field
|
||||
and no height field at all, and locked both once any marker was placed - which
|
||||
blocked the case the feature was built for, a new blueprint of new dimensions
|
||||
on a floor that already has markers. Both fields are editable, with a
|
||||
confirmation and a pointer to landmark recalibration.
|
||||
- **The same search could return different results.** Fourteen of the sixteen
|
||||
searchers behind global search truncated with `LIMIT` and no `ORDER BY`, so the
|
||||
database was free to return a different subset of matching rows each time.
|
||||
Every searcher now ends in a total order.
|
||||
- Re-searching a term already in the address bar did nothing: the router treats
|
||||
it as a duplicate navigation and aborts, so the results page never re-queried.
|
||||
This is why the sidebar box and the results-page box appeared to disagree.
|
||||
- The empty legend no longer draws its bar and border, which read as a stray
|
||||
input box under the map toolbar.
|
||||
- The map editor's panel header no longer crams a heading and three controls onto
|
||||
one row, which had squeezed the search box until its placeholder read
|
||||
"Search na".
|
||||
- Both map pages had a scrollbar. They subtracted the wrong amount from the
|
||||
viewport height for the page chrome, so each overflowed by the difference; the
|
||||
padding is now a value the pages and the layout share.
|
||||
|
||||
## [0.11.1] - 2026-08-17
|
||||
|
||||
A patch release: the pagination-cap fixes, plus the documentation that missed the
|
||||
|
||||
@@ -11,7 +11,7 @@ never by editing this file.
|
||||
|
||||
| series | value | governed by |
|
||||
|---|---|---|
|
||||
| product `__version__` | `0.11.1` | ADR-007 |
|
||||
| product `__version__` | `0.11.2` | ADR-007 |
|
||||
| plugin contract `__contract_version__` | `0.20.0` | ADR-002 |
|
||||
|
||||
They move independently. A contract bump is not a release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shopdb-frontend",
|
||||
"version": "0.11.1",
|
||||
"version": "0.11.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
--sidebar-bg: #00003d;
|
||||
--sidebar-text: #ffffff;
|
||||
--sidebar-width: 250px;
|
||||
/* Vertical chrome around .main-content. A full-height page subtracts these;
|
||||
hardcoding a different number is what put a scrollbar on the map. */
|
||||
--main-pad-top: 20px;
|
||||
--main-pad-bottom: 70px;
|
||||
|
||||
/* Hover variants */
|
||||
--secondary-dark: #82503f;
|
||||
@@ -243,7 +247,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 20px 10px 70px 10px;
|
||||
padding: var(--main-pad-top) 10px var(--main-pad-bottom) 10px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Legend for asset type mode - shows subtypes when a type is selected -->
|
||||
<div class="map-legend" v-if="!pickerMode && assetTypeMode">
|
||||
<div class="map-legend" v-if="!pickerMode && assetTypeMode && hasLegendEntries">
|
||||
<!-- Show subtype legend when a specific type is selected -->
|
||||
<template v-if="selectedAssetType && Object.keys(visibleSubtypes).length">
|
||||
<span
|
||||
@@ -120,6 +120,14 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['markerClick', 'positionPicked'])
|
||||
|
||||
// An empty legend still drew its bar and border, which looked like a stray input
|
||||
// box under the toolbar. It renders only when it has something in it.
|
||||
const hasLegendEntries = computed(() => {
|
||||
if (props.selectedAssetType && Object.keys(visibleSubtypes.value).length) return true
|
||||
if (Object.keys(visibleAssetTypes.value).length) return true
|
||||
return overlayLegend.value.length > 0
|
||||
})
|
||||
|
||||
const mapContainer = ref(null)
|
||||
let map = null
|
||||
let imageOverlay = null
|
||||
@@ -296,17 +304,22 @@ function initMap() {
|
||||
map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], initialZoom)
|
||||
map.setMaxBounds(bounds)
|
||||
|
||||
// Picker mode: click to set position
|
||||
if (props.pickerMode) {
|
||||
map.on('click', handleMapClick)
|
||||
// Bound UNCONDITIONALLY, and handleMapClick ignores the click when picker mode
|
||||
// is off. Binding it only when pickerMode was true AT MOUNT meant a map that
|
||||
// becomes a picker later never got a click handler at all: the map editor
|
||||
// opens with nothing selected, so clicking to place a marker did nothing, for
|
||||
// the whole life of the page. The asset forms only worked because their picker
|
||||
// is mounted inside a modal that is already in picker mode.
|
||||
map.on('click', handleMapClick)
|
||||
|
||||
// Show initial position if provided
|
||||
if (props.initialPosition) {
|
||||
setPickerPosition(props.initialPosition.left, props.initialPosition.top)
|
||||
}
|
||||
} else {
|
||||
renderMarkers()
|
||||
if (props.pickerMode && props.initialPosition) {
|
||||
setPickerPosition(props.initialPosition.left, props.initialPosition.top)
|
||||
}
|
||||
|
||||
// Existing markers are drawn in picker mode too: placing a marker relative to
|
||||
// the ones already on the floor is the whole task. The forms pass no markers,
|
||||
// so this costs them nothing.
|
||||
renderMarkers()
|
||||
}
|
||||
|
||||
function handleMapClick(e) {
|
||||
@@ -437,8 +450,18 @@ function renderMarkers() {
|
||||
overlayLayers.forEach(layer => layer.remove())
|
||||
overlayLayers = []
|
||||
|
||||
// Markers belong to ONE level (ADR-017). This is the drawing being shown, and
|
||||
// anything positioned against a different one is not drawn on it.
|
||||
const drawnLevel = drawnLevelId() ?? null
|
||||
|
||||
props.machines.forEach(item => {
|
||||
if (item.mapx == null || item.mapy == null) return
|
||||
// A marker from another level, placed on THIS blueprint, looks entirely
|
||||
// correct and points at the wrong part of the building - so it is omitted
|
||||
// rather than approximated. A position with no level at all is omitted for
|
||||
// the same reason: the map editor lists both, badged, so they can be fixed
|
||||
// rather than silently misplaced here.
|
||||
if ((item.levelid ?? null) !== drawnLevel) return
|
||||
|
||||
// Transform coordinates (database Y is top-down, Leaflet is bottom-up)
|
||||
const leafletY = MAP_HEIGHT - item.mapy
|
||||
@@ -599,6 +622,31 @@ watch(() => props.theme, (newTheme) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Switching level changes the drawing, the coordinate space AND which markers
|
||||
// belong on it. All three move together: the size is what marker coordinates
|
||||
// mean, so swapping the image without the bounds would place every marker
|
||||
// against the wrong scale, and keeping the markers would show the previous
|
||||
// floor's assets on this floor's plan.
|
||||
watch(() => drawnLevelId(), (levelid) => {
|
||||
if (!map || !imageOverlay) return
|
||||
|
||||
MAP_WIDTH = dimensionsFor(levelid).width
|
||||
MAP_HEIGHT = dimensionsFor(levelid).height
|
||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||
|
||||
const url = blueprintUrlFor(props.theme, levelid)
|
||||
// A level with no blueprint is left blank rather than showing the previous
|
||||
// one, which would be a floor plan labelled as somewhere it is not.
|
||||
imageOverlay.setUrl(url || '')
|
||||
imageOverlay.setBounds(bounds)
|
||||
|
||||
map.setMaxBounds(bounds)
|
||||
map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], map.getZoom())
|
||||
|
||||
renderMarkers()
|
||||
loadOverlays()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// Load this facility's blueprint + dimensions before building the map so
|
||||
// bounds and coordinate math use the right size. Falls back to defaults.
|
||||
|
||||
@@ -492,7 +492,7 @@ function cancelEdit() {
|
||||
.map-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 40px);
|
||||
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -518,13 +518,30 @@ function cancelEdit() {
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
/* The heading takes its own row and the three controls share the next one(s).
|
||||
All four on a single row inside a 320px panel squeezed the search box until
|
||||
its placeholder read "Search na". */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.panel-header .form-control {
|
||||
/* One control per row. Two of them side by side in a 318px panel left the
|
||||
search box 139px for a placeholder needing 216px, so it read "Search name
|
||||
o" - a control whose own label does not fit is not a narrower control, it is
|
||||
an unlabelled one. */
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
<template v-else>
|
||||
<!-- Filter Controls -->
|
||||
<div class="map-filters">
|
||||
<select
|
||||
v-if="levelChoices.length > 1"
|
||||
v-model.number="shownLevelId"
|
||||
@change="onLevelChange"
|
||||
title="Which floor plan to show"
|
||||
>
|
||||
<option v-for="level in levelChoices" :key="level.levelid" :value="level.levelid">
|
||||
{{ level.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select v-model="selectedType" @change="onTypeChange">
|
||||
<option value="">All Asset Types</option>
|
||||
<option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype">
|
||||
@@ -60,6 +71,7 @@
|
||||
</div>
|
||||
|
||||
<ShopFloorMap
|
||||
:levelid="shownLevelId"
|
||||
:machines="filteredAssets"
|
||||
:machinetypes="[]"
|
||||
:businessunits="businessunits"
|
||||
@@ -82,7 +94,8 @@ import ShopFloorMap from '../components/ShopFloorMap.vue'
|
||||
import { assetsApi } from '../api'
|
||||
import { currentTheme } from '../stores/theme'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, state as mapConfig } from '@/composables/mapConfig'
|
||||
import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, levelOptions,
|
||||
setCurrentLevel, state as mapConfig } from '@/composables/mapConfig'
|
||||
import { exportMapPdf } from '../utils/mapPdf'
|
||||
import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
|
||||
import { getSubtypeId } from '../utils/mapColors'
|
||||
@@ -104,6 +117,41 @@ const selectedSubtype = ref('')
|
||||
const selectedBusinessUnit = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Which floor plan is on screen. Every marker coordinate is pixels of ONE level
|
||||
// (ADR-017), so this decides both the drawing and which markers belong on it.
|
||||
const shownLevelId = ref(null)
|
||||
|
||||
const levelChoices = computed(() => levelOptions())
|
||||
|
||||
function onLevelChange() {
|
||||
// Keep the shared composable in step, so a map-position picker opened later
|
||||
// starts on the level being looked at rather than the site default.
|
||||
setCurrentLevel(shownLevelId.value)
|
||||
}
|
||||
|
||||
// A search whose only matches are on another floor would otherwise show an empty
|
||||
// map: the assets matched, the filter counted them, and nothing was drawn.
|
||||
// Follow the results to the level that actually holds them - the level with the
|
||||
// most matches, so a search matching several floors lands on the best one.
|
||||
function followSearchAcrossLevels() {
|
||||
if (!searchQuery.value.trim()) return
|
||||
const matches = filteredAssets.value.filter(a => a.mapx != null && a.mapy != null)
|
||||
if (!matches.length) return
|
||||
if (matches.some(a => (a.levelid ?? null) === shownLevelId.value)) return
|
||||
|
||||
const tally = new Map()
|
||||
matches.forEach(asset => {
|
||||
const levelid = asset.levelid ?? null
|
||||
if (levelid === null) return
|
||||
tally.set(levelid, (tally.get(levelid) || 0) + 1)
|
||||
})
|
||||
if (!tally.size) return
|
||||
|
||||
const best = [...tally.entries()].sort((a, b) => b[1] - a[1])[0][0]
|
||||
shownLevelId.value = best
|
||||
setCurrentLevel(best)
|
||||
}
|
||||
const exporting = ref(false)
|
||||
|
||||
let searchTimeout = null
|
||||
@@ -245,12 +293,12 @@ async function exportPdf() {
|
||||
// blueprintUrlFor applies withBase - the raw setting value is a
|
||||
// root-relative /api path, which 404s under a subpath mount like /ops.
|
||||
// The PDF always uses the light blueprint: it prints on white paper.
|
||||
blueprintUrl: blueprintUrlFor('light', mapConfig.currentlevelid),
|
||||
mapWidth: dimensionsFor(mapConfig.currentlevelid).width,
|
||||
mapHeight: dimensionsFor(mapConfig.currentlevelid).height,
|
||||
blueprintUrl: blueprintUrlFor('light', shownLevelId.value),
|
||||
mapWidth: dimensionsFor(shownLevelId.value).width,
|
||||
mapHeight: dimensionsFor(shownLevelId.value).height,
|
||||
// Named on the sheet, because a floor plan with no level on it is not
|
||||
// identifiable once it is printed and carried to the floor.
|
||||
levelname: levelName(mapConfig.currentlevelid),
|
||||
levelname: levelName(shownLevelId.value),
|
||||
selectedType: selectedType.value,
|
||||
subtypeColors: subtypeColorMap.value,
|
||||
subtypeNames: subtypeNameMap.value,
|
||||
@@ -265,7 +313,8 @@ async function exportPdf() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadMapConfig()
|
||||
await loadMapConfig()
|
||||
shownLevelId.value = mapConfig.currentlevelid ?? mapConfig.defaultlevelid ?? null
|
||||
try {
|
||||
const response = await assetsApi.getMap()
|
||||
const data = response.data.data || {}
|
||||
@@ -305,6 +354,7 @@ function updateMapLayers() {
|
||||
function debouncedSearch() {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
followSearchAcrossLevels()
|
||||
updateMapLayers()
|
||||
}, 300)
|
||||
}
|
||||
@@ -318,7 +368,7 @@ function handleMarkerClick(asset) {
|
||||
.map-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 2rem);
|
||||
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
|
||||
}
|
||||
|
||||
.map-page .page-header {
|
||||
|
||||
@@ -213,9 +213,18 @@ async function search(q) {
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
if (searchInput.value.trim()) {
|
||||
router.push({ path: '/search', query: { q: searchInput.value.trim() } })
|
||||
const term = searchInput.value.trim()
|
||||
if (!term) return
|
||||
// Searching a term that is ALREADY in the URL is a duplicate navigation: the
|
||||
// router aborts it, so the route watcher never fires and the button does
|
||||
// nothing at all. Re-run the query directly in that case. The sidebar box
|
||||
// never hits this, because it is always navigating from somewhere else -
|
||||
// which is why the two search boxes appeared to behave differently.
|
||||
if (term === route.query.q) {
|
||||
search(term)
|
||||
return
|
||||
}
|
||||
router.push({ path: '/search', query: { q: term } })
|
||||
}
|
||||
|
||||
async function openKBArticle(result) {
|
||||
|
||||
@@ -62,18 +62,36 @@
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="mono">{{ level.mapwidth }} x {{ level.mapheight }}</span>
|
||||
<div class="size-fields">
|
||||
<input
|
||||
v-model.number="level.mapwidth"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control size-input"
|
||||
aria-label="Blueprint width in pixels"
|
||||
@blur="saveSize(level)"
|
||||
:disabled="saving"
|
||||
/>
|
||||
<span class="size-x">x</span>
|
||||
<input
|
||||
v-model.number="level.mapheight"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control size-input"
|
||||
aria-label="Blueprint height in pixels"
|
||||
@blur="saveSize(level)"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
<small v-if="level.assetcount" class="input-hint">
|
||||
fixed while {{ level.assetcount }} marker(s) are placed
|
||||
Taken from the image while a level is empty. Changing it now
|
||||
re-scales where all {{ level.assetcount }} marker(s) sit relative
|
||||
to the drawing - set the new size, then Recalibrate from
|
||||
landmarks in the map editor.
|
||||
</small>
|
||||
<small v-else class="input-hint">
|
||||
Set from the blueprint you upload.
|
||||
</small>
|
||||
<input
|
||||
v-else
|
||||
v-model.number="level.mapwidth"
|
||||
type="number"
|
||||
class="form-control size-input"
|
||||
@blur="saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="{ 'muted': !level.assetcount }">{{ level.assetcount }}</span>
|
||||
@@ -311,6 +329,29 @@ async function saveLevel(level, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSize(level) {
|
||||
if (!(level.mapwidth > 0) || !(level.mapheight > 0)) {
|
||||
error.value = 'Width and height must both be positive.'
|
||||
await load()
|
||||
return
|
||||
}
|
||||
// A level with markers keeps its coordinates when the size changes, so every
|
||||
// marker moves relative to the drawing. That is sometimes exactly right (a
|
||||
// re-export of the same plan at a new resolution) and sometimes the start of
|
||||
// a recalibration, but it is never something to do by accident.
|
||||
if (level.assetcount) {
|
||||
const ok = window.confirm(
|
||||
`${level.levelname} has ${level.assetcount} marker(s) placed against ` +
|
||||
`${level.mapwidth} x ${level.mapheight}. Changing the size moves all of ` +
|
||||
`them relative to the drawing. Recalibrate from landmarks afterwards. Continue?`)
|
||||
if (!ok) {
|
||||
await load()
|
||||
return
|
||||
}
|
||||
}
|
||||
await saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })
|
||||
}
|
||||
|
||||
async function upload(level, theme, event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
@@ -398,6 +439,8 @@ async function remove(level) {
|
||||
.mono { font-family: monospace; }
|
||||
.order-input { width: 4.5rem; }
|
||||
.size-input { width: 6rem; }
|
||||
.size-fields { display: flex; align-items: center; gap: 0.35rem; }
|
||||
.size-x { color: var(--text-light); }
|
||||
.file-input { display: block; margin-top: 0.25rem; font-size: 0.75rem; }
|
||||
|
||||
.map-thumb {
|
||||
|
||||
@@ -63,7 +63,7 @@ __contract_version__ = '0.20.0'
|
||||
# plugin-contract version above are distinct series with independent
|
||||
# bump rules. Not part of the shopdb.api contract surface, so it is
|
||||
# not re-exported there.
|
||||
__version__ = '0.11.1'
|
||||
__version__ = '0.11.2'
|
||||
|
||||
|
||||
def create_app(config_name: str = None) -> Flask:
|
||||
|
||||
@@ -23,6 +23,14 @@ logger = logging.getLogger(__name__)
|
||||
search_bp = Blueprint('search', __name__)
|
||||
|
||||
|
||||
# EVERY searcher below truncates with .limit(). An unordered LIMIT lets the
|
||||
# database return a DIFFERENT subset of the matching rows between two identical
|
||||
# requests - the same search run twice came back with different hits, which reads
|
||||
# as the search being broken rather than as a missing ORDER BY. Each query
|
||||
# therefore ends with a TOTAL order: a display key plus the primary key, so ties
|
||||
# cannot reorder. Relevance is applied afterwards in Python, over a stable set.
|
||||
|
||||
|
||||
def _word_match(query, *columns):
|
||||
"""SQL clause matching rows that contain EVERY word of the query, each word
|
||||
in any of the columns, in any order.
|
||||
@@ -213,7 +221,7 @@ def _search_applications(query, search_term):
|
||||
apps = Application.query.filter(
|
||||
Application.isactive == True,
|
||||
_word_match(query, Application.appname, Application.appdescription)
|
||||
).limit(10).all()
|
||||
).order_by(Application.appname, Application.appid).limit(10).all()
|
||||
|
||||
for app in apps:
|
||||
relevance = 20
|
||||
@@ -258,7 +266,8 @@ def _search_knowledgebase(query, search_term):
|
||||
KnowledgeBase.appid.notin_(retired)),
|
||||
_word_match(query, KnowledgeBase.shortdescription,
|
||||
KnowledgeBase.keywords)
|
||||
).limit(20).all()
|
||||
).order_by(KnowledgeBase.clicks.desc(),
|
||||
KnowledgeBase.linkid).limit(20).all()
|
||||
|
||||
for kb in kb_articles:
|
||||
relevance = 10 + (kb.clicks or 0) * 0.1
|
||||
@@ -378,7 +387,7 @@ def _search_assets(query, search_term):
|
||||
_word_match(query, Asset.assetnumber, Asset.name,
|
||||
Asset.serialnumber, Asset.notes,
|
||||
Asset.gaugelabreference, Asset.maintenancereference)
|
||||
).limit(15).all()
|
||||
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
|
||||
|
||||
for asset in assets:
|
||||
relevance = 15
|
||||
@@ -421,7 +430,7 @@ def _search_measuringtools(query, search_term):
|
||||
Asset.isactive == True,
|
||||
_word_match(query, Asset.assetnumber, Asset.name,
|
||||
Asset.serialnumber, Asset.gaugelabreference)
|
||||
).limit(15).all()
|
||||
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
|
||||
|
||||
for asset in assets:
|
||||
relevance = 15
|
||||
@@ -461,7 +470,8 @@ def _search_usbdevices(query, search_term):
|
||||
USBDevice.isactive == True,
|
||||
_word_match(query, USBDevice.serialnumber, USBDevice.assetnumber,
|
||||
USBDevice.label, USBDevice.productname)
|
||||
).limit(10).all()
|
||||
).order_by(USBDevice.serialnumber,
|
||||
USBDevice.usbdeviceid).limit(10).all()
|
||||
|
||||
for device in devices:
|
||||
relevance = 20
|
||||
@@ -507,7 +517,8 @@ def _search_printeditems(query, search_term):
|
||||
PrintedItem.isactive == True,
|
||||
_word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag,
|
||||
PrintedItem.itemname, PrintedItem.itemdescription)
|
||||
).limit(10).all()
|
||||
).order_by(PrintedItem.itemcode,
|
||||
PrintedItem.printeditemid).limit(10).all()
|
||||
|
||||
for item in items:
|
||||
relevance = 20
|
||||
@@ -563,7 +574,7 @@ def _search_customfields(query, search_term):
|
||||
CustomField.searchable == True,
|
||||
CustomField.isactive == True,
|
||||
_word_match(query, CustomFieldValue.value),
|
||||
).limit(15).all()
|
||||
).order_by(Asset.assetnumber, CustomField.fieldid).limit(15).all()
|
||||
|
||||
for asset, field in rows:
|
||||
result = _get_asset_result(asset, query, relevance=40)
|
||||
@@ -583,7 +594,8 @@ def _search_by_ip(query, search_term):
|
||||
).options(
|
||||
joinedload(Communication.asset).joinedload(Asset.assettype),
|
||||
joinedload(Communication.asset).joinedload(Asset.location),
|
||||
).limit(10).all()
|
||||
).order_by(Communication.ipaddress,
|
||||
Communication.communicationid).limit(10).all()
|
||||
|
||||
seen_assets = set()
|
||||
for comm in comms:
|
||||
@@ -643,7 +655,7 @@ def _search_hostnames(query, search_term):
|
||||
).options(
|
||||
joinedload(Computer.asset).joinedload(Asset.assettype),
|
||||
joinedload(Computer.asset).joinedload(Asset.location),
|
||||
).limit(10).all()
|
||||
).order_by(Computer.hostname, Computer.computerid).limit(10).all()
|
||||
|
||||
for comp in computers:
|
||||
if comp.asset and comp.asset.isactive:
|
||||
@@ -666,7 +678,7 @@ def _search_hostnames(query, search_term):
|
||||
).options(
|
||||
joinedload(Printer.asset).joinedload(Asset.assettype),
|
||||
joinedload(Printer.asset).joinedload(Asset.location),
|
||||
).limit(10).all()
|
||||
).order_by(Printer.hostname, Printer.printerid).limit(10).all()
|
||||
|
||||
for printer in printers:
|
||||
if printer.asset and printer.asset.isactive:
|
||||
@@ -689,7 +701,8 @@ def _search_hostnames(query, search_term):
|
||||
).options(
|
||||
joinedload(NetworkDevice.asset).joinedload(Asset.assettype),
|
||||
joinedload(NetworkDevice.asset).joinedload(Asset.location),
|
||||
).limit(10).all()
|
||||
).order_by(NetworkDevice.hostname,
|
||||
NetworkDevice.networkdeviceid).limit(10).all()
|
||||
|
||||
for device in devices:
|
||||
if device.asset and device.asset.isactive:
|
||||
@@ -773,7 +786,7 @@ def _search_vendor_model_type(query, search_term):
|
||||
Asset.isactive == True,
|
||||
_word_match(query, Vendor.vendor, Model.modelnumber,
|
||||
MachineType.machinetype)
|
||||
).limit(10).all()
|
||||
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
|
||||
|
||||
for asset in machine_assets:
|
||||
results.append(_get_asset_result(asset, query, 30))
|
||||
@@ -801,7 +814,7 @@ def _search_vendor_model_type(query, search_term):
|
||||
Asset.isactive == True,
|
||||
_word_match(query, Vendor.vendor, Model.modelnumber,
|
||||
PrinterType.printertype)
|
||||
).limit(10).all()
|
||||
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
|
||||
|
||||
for asset in printer_assets:
|
||||
results.append(_get_asset_result(asset, query, 30))
|
||||
@@ -827,7 +840,7 @@ def _search_vendor_model_type(query, search_term):
|
||||
Asset.isactive == True,
|
||||
_word_match(query, Vendor.vendor,
|
||||
NetworkDeviceType.networkdevicetype)
|
||||
).limit(10).all()
|
||||
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
|
||||
|
||||
for asset in netdev_assets:
|
||||
results.append(_get_asset_result(asset, query, 30))
|
||||
|
||||
Reference in New Issue
Block a user