From 4e78087338a4704e10f81971960481e006935c11 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sat, 11 Jan 2020 02:50:55 +0000 Subject: [PATCH] Add searchable member list to webclient --- apiserver/apiserver/api/views.py | 45 ++++++++++++++------ webclient/src/App.js | 7 ++++ webclient/src/Members.js | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 webclient/src/Members.js diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index cc75e49..f5c6a62 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -20,18 +20,20 @@ class UserViewSet(viewsets.ModelViewSet): search_strings = {} def gen_search_strings(): + import time + start = time.time() + for m in models.Member.objects.all(): - string = '{} {} {} {}'.format( + string = '{} {}'.format( m.preferred_name, m.last_name, - m.first_name, - m.last_name, ).lower() search_strings[string] = m.id + + print('Generated search strings in {} s'.format(time.time() - start)) gen_search_strings() class SearchViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = [AllowMetadata | permissions.IsAuthenticated] serializer_class = serializers.OtherMemberSerializer def get_queryset(self): @@ -40,7 +42,7 @@ class SearchViewSet(viewsets.ReadOnlyModelViewSet): queryset = models.Member.objects.all() params = self.request.query_params - if 'q' in params and len(params['q']) >= 3: + if 'q' in params and len(params['q']): search = params['q'].lower() choices = search_strings.keys() @@ -48,21 +50,33 @@ class SearchViewSet(viewsets.ReadOnlyModelViewSet): results = [x for x in choices if x.startswith(search)] # then get exact substring matches results += [x for x in choices if search in x] - # then get fuzzy matches - fuzzy_results = process.extract(search, choices, limit=NUM_SEARCH_RESULTS, scorer=fuzz.token_set_ratio) - results += [x[0] for x in fuzzy_results] - # remove dupes - results = list(OrderedDict.fromkeys(results)) + if len(results) == 0 and len(search) >= 3: + # then get fuzzy matches + fuzzy_results = process.extract(search, choices, limit=NUM_SEARCH_RESULTS, scorer=fuzz.token_set_ratio) + results += [x[0] for x in fuzzy_results] + + # remove dupes, truncate list + results = list(OrderedDict.fromkeys(results))[:NUM_SEARCH_RESULTS] result_ids = [search_strings[x] for x in results] result_objects = [queryset.get(id=x) for x in result_ids] queryset = result_objects else: - queryset = queryset.order_by('-vetted_date') + queryset = queryset.order_by('-vetted_date')[:NUM_SEARCH_RESULTS] + + return queryset - return queryset[:NUM_SEARCH_RESULTS] + def list(self, request): + try: + seq = int(request.query_params.get('seq', 0)) + except ValueError: + seq = 0 + + queryset = self.get_queryset() + serializer = self.serializer_class(queryset, many=True) + return Response({'seq': seq, 'results': serializer.data}) class MemberViewSet(viewsets.ModelViewSet): @@ -82,6 +96,13 @@ class MemberViewSet(viewsets.ModelViewSet): else: return serializers.MemberSerializer + def update(self, request, *args, **kwargs): + gen_search_strings() + return super().update(request, *args, **kwargs) + def partial_update(self, request, *args, **kwargs): + gen_search_strings() + return super().partial_update(request, *args, **kwargs) + class CourseViewSet(viewsets.ModelViewSet): permission_classes = [AllowMetadata | permissions.IsAuthenticated] diff --git a/webclient/src/App.js b/webclient/src/App.js index c1e9685..bd39ba7 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -10,6 +10,7 @@ import { Cards } from './Cards.js'; import { Training } from './Training.js'; import { Courses, CourseDetail } from './Courses.js'; import { Classes, ClassDetail } from './Classes.js'; +import { Members } from './Members.js'; import { NotFound, PleaseLogin } from './Misc.js'; function App() { @@ -84,6 +85,8 @@ function App() { + + + + diff --git a/webclient/src/Members.js b/webclient/src/Members.js new file mode 100644 index 0000000..3742a5e --- /dev/null +++ b/webclient/src/Members.js @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; +import './light.css'; +import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Menu, Message, Segment, Table } from 'semantic-ui-react'; +import moment from 'moment'; +import { requester } from './utils.js'; +import { NotFound, PleaseLogin } from './Misc.js'; + +export function Members(props) { + const [members, setMembers] = useState(false); + const [search, setSearch] = useState({t: 0, v: ''}); + const { token } = props; + + useEffect(() => { + requester('/search/?seq='+search.t+'&q='+search.v, 'GET', '') + .then(res => { + if (!members || res.seq > members.seq) { + setMembers(res); + } + }) + .catch(err => { + console.log(err); + }); + }, [search]); + + return ( + +
Member List
+ + setSearch({t: e.timeStamp, v: v.value})} + aria-label='search products' + /> + +
+ {search.length ? 'Search Results' : 'Recently Vetted'} +
+ + {members ? + + + + Name + Status + Member Since + + + + + {members.results.length ? + members.results.map((x, i) => + + {x.preferred_name} {x.last_name} + {x.status} + {x.current_start_date} + + ) + : +

No Results

+ } +
+
+ : +

Loading...

+ } + +
+ ); +}; +