Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). The format is based on the `RELEASE-NOTES-TEMPLATE.md` file.

## Release 2.20

## Introduction
* Product name: Open Supply Hub
* Release date: *Provide release date*

### Database changes

#### Migrations
* 0199_add_production_location_page_switch.py - Adds `enable_production_location_page` feature flag to redirect FE route of `facilities/:osID` to the `production-locations/:osID`.

### What's new
* [OSDEV-2352](https://opensupplyhub.atlassian.net/browse/OSDEV-2352) - Added feature flag named `enable_production_location_page` to enable production location pages with the new design. When the feature flag is enabled in the Django admin panel:
* Clicking on facility list items or map markers redirects to `/production-locations/:osID` instead of `/facilities/:osID`.
* If `embed=1` is present, redirect to the `/facilities/:osID?sort_by=contributors_desc&embed=1` (with preserving other URL parameters) regardless of active `enable_production_location_page` flag.
* Previously opened facility pages at `/facilities/:osID` will redirect to `/production-locations/:osID` after page refresh.
* When the feature flag is disabled, accessing `/production-locations/:osID` routes will result in a "Not found" page with no automatic redirection to the legacy `/facilities/:osID` route.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
* `delete_emailaddress_for_deleted_users`
* `migrate`
* `reindex_database`


## Release 2.19.0

## Introduction
Expand Down
12 changes: 11 additions & 1 deletion src/django/api/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from api.constants import FacilityClaimStatuses


PRODUCTION_LOCATION_PAGE_SWITCH = 'enable_production_location_page'


def make_oshub_url(request: Request):
if settings.DEBUG:
protocol = 'http'
Expand All @@ -28,8 +31,15 @@ def make_oshub_url(request: Request):


def make_facility_url(request, facility):
return '{}/facilities/{}'.format(
route_prefix = (
'production-locations'
if switch_is_active(PRODUCTION_LOCATION_PAGE_SWITCH)
else 'facilities'
)

return '{}/{}/{}'.format(
make_oshub_url(request),
route_prefix,
facility.id,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.db import migrations


SWITCH_NAME = 'enable_production_location_page'


def create_switch(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.get_or_create(
name=SWITCH_NAME,
defaults={'active': False},
)


def delete_switch(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name=SWITCH_NAME).delete()


class Migration(migrations.Migration):
"""
Migration to introduce a switch for the production location page routing.
"""

dependencies = [
('api', '0198_add_rainforest_alliance_certification'),
]

operations = [
migrations.RunPython(create_switch, delete_switch),
]
20 changes: 20 additions & 0 deletions src/react/src/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import SurveyDialogNotification from './components/SurveyDialogNotification';
import Settings from './components/Settings/Settings';
import ExternalRedirect from './components/ExternalRedirect';
import Facilities from './components/Facilities';
import ProductionLocationDetails from './components/ProductionLocation/ProductionLocationDetails';
import ContributeProductionLocation from './components/Contribute/ContributeProductionLocation';
import SearchByOsIdResult from './components/Contribute/SearchByOsIdResult';
import SearchByNameAndAddressResult from './components/Contribute/SearchByNameAndAddressResult';
Expand All @@ -55,6 +56,7 @@ import {
listsRoute,
facilityListItemsRoute,
facilitiesRoute,
productionLocationDetailsRoute,
dashboardRoute,
claimFacilityRoute,
claimIntroRoute,
Expand All @@ -69,6 +71,7 @@ import {
searchByNameAndAddressResultRoute,
productionLocationInfoRouteCreate,
productionLocationInfoRouteUpdate,
ENABLE_PRODUCTION_LOCATION_PAGE,
} from './util/constants';

// Pre-wrapping components outside of Routes to prevent redundant re-renders on component mount
Expand Down Expand Up @@ -151,6 +154,23 @@ class Routes extends Component {
path={facilitiesRoute}
component={Facilities}
/>
<Route
path={productionLocationDetailsRoute}
render={() => (
<FeatureFlag
flag={
ENABLE_PRODUCTION_LOCATION_PAGE
}
alternative={<RouteNotFound />}
>
<Route
component={
ProductionLocationDetails
}
/>
</FeatureFlag>
)}
/>
<Route
exact
path={authRegisterFormRoute}
Expand Down
45 changes: 44 additions & 1 deletion src/react/src/__tests__/utils.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ const {
slcValidationSchema,
formatExtendedField,
processDromoResults,
formatPartnerFieldValue
formatPartnerFieldValue,
shouldUseProductionLocationPage,
getFilteredSearchForEmbed,
makeFacilityDetailLinkOnRedirect,
} = require('../util/util');

const {
Expand Down Expand Up @@ -2859,3 +2862,43 @@ describe('formatPartnerFieldValue', () => {
});
});
});

describe('shouldUseProductionLocationPage', () => {
it('returns true when switch is active', () => {
const featureFlags = { flags: { enable_production_location_page: true } };
expect(shouldUseProductionLocationPage(featureFlags)).toBe(true);
});

it('returns false when switch is missing or inactive', () => {
expect(shouldUseProductionLocationPage({ flags: {} })).toBe(false);
expect(shouldUseProductionLocationPage({})).toBe(false);
});
});

describe('getFilteredSearchForEmbed', () => {
it('keeps only embed and contributor params', () => {
const search = '?embed=1&contributor=abc&sort_by=contributors_desc';
expect(getFilteredSearchForEmbed(search)).toBe('?embed=1&contributor=abc');
});

it('returns empty string when embed is absent or search is empty', () => {
expect(getFilteredSearchForEmbed('?sort_by=contributors_desc')).toBe('');
expect(getFilteredSearchForEmbed('')).toBe('');
});
});

describe('makeFacilityDetailLinkOnRedirect', () => {
it('builds facility link without embed mode', () => {
const url = makeFacilityDetailLinkOnRedirect('CN2026030PXM73F', '?foo=bar', false);
expect(url).toBe('/facilities/CN2026030PXM73F');
});

it('builds production location link with embed params only', () => {
const url = makeFacilityDetailLinkOnRedirect(
'CN2026030PXM73F',
'?embed=1&contributor=abc&sort_by=contributors_desc',
true,
);
expect(url).toBe('/production-locations/CN2026030PXM73F?embed=1&contributor=abc');
});
});
47 changes: 42 additions & 5 deletions src/react/src/components/Facilities.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import { Route, Switch, Redirect } from 'react-router-dom';
import CircularProgress from '@material-ui/core/CircularProgress';

import FeatureFlag from './FeatureFlag';
import FacilityDetails from './FacilityDetails';
import MapAndSidebar from './MapAndSidebar';

Expand All @@ -11,17 +12,50 @@ import {
facilitiesRoute,
facilityDetailsRoute,
profileRoute,
ENABLE_PRODUCTION_LOCATION_PAGE,
} from '../util/constants';

import { getFilteredSearchForEmbed, getLastPathParameter } from '../util/util';
import UserProfile from './UserProfile';

const Facilities = ({ fetchingFeatureFlags }) => {
const Facilities = ({ fetchingFeatureFlags, isEmbedded }) => {
if (fetchingFeatureFlags) {
return <CircularProgress />;
}

return (
<Switch>
<Route path={facilityDetailsRoute} component={FacilityDetails} />
<Route
path={facilityDetailsRoute}
render={props => {
const { location } = props;

const filteredSearch = getFilteredSearchForEmbed(
location.search,
);
const cleanOsID = getLastPathParameter(
location?.pathname || '',
);

if (isEmbedded) {
return <FacilityDetails {...props} />;
}

return (
<FeatureFlag
flag={ENABLE_PRODUCTION_LOCATION_PAGE}
alternative={<FacilityDetails {...props} />}
>
<Redirect
to={{
pathname: `/production-locations/${cleanOsID}`,
search: filteredSearch,
}}
/>
</FeatureFlag>
);
}}
/>
<Route path={facilitiesRoute} component={MapAndSidebar} />
<Route
path={profileRoute}
Expand All @@ -35,8 +69,11 @@ const Facilities = ({ fetchingFeatureFlags }) => {
);
};

function mapStateToProps({ featureFlags: { fetching: fetchingFeatureFlags } }) {
return { fetchingFeatureFlags };
function mapStateToProps({
featureFlags: { fetching: fetchingFeatureFlags },
embeddedMap: { embed },
}) {
return { fetchingFeatureFlags, isEmbedded: !!embed };
}

export default connect(mapStateToProps)(withQueryStringSync(Facilities));
51 changes: 36 additions & 15 deletions src/react/src/components/FacilityDetailsContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import {
getLocationWithoutEmbedParam,
formatAttribution,
formatExtendedField,
makeFacilityDetailLinkOnRedirect,
shouldUseProductionLocationPage,
getLastPathParameter,
} from '../util/util';

const detailsStyles = theme =>
Expand Down Expand Up @@ -106,18 +109,25 @@ const FacilityDetailsContent = ({
fetchFacility,
clearFacility,
history: { push },
location,
match: {
params: { osID },
},
userHasPendingFacilityClaim,
facilityIsClaimedByCurrentUser,
embedConfig,
hideSectorData,
useProductionLocationPage,
}) => {
const normalizedOsID =
getLastPathParameter(location?.pathname || '') ||
getLastPathParameter(osID) ||
osID;

useEffect(() => {
fetchFacility(Number(embed), contributors);
fetchFacility(normalizedOsID, Number(embed), contributors);
/* eslint-disable react-hooks/exhaustive-deps */
}, [osID]);
}, [normalizedOsID]);

// Clears the selected facility when unmounted
useEffect(() => () => clearFacility(), []);
Expand Down Expand Up @@ -175,17 +185,25 @@ const FacilityDetailsContent = ({
return (
<div className={classes.root}>
<p className={classes.primaryText}>
{`No facility found for OS ID ${osID}`}
{`No facility found for OS ID ${normalizedOsID}`}
</p>
</div>
);
}

if (data?.id && data?.id !== osID) {
if (data?.id && data?.id !== normalizedOsID) {
// When redirecting to a facility alias from a deleted facility,
// the OS ID in the url will not match the facility data id;
// redirect to the appropriate facility URL.
return <Redirect to={`/facilities/${data.id}`} />;
return (
<Redirect
to={makeFacilityDetailLinkOnRedirect(
data.id,
location.search,
useProductionLocationPage,
)}
/>
);
}

const isPendingClaim =
Expand Down Expand Up @@ -307,20 +325,23 @@ function mapStateToProps(
facilityIsClaimedByCurrentUser,
vectorTileFlagIsActive,
hideSectorData: embed ? config.hide_sector_data : false,
useProductionLocationPage: shouldUseProductionLocationPage(
featureFlags,
),
};
}

function mapDispatchToProps(
dispatch,
{
match: {
params: { osID },
},
},
) {
function mapDispatchToProps(dispatch) {
return {
fetchFacility: (embed, contributorId) =>
dispatch(fetchSingleFacility(osID, embed, contributorId, true)),
fetchFacility: (id, embed, contributorId) => {
const contributorValue = get(contributorId, ['0', 'value']);
const isEmbedded = embed && contributorValue ? embed : 0;
const contributors = contributorValue ? contributorId : null;

return dispatch(
fetchSingleFacility(id, isEmbedded, contributors, true),
);
},
clearFacility: () => dispatch(resetSingleFacility()),
searchForFacilities: vectorTilesAreActive =>
dispatch(
Expand Down
Loading