Add searchable member list to webclient
This commit is contained in:
parent
402ec28ff5
commit
4e78087338
|
@ -20,18 +20,20 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
search_strings = {}
|
search_strings = {}
|
||||||
def gen_search_strings():
|
def gen_search_strings():
|
||||||
|
import time
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
for m in models.Member.objects.all():
|
for m in models.Member.objects.all():
|
||||||
string = '{} {} {} {}'.format(
|
string = '{} {}'.format(
|
||||||
m.preferred_name,
|
m.preferred_name,
|
||||||
m.last_name,
|
m.last_name,
|
||||||
m.first_name,
|
|
||||||
m.last_name,
|
|
||||||
).lower()
|
).lower()
|
||||||
search_strings[string] = m.id
|
search_strings[string] = m.id
|
||||||
|
|
||||||
|
print('Generated search strings in {} s'.format(time.time() - start))
|
||||||
gen_search_strings()
|
gen_search_strings()
|
||||||
|
|
||||||
class SearchViewSet(viewsets.ReadOnlyModelViewSet):
|
class SearchViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
|
||||||
serializer_class = serializers.OtherMemberSerializer
|
serializer_class = serializers.OtherMemberSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -40,7 +42,7 @@ class SearchViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = models.Member.objects.all()
|
queryset = models.Member.objects.all()
|
||||||
params = self.request.query_params
|
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()
|
search = params['q'].lower()
|
||||||
choices = search_strings.keys()
|
choices = search_strings.keys()
|
||||||
|
|
||||||
|
@ -48,21 +50,33 @@ class SearchViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
results = [x for x in choices if x.startswith(search)]
|
results = [x for x in choices if x.startswith(search)]
|
||||||
# then get exact substring matches
|
# then get exact substring matches
|
||||||
results += [x for x in choices if search in x]
|
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
|
if len(results) == 0 and len(search) >= 3:
|
||||||
results = list(OrderedDict.fromkeys(results))
|
# 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_ids = [search_strings[x] for x in results]
|
||||||
result_objects = [queryset.get(id=x) for x in result_ids]
|
result_objects = [queryset.get(id=x) for x in result_ids]
|
||||||
|
|
||||||
queryset = result_objects
|
queryset = result_objects
|
||||||
else:
|
else:
|
||||||
queryset = queryset.order_by('-vetted_date')
|
queryset = queryset.order_by('-vetted_date')[:NUM_SEARCH_RESULTS]
|
||||||
|
|
||||||
return queryset[:NUM_SEARCH_RESULTS]
|
return queryset
|
||||||
|
|
||||||
|
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):
|
class MemberViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -82,6 +96,13 @@ class MemberViewSet(viewsets.ModelViewSet):
|
||||||
else:
|
else:
|
||||||
return serializers.MemberSerializer
|
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):
|
class CourseViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Cards } from './Cards.js';
|
||||||
import { Training } from './Training.js';
|
import { Training } from './Training.js';
|
||||||
import { Courses, CourseDetail } from './Courses.js';
|
import { Courses, CourseDetail } from './Courses.js';
|
||||||
import { Classes, ClassDetail } from './Classes.js';
|
import { Classes, ClassDetail } from './Classes.js';
|
||||||
|
import { Members } from './Members.js';
|
||||||
import { NotFound, PleaseLogin } from './Misc.js';
|
import { NotFound, PleaseLogin } from './Misc.js';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -84,6 +85,8 @@ function App() {
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
content='Members'
|
content='Members'
|
||||||
|
as={Link}
|
||||||
|
to='/members'
|
||||||
/>
|
/>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
content='Courses'
|
content='Courses'
|
||||||
|
@ -145,6 +148,10 @@ function App() {
|
||||||
<Classes token={token} />
|
<Classes token={token} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path='/members'>
|
||||||
|
<Members token={token} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path='/:page'>
|
<Route path='/:page'>
|
||||||
<NotFound />
|
<NotFound />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
72
webclient/src/Members.js
Normal file
72
webclient/src/Members.js
Normal file
|
@ -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 (
|
||||||
|
<Container>
|
||||||
|
<Header size='large'>Member List</Header>
|
||||||
|
|
||||||
|
<Input autoFocus focus icon='search'
|
||||||
|
placeholder='Search...'
|
||||||
|
value={search.v}
|
||||||
|
onChange={(e, v) => setSearch({t: e.timeStamp, v: v.value})}
|
||||||
|
aria-label='search products'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Header size='medium'>
|
||||||
|
{search.length ? 'Search Results' : 'Recently Vetted'}
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
{members ?
|
||||||
|
<Table basic='very'>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Status</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Member Since</Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{members.results.length ?
|
||||||
|
members.results.map((x, i) =>
|
||||||
|
<Table.Row key={i}>
|
||||||
|
<Table.Cell>{x.preferred_name} {x.last_name}</Table.Cell>
|
||||||
|
<Table.Cell>{x.status}</Table.Cell>
|
||||||
|
<Table.Cell>{x.current_start_date}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
:
|
||||||
|
<p>No Results</p>
|
||||||
|
}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
:
|
||||||
|
<p>Loading...</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user