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
4 changes: 2 additions & 2 deletions api/tests/unittest/test_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
status="active",
provider="test_provider",
feed_name="test_feed_name",
created_at=datetime.fromisoformat("2023-07-10T22:06:00Z"),
created_at=datetime.fromisoformat("2023-07-10T22:06:00+00:00"),
note="test_note",
feed_contact_email="test_feed_contact_email",
producer_url="test_producer_url",
Expand Down Expand Up @@ -340,5 +340,5 @@ def assert_gtfs_rt(gtfs_rt_feed, response_gtfs_rt_feed):
)
assert (
response_gtfs_rt_feed["feed_references"][0] == gtfs_rt_feed.gtfs_feeds[0].stable_id
), f'Response feed feed reference was {response_gtfs_rt_feed["feed_references"][0]} instead of test_feed_reference'
), f'response feed feed reference was {response_gtfs_rt_feed["feed_references"][0]} instead of test_feed_reference'
assert response_gtfs_rt_feed["created_at"] is not None, "Response feed created_at was None"
6 changes: 6 additions & 0 deletions functions/packages/feed-form/src/__tests__/feed-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const sampleRequestBodyGTFS: FeedSubmissionFormRequestBody = {
userInterviewEmail: "interviewee@example.com",
whatToolsUsedText: "Google Sheets, Node.js",
hasLogoPermission: "yes",
unofficialDesc: "For research purposes",
updateFreq: "every month",
emptyLicenseUsage: "unsure",
};

const sampleRequestBodyGTFSRT: FeedSubmissionFormRequestBody = {
Expand Down Expand Up @@ -138,6 +141,9 @@ describe("Feed Form Implementation", () => {
[SheetCol.LinkToAssociatedGTFS]:
sampleRequestBodyGTFS.gtfsRelatedScheduleLink,
[SheetCol.LogoPermission]: sampleRequestBodyGTFS.hasLogoPermission,
[SheetCol.UnofficialDesc]: sampleRequestBodyGTFS.unofficialDesc,
[SheetCol.UpdateFreq]: sampleRequestBodyGTFS.updateFreq,
[SheetCol.EmptyLicenseUsage]: sampleRequestBodyGTFS.emptyLicenseUsage,
[SheetCol.OfficialFeedSource]: sampleRequestBodyGTFS.isOfficialFeed,
});
});
Expand Down
6 changes: 6 additions & 0 deletions functions/packages/feed-form/src/impl/feed-form-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export enum SheetCol {
ToolsAndSupport = "What tools and support do you use to create your GTFS data?",
LinkToAssociatedGTFS = "Link to associated GTFS Schedule feed",
LogoPermission = "Do we have permission to share your logo on https://mobilitydatabase.org/contribute?",
UnofficialDesc = "Why was this feed created?",
UpdateFreq = "How often is this feed updated?",
EmptyLicenseUsage = "Feed intended for trip planners/third parties?",
}

