diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5718e7cb0..4a1b5d32d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Enable administrators to split facility matches into new facilities [#633](https://github.com/open-apparel-registry/open-apparel-registry/pull/633)
- Log requests made with token authentication [#646](https://github.com/open-apparel-registry/open-apparel-registry/pull/646)
- `./scripts/resetdb` to expedite refreshing application data during development [#672](https://github.com/open-apparel-registry/open-apparel-registry/pull/672)
+- Enable searching by OAR ID from the facility search tab [#675](https://github.com/open-apparel-registry/open-apparel-registry/pull/675)
### Changed
- Require a token for all API endpoints [#644](https://github.com/open-apparel-registry/open-apparel-registry/pull/644)
diff --git a/src/app/src/__tests__/utils.tests.js b/src/app/src/__tests__/utils.tests.js
index 674fc5c57..d2c5aa961 100644
--- a/src/app/src/__tests__/utils.tests.js
+++ b/src/app/src/__tests__/utils.tests.js
@@ -152,7 +152,7 @@ it('gets the value from a React Select option object', () => {
it('creates a querystring from a set of filter selection', () => {
const emptyFilterSelections = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [],
contributorTypes: [],
countries: [],
@@ -163,7 +163,7 @@ it('creates a querystring from a set of filter selection', () => {
.toEqual(expectedEmptySelectionQSMatch);
const multipleFilterSelections = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [
{ value: 'foo', label: 'foo' },
{ value: 'bar', label: 'bar' },
@@ -181,7 +181,7 @@ it('creates a querystring from a set of filter selection', () => {
.toEqual(expectedMultipleFilterSelectionsMatch);
const allFilters = {
- facilityName: 'hello',
+ facilityFreeTextQuery: 'hello',
contributors: [
{ value: 'hello', label: 'hello' },
{ value: 'world', label: 'hello' },
@@ -195,7 +195,7 @@ it('creates a querystring from a set of filter selection', () => {
};
const expectedAllFiltersMatch =
- 'name=hello&contributors=hello&contributors=world'
+ 'q=hello&contributors=hello&contributors=world'
.concat('&contributor_types=foo&countries=bar');
expect(createQueryStringFromSearchFilters(allFilters))
.toEqual(expectedAllFiltersMatch);
@@ -230,7 +230,7 @@ it('checks whether the filters object has only empty values', () => {
it('creates a set of filters from a querystring', () => {
const contributorsString = '?contributors=1&contributors=2';
const expectedContributorsMatch = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [
{
value: 1,
@@ -252,7 +252,7 @@ it('creates a set of filters from a querystring', () => {
const typesString = '?contributor_types=Union&contributor_types=Service Provider';
const expectedTypesMatch = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [],
contributorTypes: [
{
@@ -274,7 +274,7 @@ it('creates a set of filters from a querystring', () => {
const countriesString = '?countries=US&countries=CN';
const expectedCountriesMatch = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [],
contributorTypes: [],
countries: [
@@ -296,7 +296,7 @@ it('creates a set of filters from a querystring', () => {
const stringWithCountriesMissing = '?contributor_types=Union&countries=';
const expectedMissingCountriesMatch = {
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: [],
contributorTypes: [
{
diff --git a/src/app/src/actions/facilities.js b/src/app/src/actions/facilities.js
index 931f54d9e..c9ecd7494 100644
--- a/src/app/src/actions/facilities.js
+++ b/src/app/src/actions/facilities.js
@@ -1,4 +1,6 @@
import { createAction } from 'redux-act';
+import noop from 'lodash/noop';
+import get from 'lodash/get';
import apiRequest from '../util/apiRequest';
@@ -6,6 +8,7 @@ import {
logErrorAndDispatchFailure,
makeGetFacilitiesURLWithQueryString,
makeGetFacilityByOARIdURL,
+ makeFacilityDetailLink,
createQueryStringFromSearchFilters,
} from '../util/util';
@@ -19,7 +22,7 @@ export const failFetchSingleFacility = createAction('FAIL_FETCH_SINGLE_FACILITY'
export const completeFetchSingleFacility = createAction('COMPLETE_FETCH_SINGLE_FACILITY');
export const resetSingleFacility = createAction('RESET_SINGLE_FACILITY');
-export function fetchFacilities() {
+export function fetchFacilities(pushNewRoute = noop) {
return (dispatch, getState) => {
dispatch(startFetchFacilities());
@@ -31,7 +34,26 @@ export function fetchFacilities() {
return apiRequest
.get(makeGetFacilitiesURLWithQueryString(qs))
- .then(({ data }) => dispatch(completeFetchFacilities(data)))
+ .then(({ data }) => {
+ const responseHasOnlyOneFacility = get(
+ data,
+ 'features',
+ [],
+ ).length === 1;
+
+ if (responseHasOnlyOneFacility) {
+ const facilityID = get(data, 'features[0].id', null);
+
+ if (!facilityID) {
+ throw new Error('No facility ID was found');
+ }
+
+ pushNewRoute(makeFacilityDetailLink(facilityID));
+ }
+
+ return data;
+ })
+ .then(data => dispatch(completeFetchFacilities(data)))
.catch(err => dispatch(logErrorAndDispatchFailure(
err,
'An error prevented fetching facilities',
diff --git a/src/app/src/actions/filters.js b/src/app/src/actions/filters.js
index c04550123..dd8b09dfc 100644
--- a/src/app/src/actions/filters.js
+++ b/src/app/src/actions/filters.js
@@ -6,7 +6,8 @@ import {
updateListWithLabels,
} from '../util/util';
-export const updateFacilityNameFilter = createAction('UPDATE_FACILITY_NAME_FILTER');
+export const updateFacilityFreeTextQueryFilter =
+ createAction('UPDATE_FACILITY_FREE_TEXT_QUERY_FILTER');
export const updateContributorFilter = createAction('UPDATE_CONTRIBUTOR_FILTER');
export const updateContributorTypeFilter = createAction('UPDATE_CONTRIBUTOR_TYPE_FILTER');
export const updateCountryFilter = createAction('UPDATE_COUNTRY_FILTER');
diff --git a/src/app/src/components/FilterSidebar.jsx b/src/app/src/components/FilterSidebar.jsx
index 9fbb0f191..af1e1fb4f 100644
--- a/src/app/src/components/FilterSidebar.jsx
+++ b/src/app/src/components/FilterSidebar.jsx
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
+import { Route } from 'react-router-dom';
import FilterSidebarGuideTab from './FilterSidebarGuideTab';
import FilterSidebarSearchTab from './FilterSidebarSearchTab';
@@ -131,7 +132,9 @@ class FilterSidebar extends Component {
case filterSidebarTabsEnum.guide:
return ;
case filterSidebarTabsEnum.search:
- return ;
+ // We wrap this component in a `Route` to give it access to `history.push`
+ // in its `mapDispatchToProps` function.
+ return ;
case filterSidebarTabsEnum.facilities:
return ;
default:
diff --git a/src/app/src/components/FilterSidebarSearchTab.jsx b/src/app/src/components/FilterSidebarSearchTab.jsx
index ce489c96c..69f2b97d9 100644
--- a/src/app/src/components/FilterSidebarSearchTab.jsx
+++ b/src/app/src/components/FilterSidebarSearchTab.jsx
@@ -10,7 +10,7 @@ import ReactSelect from 'react-select';
import FacilitySidebarSearchTabFacilitiesCount from './FacilitySidebarSearchTabFacilitiesCount';
import {
- updateFacilityNameFilter,
+ updateFacilityFreeTextQueryFilter,
updateContributorFilter,
updateContributorTypeFilter,
updateCountryFilter,
@@ -59,8 +59,8 @@ function FilterSidebarSearchTab({
contributorTypeOptions,
countryOptions,
resetFilters,
- facilityName,
- updateFacilityName,
+ facilityFreeTextQuery,
+ updateFacilityFreeTextQuery,
contributors,
updateContributor,
contributorTypes,
@@ -114,14 +114,14 @@ function FilterSidebarSearchTab({
htmlFor={FACILITIES}
className="form__label"
>
- Search a Facility Name
+ Search a Facility Name or OAR ID
@@ -243,11 +243,11 @@ FilterSidebarSearchTab.propTypes = {
contributorTypeOptions: contributorTypeOptionsPropType.isRequired,
countryOptions: countryOptionsPropType.isRequired,
resetFilters: func.isRequired,
- updateFacilityName: func.isRequired,
+ updateFacilityFreeTextQuery: func.isRequired,
updateContributor: func.isRequired,
updateContributorType: func.isRequired,
updateCountry: func.isRequired,
- facilityName: string.isRequired,
+ facilityFreeTextQuery: string.isRequired,
contributors: contributorOptionsPropType.isRequired,
contributorTypes: contributorTypeOptionsPropType.isRequired,
countries: countryOptionsPropType.isRequired,
@@ -273,7 +273,7 @@ function mapStateToProps({
},
},
filters: {
- facilityName,
+ facilityFreeTextQuery,
contributors,
contributorTypes,
countries,
@@ -289,7 +289,7 @@ function mapStateToProps({
contributorOptions,
contributorTypeOptions,
countryOptions,
- facilityName,
+ facilityFreeTextQuery,
contributors,
contributorTypes,
countries,
@@ -301,9 +301,14 @@ function mapStateToProps({
};
}
-function mapDispatchToProps(dispatch) {
+function mapDispatchToProps(dispatch, {
+ history: {
+ push,
+ },
+}) {
return {
- updateFacilityName: e => dispatch(updateFacilityNameFilter(getValueFromEvent(e))),
+ updateFacilityFreeTextQuery: e =>
+ dispatch(updateFacilityFreeTextQueryFilter(getValueFromEvent(e))),
updateContributor: v => dispatch(updateContributorFilter(v)),
updateContributorType: v => dispatch(updateContributorTypeFilter(v)),
updateCountry: v => dispatch(updateCountryFilter(v)),
@@ -311,9 +316,9 @@ function mapDispatchToProps(dispatch) {
dispatch(recordSearchTabResetButtonClick());
return dispatch(resetAllFilters());
},
- searchForFacilities: () => dispatch(fetchFacilities()),
+ searchForFacilities: () => dispatch(fetchFacilities(push)),
submitFormOnEnterKeyPress: makeSubmitFormOnEnterKeyPressFunction(
- () => dispatch(fetchFacilities()),
+ () => dispatch(fetchFacilities(push)),
),
};
}
diff --git a/src/app/src/reducers/FacilitiesReducer.js b/src/app/src/reducers/FacilitiesReducer.js
index 01be5fc62..b356a0003 100644
--- a/src/app/src/reducers/FacilitiesReducer.js
+++ b/src/app/src/reducers/FacilitiesReducer.js
@@ -16,7 +16,7 @@ import {
} from '../actions/facilities';
import {
- updateFacilityNameFilter,
+ updateFacilityFreeTextQueryFilter,
updateContributorFilter,
updateContributorTypeFilter,
updateCountryFilter,
@@ -122,7 +122,7 @@ export default createReducer({
[resetSingleFacility]: state => update(state, {
singleFacility: { $set: initialState.singleFacility },
}),
- [updateFacilityNameFilter]: clearFacilitiesDataOnFilterChange,
+ [updateFacilityFreeTextQueryFilter]: clearFacilitiesDataOnFilterChange,
[updateContributorFilter]: clearFacilitiesDataOnFilterChange,
[updateContributorTypeFilter]: clearFacilitiesDataOnFilterChange,
[updateCountryFilter]: clearFacilitiesDataOnFilterChange,
diff --git a/src/app/src/reducers/FiltersReducer.js b/src/app/src/reducers/FiltersReducer.js
index 8b33d981a..d72957e05 100644
--- a/src/app/src/reducers/FiltersReducer.js
+++ b/src/app/src/reducers/FiltersReducer.js
@@ -2,7 +2,7 @@ import { createReducer } from 'redux-act';
import update from 'immutability-helper';
import {
- updateFacilityNameFilter,
+ updateFacilityFreeTextQueryFilter,
updateContributorFilter,
updateContributorTypeFilter,
updateCountryFilter,
@@ -23,7 +23,7 @@ import {
} from '../util/util';
const initialState = Object.freeze({
- facilityName: '',
+ facilityFreeTextQuery: '',
contributors: Object.freeze([]),
contributorTypes: Object.freeze([]),
countries: Object.freeze([]),
@@ -44,8 +44,8 @@ export const maybeSetFromQueryString = field => (state, payload) => {
};
export default createReducer({
- [updateFacilityNameFilter]: (state, payload) => update(state, {
- facilityName: { $set: payload },
+ [updateFacilityFreeTextQueryFilter]: (state, payload) => update(state, {
+ facilityFreeTextQuery: { $set: payload },
}),
[updateContributorFilter]: (state, payload) => update(state, {
contributors: { $set: payload },
diff --git a/src/app/src/util/propTypes.js b/src/app/src/util/propTypes.js
index 6761883a6..8f54d6a4a 100644
--- a/src/app/src/util/propTypes.js
+++ b/src/app/src/util/propTypes.js
@@ -203,7 +203,7 @@ export const reactSelectOptionPropType = shape({
});
export const filtersPropType = shape({
- facilityName: string.isRequired,
+ facilityFreeTextQuery: string.isRequired,
contributors: arrayOf(reactSelectOptionPropType).isRequired,
contributorTypes: arrayOf(reactSelectOptionPropType).isRequired,
countries: arrayOf(reactSelectOptionPropType).isRequired,
diff --git a/src/app/src/util/util.js b/src/app/src/util/util.js
index 5648acddc..e84676ace 100644
--- a/src/app/src/util/util.js
+++ b/src/app/src/util/util.js
@@ -129,13 +129,13 @@ export const makeGetClientInfoURL = () => {
export const getValueFromObject = ({ value }) => value;
export const createQueryStringFromSearchFilters = ({
- facilityName = '',
+ facilityFreeTextQuery = '',
contributors = [],
contributorTypes = [],
countries = [],
}) => {
const inputForQueryString = Object.freeze({
- name: facilityName,
+ q: facilityFreeTextQuery,
contributors: compact(contributors.map(getValueFromObject)),
contributor_types: compact(contributorTypes.map(getValueFromObject)),
countries: compact(countries.map(getValueFromObject)),
@@ -177,14 +177,14 @@ export const createFiltersFromQueryString = (qs) => {
: qs;
const {
- name = '',
+ q: facilityFreeTextQuery = '',
contributors = [],
contributor_types: contributorTypes = [],
countries = [],
} = querystring.parse(qsToParse);
return Object.freeze({
- facilityName: name,
+ facilityFreeTextQuery,
contributors: createSelectOptionsFromParams(contributors),
contributorTypes: createSelectOptionsFromParams(contributorTypes),
countries: createSelectOptionsFromParams(countries),
diff --git a/src/app/src/util/withQueryStringSync.jsx b/src/app/src/util/withQueryStringSync.jsx
index 82a57cb88..58eccd12a 100644
--- a/src/app/src/util/withQueryStringSync.jsx
+++ b/src/app/src/util/withQueryStringSync.jsx
@@ -99,13 +99,17 @@ export default function withQueryStringSync(WrappedComponent) {
};
}
- function mapDispatchToProps(dispatch) {
+ function mapDispatchToProps(dispatch, {
+ history: {
+ push,
+ },
+ }) {
return {
hydrateFiltersFromQueryString: (qs, fetch = true) => {
dispatch(setFiltersFromQueryString(qs));
return fetch
- ? dispatch(fetchFacilities())
+ ? dispatch(fetchFacilities(push))
: null;
},
clearFacilities: () => dispatch(resetFacilities()),
diff --git a/src/django/api/constants.py b/src/django/api/constants.py
index 545aee317..18da42fd3 100644
--- a/src/django/api/constants.py
+++ b/src/django/api/constants.py
@@ -19,6 +19,7 @@ class ProcessingAction:
class FacilitiesQueryParams:
+ Q = 'q'
NAME = 'name'
CONTRIBUTORS = 'contributors'
CONTRIBUTOR_TYPES = 'contributor_types'
diff --git a/src/django/api/views.py b/src/django/api/views.py
index 9d3217c17..fa0a15c53 100644
--- a/src/django/api/views.py
+++ b/src/django/api/views.py
@@ -385,12 +385,19 @@ class FacilitiesAPIFilterBackend(BaseFilterBackend):
def get_schema_fields(self, view):
if view.action == 'list':
return [
+ coreapi.Field(
+ name='q',
+ location='query',
+ type='string',
+ required=False,
+ description='Facility Name or OAR ID',
+ ),
coreapi.Field(
name='name',
location='query',
type='string',
required=False,
- description='Facility Name',
+ description='Facility Name (DEPRECATED; use `q` instead)'
),
coreapi.Field(
name='contributors',
@@ -495,8 +502,10 @@ def list(self, request):
if not params.is_valid():
raise ValidationError(params.errors)
- name = request.query_params.get(FacilitiesQueryParams.NAME,
- None)
+ free_text_query = request.query_params.get(FacilitiesQueryParams.Q,
+ None)
+ name = request.query_params.get(FacilitiesQueryParams.NAME, None)
+
contributors = request.query_params \
.getlist(FacilitiesQueryParams.CONTRIBUTORS)
@@ -508,8 +517,15 @@ def list(self, request):
queryset = Facility.objects.all()
+ if free_text_query is not None:
+ queryset = queryset.filter(Q(name__icontains=free_text_query) |
+ Q(id__icontains=free_text_query))
+
+ # `name` is deprecated in favor of `q`. We keep `name` available for
+ # backward compatibility.
if name is not None:
- queryset = queryset.filter(name__icontains=name)
+ queryset = queryset.filter(Q(name__icontains=name) |
+ Q(id__icontains=name))
if countries is not None and len(countries):
queryset = queryset.filter(country_code__in=countries)