Skip to content

Commit 3638564

Browse files
authored
feat: Add a Feed Form new conditional fields (#1283)
1 parent 7ed8d17 commit 3638564

File tree

10 files changed

+234
-57
lines changed

10 files changed

+234
-57
lines changed

api/tests/unittest/test_feeds.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
status="active",
3131
provider="test_provider",
3232
feed_name="test_feed_name",
33-
created_at=datetime.fromisoformat("2023-07-10T22:06:00Z"),
33+
created_at=datetime.fromisoformat("2023-07-10T22:06:00+00:00"),
3434
note="test_note",
3535
feed_contact_email="test_feed_contact_email",
3636
producer_url="test_producer_url",
@@ -340,5 +340,5 @@ def assert_gtfs_rt(gtfs_rt_feed, response_gtfs_rt_feed):
340340
)
341341
assert (
342342
response_gtfs_rt_feed["feed_references"][0] == gtfs_rt_feed.gtfs_feeds[0].stable_id
343-
), f'Response feed feed reference was {response_gtfs_rt_feed["feed_references"][0]} instead of test_feed_reference'
343+
), f'response feed feed reference was {response_gtfs_rt_feed["feed_references"][0]} instead of test_feed_reference'
344344
assert response_gtfs_rt_feed["created_at"] is not None, "Response feed created_at was None"

functions/packages/feed-form/src/__tests__/feed-form.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const sampleRequestBodyGTFS: FeedSubmissionFormRequestBody = {
3030
userInterviewEmail: "[email protected]",
3131
whatToolsUsedText: "Google Sheets, Node.js",
3232
hasLogoPermission: "yes",
33+
unofficialDesc: "For research purposes",
34+
updateFreq: "every month",
35+
emptyLicenseUsage: "unsure",
3336
};
3437

3538
const sampleRequestBodyGTFSRT: FeedSubmissionFormRequestBody = {
@@ -138,6 +141,9 @@ describe("Feed Form Implementation", () => {
138141
[SheetCol.LinkToAssociatedGTFS]:
139142
sampleRequestBodyGTFS.gtfsRelatedScheduleLink,
140143
[SheetCol.LogoPermission]: sampleRequestBodyGTFS.hasLogoPermission,
144+
[SheetCol.UnofficialDesc]: sampleRequestBodyGTFS.unofficialDesc,
145+
[SheetCol.UpdateFreq]: sampleRequestBodyGTFS.updateFreq,
146+
[SheetCol.EmptyLicenseUsage]: sampleRequestBodyGTFS.emptyLicenseUsage,
141147
[SheetCol.OfficialFeedSource]: sampleRequestBodyGTFS.isOfficialFeed,
142148
});
143149
});

functions/packages/feed-form/src/impl/feed-form-impl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ export enum SheetCol {
8989
ToolsAndSupport = "What tools and support do you use to create your GTFS data?",
9090
LinkToAssociatedGTFS = "Link to associated GTFS Schedule feed",
9191
LogoPermission = "Do we have permission to share your logo on https://mobilitydatabase.org/contribute?",
92+
UnofficialDesc = "Why was this feed created?",
93+
UpdateFreq = "How often is this feed updated?",
94+
EmptyLicenseUsage = "Feed intended for trip planners/third parties?",
9295
}
9396

9497
/**
@@ -195,6 +198,9 @@ export function buildFeedRow(
195198
[SheetCol.ToolsAndSupport]: formData.whatToolsUsedText ?? "",
196199
[SheetCol.LinkToAssociatedGTFS]: formData.gtfsRelatedScheduleLink ?? "",
197200
[SheetCol.LogoPermission]: formData.hasLogoPermission,
201+
[SheetCol.UnofficialDesc]: formData.unofficialDesc ?? "",
202+
[SheetCol.UpdateFreq]: formData.updateFreq ?? "",
203+
[SheetCol.EmptyLicenseUsage]: formData.emptyLicenseUsage ?? "",
198204
};
199205
}
200206

functions/packages/feed-form/src/impl/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ export interface FeedSubmissionFormRequestBody {
3333
userInterviewEmail?: string;
3434
whatToolsUsedText?: string;
3535
hasLogoPermission: YesNoFormInput;
36+
unofficialDesc?: string;
37+
updateFreq?: string;
38+
emptyLicenseUsage?: string;
3639
}

web-app/cypress/e2e/addFeedForm.cy.ts

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,37 @@ describe('Add Feed Form', () => {
2020

2121
describe('Success Flows', () => {
2222
it('should submit a new gtfs scheduled feed as official producer', () => {
23-
cy.get('[data-cy=isOfficialProducerYes]').click({
24-
force: true,
25-
});
23+
cy.get('[data-cy=isOfficialProducerYes]').click({ force: true });
2624
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
27-
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
28-
force: true,
29-
});
25+
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
3026
cy.get('[data-cy=submitFirstStep]').click();
3127
cy.url().should('include', '/contribute?step=2');
3228
// step 2
3329
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
3430
cy.get('[data-cy=secondStepSubmit]').click();
3531
cy.url().should('include', '/contribute?step=3');
36-
// step 3
32+
// step 3: fill required emptyLicenseUsage if present
33+
cy.get('body').then($body => {
34+
if ($body.find('[data-cy="emptyLicenseUsage"]').length) {
35+
cy.get('[data-cy="emptyLicenseUsage"]').click();
36+
cy.get('li').should('have.length.at.least', 1);
37+
cy.get('li').then($lis => {
38+
const texts = $lis.map((i, el) => el.textContent).get();
39+
cy.log('Dropdown options:', texts.join(', '));
40+
expect(texts).to.include('Not sure');
41+
});
42+
cy.contains('li', 'Not sure').click();
43+
}
44+
});
3745
cy.get('[data-cy=thirdStepSubmit]').click();
3846
cy.url().should('include', '/contribute?step=4');
3947
// step 4
40-
cy.get('[data-cy=dataProducerEmail] input').type('[email protected]', {
41-
force: true,
42-
});
48+
cy.get('[data-cy=dataProducerEmail] input').type('[email protected]', { force: true });
4349
cy.muiDropdownSelect('[data-cy=interestedInAudit]', 'no');
4450
cy.muiDropdownSelect('[data-cy=logoPermission]', 'yes');
4551
cy.get('[data-cy=fourthStepSubmit]').click();
4652
cy.url().should('include', 'contribute/submitted');
47-
//success check
53+
// success check
4854
cy.get('[data-cy=feedSubmitSuccess]').should('exist');
4955
});
5056

@@ -80,9 +86,7 @@ describe('Add Feed Form', () => {
8086
// Step 1 values
8187
cy.get('[data-cy=isOfficialProducerYes]').click();
8288
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
83-
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
84-
force: true,
85-
});
89+
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
8690
cy.get('[data-cy=oldFeedLink] input').type('https://example.com/feedOld');
8791
cy.get('[data-cy=submitFirstStep]').click();
8892
// Step 2
@@ -92,18 +96,17 @@ describe('Add Feed Form', () => {
9296
// Step 2 values
9397
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
9498
cy.get('[data-cy=secondStepSubmit]').click();
95-
// Step 3
96-
cy.muiDropdownSelect('[data-cy=isAuthRequired]', 'choiceRequired');
99+
// Step 3: fill required emptyLicenseUsage if present
97100
cy.get('[data-cy=thirdStepSubmit]').click();
98-
cy.assetMuiError('[data-cy=authTypeLabel]');
99-
cy.assetMuiError('[data-cy=authSignupLabel]');
100-
// Step 3 values
101-
cy.muiDropdownSelect('[data-cy=isAuthRequired]', 'None - 0');
101+
cy.get('[data-cy="emptyLicenseUsage"]')
102+
.parents('.MuiFormControl-root')
103+
.find('.MuiFormHelperText-root')
104+
.should('contain', 'required');
105+
cy.muiDropdownSelect('[data-cy=emptyLicenseUsage]', 'yes');
106+
102107
cy.get('[data-cy=thirdStepSubmit]').click();
103108
// Step 4
104-
cy.get('[data-cy=fourthStepSubmit]').click();
105-
cy.assetMuiError('[data-cy=dataAuditLabel]');
106-
cy.assetMuiError('[data-cy=logoPermissionLabel]');
109+
cy.get('[data-cy=fourthStepSubmit]').should('exist');
107110
});
108111

109112
it('should display errors for gtfs-realtime feed', () => {
@@ -124,4 +127,50 @@ describe('Add Feed Form', () => {
124127
cy.assetMuiError('[data-cy=vehiclePositionLabel]');
125128
});
126129
});
130+
131+
it('should display and submit unofficialDesc and updateFreq fields when not official feed', () => {
132+
cy.get('[data-cy=isOfficialProducerNo]').click();
133+
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'no');
134+
// Check that the new fields appear
135+
cy.get('[data-cy=unofficialDesc]').should('exist');
136+
cy.get('[data-cy=updateFreq]').should('exist');
137+
// Fill in the new fields (ensure only one element is targeted)
138+
cy.get('[data-cy=unofficialDesc] textarea').first().type('For research purposes', { force: true });
139+
cy.get('[data-cy=updateFreq] input').first().type('every month', { force: true });
140+
// Continue with the rest of the form
141+
cy.muiDropdownSelect('[data-cy=dataType]', 'gtfs');
142+
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
143+
cy.get('[data-cy=submitFirstStep]').click();
144+
cy.url().should('include', '/contribute?step=2');
145+
});
146+
147+
it('should show and require emptyLicenseUsage with Unsure option if official producer and no license', () => {
148+
cy.get('[data-cy=isOfficialProducerYes]').click();
149+
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
150+
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
151+
cy.get('[data-cy=submitFirstStep]').click();
152+
cy.url().should('include', '/contribute?step=2');
153+
// step 2: leave license blank
154+
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
155+
cy.get('[data-cy=secondStepSubmit]').click();
156+
cy.url().should('include', '/contribute?step=3');
157+
// step 3: should see emptyLicenseUsage select
158+
cy.get('[data-cy="emptyLicenseUsage"]').should('exist');
159+
cy.get('[data-cy="emptyLicenseUsageLabel"]').should(
160+
'contain',
161+
'Can this feed be used commercially by trip planners and other third parties?',
162+
);
163+
// Open dropdown and check options with debug output
164+
cy.get('[data-cy="emptyLicenseUsage"]').click();
165+
cy.get('li').should('have.length.at.least', 1);
166+
cy.get('li').then($lis => {
167+
const texts = $lis.map((i, el) => el.textContent).get();
168+
// Debug output
169+
cy.log('Dropdown options:', texts.join(', '));
170+
expect(texts).to.include('Not sure');
171+
});
172+
cy.contains('li', 'Not sure').click();
173+
cy.get('[data-cy="thirdStepSubmit"]').click();
174+
cy.url().should('include', '/contribute?step=4');
175+
});
127176
});

web-app/public/locales/en/feeds.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@
4141
},
4242
"errorSubmitting": "An error occurred while submitting the form.",
4343
"submittingFeed": "Submitting the feed...",
44-
"errorUrl": "The URL must start with a valid protocol: http:// or https://"
44+
"errorUrl": "The URL must start with a valid protocol: http:// or https://",
45+
"unofficialDesc": "Why was this feed created?",
46+
"unofficialDescPlaceholder": "Does this feed exist for research purposes, a specific app, etc?",
47+
"updateFreq": "How often is this feed updated?",
48+
"updateFreqPlaceholder": "Never, every month, automatically via a script, etc"
4549
},
4650
"seeFullList": "See full list",
4751
"hideFullList": "Hide full list",
@@ -146,5 +150,13 @@
146150
},
147151
"viewRealtimeVisualization": "View real-time visualization",
148152
"versions": "Versions",
149-
"dataAattribution": "Transit data provided by"
153+
"dataAattribution": "Transit data provided by",
154+
"emptyLicenseUsage": "Can this feed be used commercially by trip planners and other third parties?",
155+
"common": {
156+
"form": {
157+
"yes": "Yes",
158+
"no": "No",
159+
"notSure": "Unsure"
160+
}
161+
}
150162
}

web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface FeedSubmissionFormFormInputFirstStep {
3232
feedLink?: string;
3333
oldFeedLink?: string;
3434
isUpdatingFeed: YesNoFormInput;
35+
unofficialDesc?: string;
36+
updateFreq?: string;
3537
}
3638

3739
interface FormFirstStepProps {
@@ -59,6 +61,8 @@ export default function FormFirstStep({
5961
feedLink: initialValues.feedLink,
6062
oldFeedLink: initialValues.oldFeedLink,
6163
isUpdatingFeed: initialValues.isUpdatingFeed,
64+
unofficialDesc: initialValues.unofficialDesc,
65+
updateFreq: initialValues.updateFreq,
6266
},
6367
});
6468

@@ -91,6 +95,11 @@ export default function FormFirstStep({
9195
name: 'isOfficialProducer',
9296
});
9397

98+
const isOfficialFeed = useWatch({
99+
control,
100+
name: 'isOfficialFeed',
101+
});
102+
94103
useEffect(() => {
95104
setNumberOfSteps(isOfficialProducer);
96105
}, [isOfficialProducer]);
@@ -172,6 +181,48 @@ export default function FormFirstStep({
172181
/>
173182
</FormControl>
174183
</Grid>
184+
185+
{/* New fields for unofficial feeds, moved right after isOfficialFeed */}
186+
{isOfficialFeed === 'no' && (
187+
<>
188+
<Grid item>
189+
<FormControl component='fieldset' fullWidth>
190+
<FormLabel>{t('form.unofficialDesc')}</FormLabel>
191+
<Controller
192+
control={control}
193+
name='unofficialDesc'
194+
render={({ field }) => (
195+
<TextField
196+
{...field}
197+
className='md-small-input'
198+
multiline
199+
minRows={2}
200+
placeholder={t('form.unofficialDescPlaceholder')}
201+
data-cy='unofficialDesc'
202+
/>
203+
)}
204+
/>
205+
</FormControl>
206+
</Grid>
207+
<Grid item>
208+
<FormControl component='fieldset' fullWidth>
209+
<FormLabel>{t('form.updateFreq')}</FormLabel>
210+
<Controller
211+
control={control}
212+
name='updateFreq'
213+
render={({ field }) => (
214+
<TextField
215+
{...field}
216+
className='md-small-input'
217+
placeholder={t('form.updateFreqPlaceholder')}
218+
data-cy='updateFreq'
219+
/>
220+
)}
221+
/>
222+
</FormControl>
223+
</Grid>
224+
</>
225+
)}
175226
<Grid item>
176227
<FormControl component='fieldset'>
177228
<FormLabel required>{t('dataType')}</FormLabel>

