New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PD: international opt-in form #23235
Conversation
} | ||
|
||
test 'create creates a new international optin' do | ||
assert_creates Pd::InternationalOptin do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why this fails...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the API call is failing for some reason? I'd suggest moving the assert_response
inside the assert_creates
so we first assert the successful response and then assert that it created a new model instance.
Also does assert_response :created
(201) work? I think we return 200 :success
. In any case, I suspect we're failing to create and returning a 400 or other error code. See https://www.rubydoc.info/gems/rack/Rack/Utils for the list of codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion, it helped narrow down the problems (which were a failure to name the class properly and a failure to underscore the parameters).
class Pd::InternationalOptin < ApplicationRecord | ||
include Pd::Form | ||
|
||
belongs_to :user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not seeing an associated user_id
in the database after submitting this form, even when signed in. Is there something else needed to make the association work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The association is fine, but its value is not being set. It should be set on initialization, here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is user_id
required? If so we should also probably update the db column to not null, add a required validation, and controller auth.
</Row> | ||
</FormGroup> | ||
{ | ||
this.buildSelectFieldGroupFromOptions({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though these dropdowns are shown with a default selection, pressing Submit causes them to be highlighted as required, indicating that there's no known selection at this point. Is there a preferred solution?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no default value in React component props, and it will render as empty string initially. I think at that point, the browser shows the first element selected but it doesn't trigger a React event so doesn't set the value. Either initialize formData
in the controller to have these initial values, or add a placeholder here
Conflicts: dashboard/db/schema.rb
apps/Gruntfile.js
Outdated
@@ -513,6 +513,8 @@ describe('entry tests', () => { | |||
'pd/professional_learning_landing/index': './src/sites/studio/pages/pd/professional_learning_landing/index.js', | |||
'pd/regional_partner_contact/new': './src/sites/studio/pages/pd/regional_partner_contact/new.js', | |||
|
|||
'pd/international_optin/new': './src/sites/studio/pages/pd/international_optin/new.js', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/optin/opt_in
And elsewhere. optin isn't a word, nor a common abbreviation AFAIK, so I'd go with opt_in
and OptIn
over optin
and Optin
//Object.assign(formData['form_data'], this.getDistrictData()); | ||
|
||
return formData; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of this override if it just calls super and returns the result?
</Row> | ||
</FormGroup> | ||
{ | ||
this.buildSelectFieldGroupFromOptions({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no default value in React component props, and it will render as empty string initially. I think at that point, the browser shows the first element selected but it doesn't trigger a React event so doesn't set the value. Either initialize formData
in the controller to have these initial values, or add a placeholder here
class Pd::InternationalOptin < ApplicationRecord | ||
include Pd::Form | ||
|
||
belongs_to :user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The association is fine, but its value is not being set. It should be set on initialization, here.
class Pd::InternationalOptin < ApplicationRecord | ||
include Pd::Form | ||
|
||
belongs_to :user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is user_id
required? If so we should also probably update the db column to not null, add a required validation, and controller auth.
def change | ||
create_table :pd_international_optins do |t| | ||
t.references :user, index: true | ||
t.text :form_data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is user required?
Presumably form_data is required, and should be not null.
Also please move the migration out into its own PR (per convention, in case we have to roll back).
} | ||
|
||
test 'create creates a new international optin' do | ||
assert_creates Pd::InternationalOptin do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the API call is failing for some reason? I'd suggest moving the assert_response
inside the assert_creates
so we first assert the successful response and then assert that it created a new model instance.
Also does assert_response :created
(201) work? I think we return 200 :success
. In any case, I suspect we're failing to create and returning a 400 or other error code. See https://www.rubydoc.info/gems/rack/Rack/Utils for the list of codes.
} | ||
end | ||
|
||
assert_response :bad_request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, move this inside the assert_creates
.
I think it's a clearer test (and easier to decipher results) to first check the return code, then check the side effects.
optin = build :pd_international_optin, form_data: {}.to_json | ||
refute optin.valid? | ||
|
||
assert build(:pd_international_optin, form_data: FORM_DATA.to_json).valid? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why 2 lines for refute and one line for assert?
The optin
variable from line 25 doesn't appear to be used anywhere else (after line 26, which can be combined).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch; will simplify.
No particular reason, was just basing it off of these tests.
lib/international_optin_people.rb
Outdated
@@ -0,0 +1,19 @@ | |||
INTERNATIONAL_OPTIN_FACILITATORS = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be contained in a named module, which can then be included where needed.
Then, since dashboard/lib
is autoloaded we won't need to explicitly require this file, and these constants and methods will be contained in the module and applied to the imported class. Better containment, better style :)
FormGroup | ||
} from 'react-bootstrap'; | ||
|
||
export default class InternationalOptin extends FormController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest renaming to InternationalOptIn
|
||
class InternationalOptinComponent extends FormComponent { | ||
static propTypes = { | ||
accountEmail: PropTypes.string.isRequired |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to pre-fill the name as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Is teacher name always available as separate first & last name fields on User
? Is that what ops_first_name
and ops_last_name
are?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, User.name
is whatever the user provides for Display Name in account settings.
ops_first_name
and ops_last_name
were manually supplied by our internal "ops" team via the old / deprecated ops workshop dashboard. These are not reliable and not up to date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. We won't pre-fill the name then, since we're not sure how to parse the Display Name afaik.
name: 'email', | ||
label: labels.email, | ||
type: 'text', | ||
value: this.props.accountEmail, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this a bit - there are some users that would fill out this form that do not have an email (they mistakenly created a student account way back when). Maybe make this readOnly only if we have an email?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. We will actually route them to a separate page directing them to edit their profile and add an email, similar to what we do for non-teachers and signed-out users. So anybody who actually gets to this form will have an email.
@@ -0,0 +1,17 @@ | |||
class Api::V1::Pd::InternationalOptinsController < Api::V1::Pd::FormsController | |||
def new_form | |||
@contact_form = ::Pd::InternationalOptin.new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we let anyone use this form, or only teachers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only teachers with email addresses.
cc @bencodeorg |
Checking "Other:" for one of the three choices that supports it now satisfies the requirement (for two of them) that an option is checked. This is written to the server something like this: {"form_data":{"robotics":["Other:","Wonder Workshop"],"robotics_other":"My custom information"}} Some extra work is done in the textFieldMap setup to handle the "Other:" string being localized.
Also loc'ed the three standalone HAML pages.
|
||
let textFieldMapSubjects = {}; | ||
const lastSubjectsKey = this.props.options.subjects.slice(-1)[0]; | ||
textFieldMapSubjects[lastSubjectsKey] = "other"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to change the underlying form system, but it seems this would be more robust if we identified the fields based on an identifier rather than by a user-facing string, especially if we're loc'ing like here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate? I'm not sure I understand. Do you mean here the key (option text, which does appear to be localized since it's pulled from the dynamic last value of props.options.subjects
), or the value (which will become the key suffix for the additional text field)? If the latter, can you explain why localizing the key would be helpful? If the former, what is missing? If you want it to be a constant rather than the last in options, then by all means create a new object mapping field name to the "Other" text fields and pass it through from the server in script data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, it's the key. It just seemed a bit delicate to have a key in textFieldMap
being a human-readable string (e.g. "Other:"
, especially since it will vary with language. As far as I can tell, this is implemented in FormComponent and I didn't want to change that code. Hopefully not a big deal, unless there's something unexpected in any translations...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The human-readable string is the option (out of options
passed through from the model) that gets an additional text box. It's just used for matching purposes at render time, not stored. The right-side, the value in this mapping, becomes the suffix on the new key in the resulting form_data
for the new field. Like other field names, this key is not (and should not be) localized.
Hopefully this explanation helps (from just above your link in FormComponent
):
code-dot-org/apps/src/code-studio/pd/form_components/FormComponent.jsx
Lines 234 to 236 in f0d1496
* @param {Object} textFieldMap - map specifying which answers should be followed by a text field | |
* Each key is an answer text from options. | |
* Each value is the suffix (appended to `${name}_`) which will become the name of the new text field |
If not, feel free to modify / rewrite to make it more clear.
In our case here with localized options
the key in this mapping, the option to match, must also be localized.
Backing up, another design choice originally would have been to have richer objects for options
that specified aspects of the UI including whether that particular option had an additional text box. However we added this feature after options was already just a simple array, so we chose this approach instead - to specify additional text fields as an add-on, with a mapping from the options that already existed.
Anyway, if both options
and the source for the textFieldMap
both come from the model and are localized, then what is delicate? It's only really different from the above approach in that it's distributed over 2 lists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed offline. My main concern was having JS hash key names be user-facing strings that are localised, meaning they can contain special & international characters. However, we believe that all of those are valid, so this should be fine.
|
||
let textFieldMapRobotics = {}; | ||
const lastRoboticsKey = this.props.options.robotics.slice(-1)[0]; | ||
textFieldMapRobotics[lastRoboticsKey] = "other"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this can be const since its not reassigned (let vs const is about the reference. You can still modify the object contents in an object referred to by a const variable)`
Also the declaration and assignment can be combined if you want:
const textFieldMapRobotics = {[lastRoboticsKey]: "other"};
See Computed Property Names here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
lib/international_opt_in_people.rb
Outdated
@@ -0,0 +1,13 @@ | |||
module InternationalOptInPeople |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this only used in Pd::InternationalOptIn
? Why does it need to be its own module? And why in the root lib
directory? Do we expect to share with Pegasus?
As suggested earlier, if we put it instead in dashboard/lib
we won't need this require.
Sorry @breville I forgot about this PR and just checked it out, two questions:
Asking b/c I believe this form is the way we'll establish teacher "cohorts" for international partners, so any work we can do up front to avoid problems later are appreciated. |
@bencodeorg Yes and yes, because of the ability granted only to teachers, and authorized in the controller. @breville since this is a requirement, it's probably worth adding tests for the |
Helpful, thanks @aoby! |
@aoby I added tests that the correct page is rendered in #23603, though they are for
Sorry if I've got these back-to-front. |
Heh, the names are confusing. How about this:
|
This implements a form for teachers to record their attendance at a non-U.S. workshop, at https://studio.code.org/pd/international_workshop.
Existing logged-out page (now loc'ed)
Existing not-teacher page (now loc'ed)
New teacher-with-no-email page (loc'ed)
Thanks (loc'ed)