Skip to content
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

Merged
merged 15 commits into from Jul 6, 2018
Merged

PD: international opt-in form #23235

merged 15 commits into from Jul 6, 2018

Conversation

breville
Copy link
Member

@breville breville commented Jun 21, 2018

This implements a form for teachers to record their attendance at a non-U.S. workshop, at https://studio.code.org/pd/international_workshop.

international_optin_wip_3

Existing logged-out page (now loc'ed)

international_optin_wip_3a

Existing not-teacher page (now loc'ed)

international_optin_wip_3b

New teacher-with-no-email page (loc'ed)

international_optin_wip_3c

Thanks (loc'ed)

international_optin_wip_3d

}

test 'create creates a new international optin' do
assert_creates Pd::InternationalOptin do
Copy link
Member Author

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...

Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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({
Copy link
Member Author

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?

Copy link
Contributor

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

@mehalshah mehalshah self-requested a review June 26, 2018 19:16
@@ -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',
Copy link
Contributor

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;
}
Copy link
Contributor

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({
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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?
Copy link
Contributor

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).

Copy link
Member Author

@breville breville Jun 27, 2018

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.

@@ -0,0 +1,19 @@
INTERNATIONAL_OPTIN_FACILITATORS = [
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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,
Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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?

Copy link
Member Author

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.

@tanyaparker
Copy link
Contributor

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";
Copy link
Member Author

@breville breville Jul 2, 2018

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.

Copy link
Contributor

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.

Copy link
Member Author

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...

Copy link
Contributor

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):

* @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.

Copy link
Member Author

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";
Copy link
Contributor

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

@@ -0,0 +1,13 @@
module InternationalOptInPeople
Copy link
Contributor

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.

@breville breville merged commit b30a86f into staging Jul 6, 2018
@breville breville deleted the international-optin-pd-form branch July 6, 2018 18:02
@bencodeorg
Copy link
Contributor

Sorry @breville I forgot about this PR and just checked it out, two questions:

  1. can only teacher accounts fill out this form?
  2. do you have to be logged in to fill it out?

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.

@aoby
Copy link
Contributor

aoby commented Jul 6, 2018

@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 InternationalOptInsController to make sure that signed in teachers can see the page, but students and sign outs can't. It's probably also worth adding an authorize to the API controller (& tests) for the same.

@bencodeorg
Copy link
Contributor

Helpful, thanks @aoby!

@breville
Copy link
Member Author

@aoby I added tests that the correct page is rendered in #23603, though they are for InternationalOptInController rather than InternationalOptInsController. Is that right?

Api::V1::Pd::InternationalOptInsController already has an authorize_resource. Should Pd::InternationalOptInController get one too?

Sorry if I've got these back-to-front.

@aoby
Copy link
Contributor

aoby commented Jul 10, 2018

Heh, the names are confusing. How about this:

  1. The front end controller (Pd::InternationalOptInController). Now that I see the tests in PD: international opt-in additional work #23603 it's clear this should not have an authorize because it renders various templates for users who are not authorized to see the form. They should not be forbidden. 👍
  2. The API controller (Api::V1::Pd::InternationalOptInController), which does use authorize_resource still should have a unit test confirming that authorization IMO - that only (logged in) teachers can create one, while students cannot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants