aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2023-01-06 20:23:45 +1100
committerTerry Truong <terry06890@gmail.com>2023-01-06 20:23:45 +1100
commitd8c29e8dcc925b6013880f66e690fa6b006d9154 (patch)
tree479cc362b33862cce89694671d7b1a24f3b8bdae
parent50fc47e6e387c3b278526ef773badf63913389d6 (diff)
Implement filtering by event categories
Filter events in display and search suggestions. Make server queries allow specification of multiple categories. Make settings modal avoid disabling all categories.
-rwxr-xr-xbackend/histplorer.py31
-rw-r--r--backend/tests/test_histplorer.py10
-rw-r--r--src/App.vue2
-rw-r--r--src/components/SearchModal.vue7
-rw-r--r--src/components/SettingsModal.vue20
-rw-r--r--src/components/TimeLine.vue3
6 files changed, 46 insertions, 27 deletions
diff --git a/backend/histplorer.py b/backend/histplorer.py
index 988b69d..a553f88 100755
--- a/backend/histplorer.py
+++ b/backend/histplorer.py
@@ -16,7 +16,8 @@ Expected HTTP query parameters:
- event: With type=info, specifies the event title to get info for
- input: With type=sugg, specifies a search string to suggest for
- limit: With type=events or type=sugg, specifies the max number of results
-- ctg: With type=events or type=sugg, specifies an event category to restrict results to
+- ctgs: With type=events or type=sugg, specifies event categories to restrict results to
+ Interpreted as a period-separated list of category names (eg: person.place). An empty string is ignored.
"""
from typing import Iterable
@@ -171,7 +172,7 @@ def handleEventsReq(params: dict[str, str], dbCur: sqlite3.Cursor) -> EventRespo
print('INFO: Invalid scale value', file=sys.stderr)
return None
# Get event category
- ctg = params['ctg'] if 'ctg' in params else None
+ ctgs = params['ctgs'].split('.') if 'ctgs' in params else None
# Get incl value
try:
incl = int(params['incl']) if 'incl' in params else None
@@ -188,7 +189,7 @@ def handleEventsReq(params: dict[str, str], dbCur: sqlite3.Cursor) -> EventRespo
print(f'INFO: Invalid results limit {resultLimit}', file=sys.stderr)
return None
#
- events = lookupEvents(start, end, scale, ctg, incl, resultLimit, dbCur)
+ events = lookupEvents(start, end, scale, ctgs, incl, resultLimit, dbCur)
unitCounts = lookupUnitCounts(start, end, scale, dbCur)
return EventResponse(events, unitCounts)
def reqParamToHistDate(s: str):
@@ -202,7 +203,7 @@ def reqParamToHistDate(s: str):
return HistDate(None, int(m.group(1)))
else:
return HistDate(True, int(m.group(1)), int(m.group(2)), int(m.group(3)))
-def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctg: str | None,
+def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctgs: list[str] | None,
incl: int | None, resultLimit: int, dbCur: sqlite3.Cursor) -> list[Event]:
""" Looks for events within a date range, in given scale,
restricted by event category, an optional particular inclusion, and a result limit """
@@ -232,9 +233,9 @@ def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctg:
constraints.append('event_disp.unit < ?')
params.append(endUnit)
# Constrain by event category
- if ctg is not None:
- constraints.append('ctg = ?')
- params.append(ctg)
+ if ctgs is not None:
+ constraints.append('ctg IN (' + ','.join('?' * len(ctgs)) + ')')
+ params.extend(ctgs)
# Add constraints to query
query2 = query
if constraints:
@@ -341,24 +342,24 @@ def handleSuggReq(params: dict[str, str], dbCur: sqlite3.Cursor):
print(f'INFO: Invalid suggestion limit {resultLimit}', file=sys.stderr)
return None
#
- ctg = params['ctg'] if 'ctg' in params else None
- return lookupSuggs(searchStr, resultLimit, ctg, dbCur)
-def lookupSuggs(searchStr: str, resultLimit: int, ctg: str | None, dbCur: sqlite3.Cursor) -> SuggResponse:
+ ctgs = params['ctgs'].split('.') if 'ctgs' in params else None
+ return lookupSuggs(searchStr, resultLimit, ctgs, dbCur)
+def lookupSuggs(searchStr: str, resultLimit: int, ctgs: list[str] | None, dbCur: sqlite3.Cursor) -> SuggResponse:
""" For a search string, returns a SuggResponse describing search suggestions """
tempLimit = resultLimit + 1 # For determining if 'more suggestions exist'
query = 'SELECT title FROM events LEFT JOIN pop ON events.id = pop.id WHERE title LIKE ?'
- if ctg is not None:
- query += ' AND ctg = ?'
- query += f' ORDER BY pop.pop DESC LIMIT + {tempLimit}'
+ if ctgs is not None:
+ query += ' AND ctg IN (' + ','.join('?' * len(ctgs)) + ')'
+ query += f' ORDER BY pop.pop DESC LIMIT {tempLimit}'
suggs: list[str] = []
# Prefix search
- params = [searchStr + '%'] + ([ctg] if ctg is not None else [])
+ params = [searchStr + '%'] + (ctgs if ctgs is not None else [])
for (title,) in dbCur.execute(query, params):
suggs.append(title)
# If insufficient results, try substring search
if len(suggs) < tempLimit:
existing = set(suggs)
- params = ['%' + searchStr + '%'] + ([ctg] if ctg is not None else [])
+ params = ['%' + searchStr + '%'] + (ctgs if ctgs is not None else [])
for (title,) in dbCur.execute(query, params):
if title not in existing:
suggs.append(title)
diff --git a/backend/tests/test_histplorer.py b/backend/tests/test_histplorer.py
index fcaafb5..8f7e281 100644
--- a/backend/tests/test_histplorer.py
+++ b/backend/tests/test_histplorer.py
@@ -12,7 +12,7 @@ def initTestDb(dbFile: str) -> None:
'INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
{
(1, 'event one', 1900, None, None, None, 0, 'event'),
- (2, 'event two', 2452594, None, 2455369, None, 3, 'human'), # 2/11/2002 to 21/06/2010
+ (2, 'event two', 2452594, None, 2455369, None, 3, 'person'), # 2/11/2002 to 21/06/2010
(3, 'event three', 2448175, 2451828, None, None, 1, 'discovery'), # 10/10/1990 til 10/10/2000
(4, 'event four', 991206, None, 1721706, None, 2, 'event'), # 10/10/-2000 to 10/10/1
(5, 'event five', 2000, None, 2001, None, 0, 'event'),
@@ -116,7 +116,7 @@ class TestHandleReq(unittest.TestCase):
'discovery', 30, 0),
])
self.assertEqual(response.unitCounts, {1900: 2, 1990: 1, 2000: 1, 2001: 1})
- response = handleReq(self.dbFile, {'QUERY_STRING': 'type=events&range=.1999-11-27&scale=1&ctg=event'})
+ response = handleReq(self.dbFile, {'QUERY_STRING': 'type=events&range=.1999-11-27&scale=1&ctgs=event'})
self.assertEqual(response.events, [
Event(4, 'event four', HistDate(False, -2000, 10, 10), None, HistDate(False, 1, 10, 10), None,
'event', 20, 1000),
@@ -139,7 +139,7 @@ class TestHandleReq(unittest.TestCase):
def test_sugg_req(self):
response = handleReq(self.dbFile, {'QUERY_STRING': 'type=sugg&input=event t'})
self.assertEqual(response, SuggResponse(['event two', 'event three'], False))
- response = handleReq(self.dbFile, {'QUERY_STRING': 'type=sugg&input=o&ctg=event'})
- self.assertEqual(response, SuggResponse(['event four', 'event one'], False))
- response = handleReq(self.dbFile, {'QUERY_STRING': 'type=sugg&input=event&ctg=event&limit=1'})
+ response = handleReq(self.dbFile, {'QUERY_STRING': 'type=sugg&input=o&ctgs=event.person'})
+ self.assertEqual(response, SuggResponse(['event four', 'event two', 'event one'], False))
+ response = handleReq(self.dbFile, {'QUERY_STRING': 'type=sugg&input=event&ctgs=event&limit=1'})
self.assertEqual(response, SuggResponse(['event four'], True))
diff --git a/src/App.vue b/src/App.vue
index 70cd390..8ca2f95 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -230,6 +230,8 @@ async function onEventDisplay(
}
lastQueriedRange = [firstDate, lastDate];
let urlParams = new URLSearchParams({
+ // Note: Intentionally not filtering by event categories (would need category-sensitive
+ // unit count data to determine when enough events have been obtained)
type: 'events',
range: `${firstDate}.${lastDate}`,
scale: String(SCALES[scaleIdx]),
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 65d2496..96d23bd 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
-import {ref, computed, onMounted, onUnmounted, PropType} from 'vue';
+import {ref, computed, onMounted, PropType} from 'vue';
import SearchIcon from './icon/SearchIcon.vue';
import InfoIcon from './icon/InfoIcon.vue';
import {HistEvent, queryServer, EventInfoJson, jsonToEventInfo, SuggResponseJson} from '../lib';
@@ -93,6 +93,11 @@ async function onInput(){
input: input.value,
limit: String(store.searchSuggLimit),
});
+ // Check if any event categories are disabled
+ if (Object.values(store.ctgs).some((b: boolean) => !b)){
+ let visibleCtgs = Object.entries(store.ctgs).filter(([, enabled]) => enabled).map(([ctg, ]) => ctg);
+ urlParams.append('ctgs', visibleCtgs.join('.'));
+ }
// Code for querying server
pendingReqParams.value = urlParams;
pendingReqInput.value = input.value;
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue
index 1ccea2a..21ac7b8 100644
--- a/src/components/SettingsModal.vue
+++ b/src/components/SettingsModal.vue
@@ -9,18 +9,18 @@
<h2 class="font-bold md:text-xl text-center pt-1 md:pt-2 md:pb-1">Categories</h2>
<ul class="px-2 grid grid-cols-3">
<!-- Row 1 -->
- <li> <label> <input type="checkbox" v-model="store.ctgs.event"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.event" :disabled="lastCtg == 'event'"
@change="onSettingChg('ctgs.event')"/> Event </label> </li>
- <li> <label> <input type="checkbox" v-model="store.ctgs.person"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.person" :disabled="lastCtg == 'person'"
@change="onSettingChg('ctgs.person')"/> Person </label> </li>
- <li> <label> <input type="checkbox" v-model="store.ctgs.work"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.work" :disabled="lastCtg == 'work'"
@change="onSettingChg('ctgs.work')"/> Work </label> </li>
<!-- Row 2 -->
- <li> <label> <input type="checkbox" v-model="store.ctgs.place"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.place" :disabled="lastCtg == 'place'"
@change="onSettingChg('ctgs.place')"/> Place </label> </li>
- <li> <label> <input type="checkbox" v-model="store.ctgs.organism"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.organism" :disabled="lastCtg == 'organism'"
@change="onSettingChg('ctgs.organism')"/> Organism </label> </li>
- <li> <label> <input type="checkbox" v-model="store.ctgs.discovery"
+ <li> <label> <input type="checkbox" v-model="store.ctgs.discovery" :disabled="lastCtg == 'discovery'"
@change="onSettingChg('ctgs.discovery')"/> Discovery </label> </li>
</ul>
</div>
@@ -88,6 +88,14 @@ const emit = defineEmits(['close']);
// Settings change handling
const saved = ref(false); // Set to true after a setting is saved
+const lastCtg = computed(() => { // When all but one category is disabled, names the remaining category
+ let enabledCtgs = Object.entries(store.ctgs).filter(([, enabled]) => enabled).map(([ctg, ]) => ctg);
+ if (enabledCtgs.length == 1){
+ return enabledCtgs[0];
+ } else {
+ return null;
+ }
+});
function onSettingChg(option: string){
store.save(option);
// Make 'Saved' indicator appear/animate
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index 13589d2..1583aef 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -444,6 +444,9 @@ const idToEvent = computed(() => { // Maps visible event IDs to HistEvents
if (!event.start.isEarlier(lastDate.value)){
break;
}
+ if ((store.ctgs as {[ctg: string]: boolean})[event.ctg] == false){
+ continue;
+ }
map.set(event.id, event);
}
return map;