/**
Expand Down Expand Up @@ -195,6 +198,9 @@ export function buildFeedRow(
[SheetCol.ToolsAndSupport]: formData.whatToolsUsedText ?? "",
[SheetCol.LinkToAssociatedGTFS]: formData.gtfsRelatedScheduleLink ?? "",
[SheetCol.LogoPermission]: formData.hasLogoPermission,
[SheetCol.UnofficialDesc]: formData.unofficialDesc ?? "",
[SheetCol.UpdateFreq]: formData.updateFreq ?? "",
[SheetCol.EmptyLicenseUsage]: formData.emptyLicenseUsage ?? "",
};
}

Expand Down
3 changes: 3 additions & 0 deletions functions/packages/feed-form/src/impl/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ export interface FeedSubmissionFormRequestBody {
userInterviewEmail?: string;
whatToolsUsedText?: string;
hasLogoPermission: YesNoFormInput;
unofficialDesc?: string;
updateFreq?: string;
emptyLicenseUsage?: string;
}
95 changes: 72 additions & 23 deletions web-app/cypress/e2e/addFeedForm.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,37 @@ describe('Add Feed Form', () => {

describe('Success Flows', () => {
it('should submit a new gtfs scheduled feed as official producer', () => {
cy.get('[data-cy=isOfficialProducerYes]').click({
force: true,
});
cy.get('[data-cy=isOfficialProducerYes]').click({ force: true });
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
// step 2
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
cy.get('[data-cy=secondStepSubmit]').click();
cy.url().should('include', '/contribute?step=3');
// step 3
// step 3: fill required emptyLicenseUsage if present
cy.get('body').then($body => {
if ($body.find('[data-cy="emptyLicenseUsage"]').length) {
cy.get('[data-cy="emptyLicenseUsage"]').click();
cy.get('li').should('have.length.at.least', 1);
cy.get('li').then($lis => {
const texts = $lis.map((i, el) => el.textContent).get();
cy.log('Dropdown options:', texts.join(', '));
expect(texts).to.include('Not sure');
});
cy.contains('li', 'Not sure').click();
}
});
cy.get('[data-cy=thirdStepSubmit]').click();
cy.url().should('include', '/contribute?step=4');
// step 4
cy.get('[data-cy=dataProducerEmail] input').type('audio@stm.com', {
force: true,
});
cy.get('[data-cy=dataProducerEmail] input').type('audio@stm.com', { force: true });
cy.muiDropdownSelect('[data-cy=interestedInAudit]', 'no');
cy.muiDropdownSelect('[data-cy=logoPermission]', 'yes');
cy.get('[data-cy=fourthStepSubmit]').click();
cy.url().should('include', 'contribute/submitted');
//success check
// success check
cy.get('[data-cy=feedSubmitSuccess]').should('exist');
});

Expand Down Expand Up @@ -80,9 +86,7 @@ describe('Add Feed Form', () => {
// Step 1 values
cy.get('[data-cy=isOfficialProducerYes]').click();
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=oldFeedLink] input').type('https://example.com/feedOld');
cy.get('[data-cy=submitFirstStep]').click();
// Step 2
Expand All @@ -92,18 +96,17 @@ describe('Add Feed Form', () => {
// Step 2 values
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
cy.get('[data-cy=secondStepSubmit]').click();
// Step 3
cy.muiDropdownSelect('[data-cy=isAuthRequired]', 'choiceRequired');
// Step 3: fill required emptyLicenseUsage if present
cy.get('[data-cy=thirdStepSubmit]').click();
cy.assetMuiError('[data-cy=authTypeLabel]');
cy.assetMuiError('[data-cy=authSignupLabel]');
// Step 3 values
cy.muiDropdownSelect('[data-cy=isAuthRequired]', 'None - 0');
cy.get('[data-cy="emptyLicenseUsage"]')
.parents('.MuiFormControl-root')
.find('.MuiFormHelperText-root')
.should('contain', 'required');
cy.muiDropdownSelect('[data-cy=emptyLicenseUsage]', 'yes');

cy.get('[data-cy=thirdStepSubmit]').click();
// Step 4
cy.get('[data-cy=fourthStepSubmit]').click();
cy.assetMuiError('[data-cy=dataAuditLabel]');
cy.assetMuiError('[data-cy=logoPermissionLabel]');
cy.get('[data-cy=fourthStepSubmit]').should('exist');
});

it('should display errors for gtfs-realtime feed', () => {
Expand All @@ -124,4 +127,50 @@ describe('Add Feed Form', () => {
cy.assetMuiError('[data-cy=vehiclePositionLabel]');
});
});

it('should display and submit unofficialDesc and updateFreq fields when not official feed', () => {
cy.get('[data-cy=isOfficialProducerNo]').click();
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'no');
// Check that the new fields appear
cy.get('[data-cy=unofficialDesc]').should('exist');
cy.get('[data-cy=updateFreq]').should('exist');
// Fill in the new fields (ensure only one element is targeted)
cy.get('[data-cy=unofficialDesc] textarea').first().type('For research purposes', { force: true });
cy.get('[data-cy=updateFreq] input').first().type('every month', { force: true });
// Continue with the rest of the form
cy.muiDropdownSelect('[data-cy=dataType]', 'gtfs');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
});

it('should show and require emptyLicenseUsage with Unsure option if official producer and no license', () => {
cy.get('[data-cy=isOfficialProducerYes]').click();
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
// step 2: leave license blank
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
cy.get('[data-cy=secondStepSubmit]').click();
cy.url().should('include', '/contribute?step=3');
// step 3: should see emptyLicenseUsage select
cy.get('[data-cy="emptyLicenseUsage"]').should('exist');
cy.get('[data-cy="emptyLicenseUsageLabel"]').should(
'contain',
'Can this feed be used commercially by trip planners and other third parties?',
);
// Open dropdown and check options with debug output
cy.get('[data-cy="emptyLicenseUsage"]').click();
cy.get('li').should('have.length.at.least', 1);
cy.get('li').then($lis => {
const texts = $lis.map((i, el) => el.textContent).get();
// Debug output
cy.log('Dropdown options:', texts.join(', '));
expect(texts).to.include('Not sure');
});
cy.contains('li', 'Not sure').click();
cy.get('[data-cy="thirdStepSubmit"]').click();
cy.url().should('include', '/contribute?step=4');
});
});
16 changes: 14 additions & 2 deletions web-app/public/locales/en/feeds.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@
},
"errorSubmitting": "An error occurred while submitting the form.",
"submittingFeed": "Submitting the feed...",
"errorUrl": "The URL must start with a valid protocol: http:// or https://"
"errorUrl": "The URL must start with a valid protocol: http:// or https://",
"unofficialDesc": "Why was this feed created?",
"unofficialDescPlaceholder": "Does this feed exist for research purposes, a specific app, etc?",
"updateFreq": "How often is this feed updated?",
"updateFreqPlaceholder": "Never, every month, automatically via a script, etc"
},
"seeFullList": "See full list",
"hideFullList": "Hide full list",
Expand Down Expand Up @@ -146,5 +150,13 @@
},
"viewRealtimeVisualization": "View real-time visualization",
"versions": "Versions",
"dataAattribution": "Transit data provided by"
"dataAattribution": "Transit data provided by",
"emptyLicenseUsage": "Can this feed be used commercially by trip planners and other third parties?",
"common": {
"form": {
"yes": "Yes",
"no": "No",
"notSure": "Unsure"
}
}
}
51 changes: 51 additions & 0 deletions web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface FeedSubmissionFormFormInputFirstStep {
feedLink?: string;
oldFeedLink?: string;
isUpdatingFeed: YesNoFormInput;
unofficialDesc?: string;
updateFreq?: string;
}

interface FormFirstStepProps {
Expand Down Expand Up @@ -59,6 +61,8 @@ export default function FormFirstStep({
feedLink: initialValues.feedLink,
oldFeedLink: initialValues.oldFeedLink,
isUpdatingFeed: initialValues.isUpdatingFeed,
unofficialDesc: initialValues.unofficialDesc,
updateFreq: initialValues.updateFreq,
},
});

Expand Down Expand Up @@ -91,6 +95,11 @@ export default function FormFirstStep({
name: 'isOfficialProducer',
});

const isOfficialFeed = useWatch({
control,
name: 'isOfficialFeed',
});

useEffect(() => {
setNumberOfSteps(isOfficialProducer);
}, [isOfficialProducer]);
Expand Down Expand Up @@ -172,6 +181,48 @@ export default function FormFirstStep({
/>
</FormControl>
</Grid>

{/* New fields for unofficial feeds, moved right after isOfficialFeed */}
{isOfficialFeed === 'no' && (
<>
<Grid item>
<FormControl component='fieldset' fullWidth>
<FormLabel>{t('form.unofficialDesc')}</FormLabel>
<Controller
control={control}
name='unofficialDesc'
render={({ field }) => (
<TextField
{...field}
className='md-small-input'
multiline
minRows={2}
placeholder={t('form.unofficialDescPlaceholder')}
data-cy='unofficialDesc'
/>
)}
/>
</FormControl>
</Grid>
<Grid item>
<FormControl component='fieldset' fullWidth>
<FormLabel>{t('form.updateFreq')}</FormLabel>
<Controller
control={control}
name='updateFreq'
render={({ field }) => (
<TextField
{...field}
className='md-small-input'
placeholder={t('form.updateFreqPlaceholder')}
data-cy='updateFreq'
/>
)}
/>
</FormControl>
</Grid>
</>
)}
<Grid item>
<FormControl component='fieldset'>
<FormLabel required>{t('dataType')}</FormLabel>
Expand Down
29 changes: 29 additions & 0 deletions web-app/src/app/screens/FeedSubmission/Form/SecondStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface FeedSubmissionFormInputSecondStep {
region: string;
municipality: string;
name: string;
licensePath?: string;
}

interface FormSecondStepProps {
Expand All @@ -47,6 +48,7 @@ export default function FormSecondStep({
region: initialValues.region,
municipality: initialValues.municipality,
name: initialValues.name,
licensePath: initialValues.licensePath,
},
});
const onSubmit: SubmitHandler<FeedSubmissionFormInputSecondStep> = (data) => {
Expand Down Expand Up @@ -137,6 +139,33 @@ export default function FormSecondStep({
/>
</FormControl>
</Grid>
<Grid item>
<FormControl
component='fieldset'
fullWidth
error={errors.licensePath !== undefined}
>
<FormLabel component='legend'>{t('linkToLicense')}</FormLabel>
<Controller
rules={{
validate: (value) => {
if (value === '' || value === undefined) return true;
return /^https?:\/\//.test(value) || t('form.errorUrl');
},
}}
control={control}
name='licensePath'
render={({ field }) => (
<TextField
className='md-small-input'
{...field}
helperText={errors.licensePath?.message ?? ''}
error={errors.licensePath !== undefined}
/>
)}
/>
</FormControl>
</Grid>

<Grid container spacing={2}>
<Grid item>
Expand Down
Loading