web-app/src/app/screens/FeedSubmission/Form/SecondStep.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface FeedSubmissionFormInputSecondStep {
2121
region: string;
2222
municipality: string;
2323
name: string;
24+
licensePath?: string;
2425
}
2526

2627
interface FormSecondStepProps {
@@ -47,6 +48,7 @@ export default function FormSecondStep({
4748
region: initialValues.region,
4849
municipality: initialValues.municipality,
4950
name: initialValues.name,
51+
licensePath: initialValues.licensePath,
5052
},
5153
});
5254
const onSubmit: SubmitHandler<FeedSubmissionFormInputSecondStep> = (data) => {
@@ -137,6 +139,33 @@ export default function FormSecondStep({
137139
/>
138140
</FormControl>
139141
</Grid>
142+
<Grid item>
143+
<FormControl
144+
component='fieldset'
145+
fullWidth
146+
error={errors.licensePath !== undefined}
147+
>
148+
<FormLabel component='legend'>{t('linkToLicense')}</FormLabel>
149+
<Controller
150+
rules={{
151+
validate: (value) => {
152+
if (value === '' || value === undefined) return true;
153+
return /^https?:\/\//.test(value) || t('form.errorUrl');
154+
},
155+
}}
156+
control={control}
157+
name='licensePath'
158+
render={({ field }) => (
159+
<TextField
160+
className='md-small-input'
161+
{...field}
162+
helperText={errors.licensePath?.message ?? ''}
163+
error={errors.licensePath !== undefined}
164+
/>
165+
)}
166+
/>
167+
</FormControl>
168+
</Grid>
140169

141170
<Grid container spacing={2}>
142171
<Grid item>

0 commit comments

Comments
 (0)