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

[WIP] Online editor for python #1732

Closed

Conversation

justwudi
Copy link
Contributor

Change existing form to React

The following partials have been moved to React:

  • _form.html.slim -> ProgrammingQuestionForm.jsx
    Simple form has been removed, currently it contains just a <div> with a spinner that serves as the mounting point for the React component.
    The import alert has also been moved into _props.json.jbuilder
  • _form_build_log.html.slim -> BuildLog.jsx
  • _form_templates.html.slim -> TemplatePackageView.jsx
  • _form_test_cases.html.slim -> TemplateTestCaseView.jsx

Add online editor for python test cases

Users will be able to select to either use the online editor or upload a zip package when creating the programming question. This will be stored in in the database under a new column package_type. When editing the programming question, users will not be able to switch between the two types.

The online editor can be found in OnlineEditorPythonView.jsx. This view will be displayed when the user selects python as the language. Other editor views can be added in the future for other languages.

On the backend side, Course::Assessment::Question::Programming::PackageGenerationConcern will take in the params and construct the zip package from the fields of the online editor. This concern will route to Course::Assessment::Question::Programming::Python::PackageConcern when the language is python.

In the generated zip package, the fields used to generate the package are stored in a hidden json file .meta. This .meta file is used for displaying the fields for editing as well as to check whether there are any changes to the package when updating, the package will only be re-generated when the fields are modified.

TODO:

  • Fix tests
  • Allow switching from online editor to zip package when updating
  • Allow users to select between online editor and uploading zip package when they change the programming question to another language
  • Allow users to attach other files that are used in the autograder. One challenge here is that we do not know whether to re-generate the package, maybe can store the hash of the file in the .meta file and compare them.

This closes #1568.

}

.testSelectionRuler {
border-top: 1px solid #cccccc;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color literals like #cccccc should only be used in variable declarations; they should be referred to via variable everywhere else.

border: none;
border-radius: 0;
width: 100%;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files should end with a trailing newline

}

.addNewTestButton {
border: none;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border: 0 is preferred over border: none

@@ -0,0 +1,9 @@
.addNewTestRow {
padding: 0 !important;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!important should not be used

when language.is_a?(Coursemology::Polyglot::Language::Python)
data = python_meta(attachment)
{ editor_mode: 'python', data: data } if data.present?
else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant else-clause.

end

def extract_meta(language, attachment)
case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use empty case condition, instead use an if expression.

when language.is_a?(Coursemology::Polyglot::Language::Python)
new_package = python_package(old_attachment, params)
yield new_package if new_package.present?
else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant else-clause.



def package(language, old_attachment, params)
case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use empty case condition, instead use an if expression.

extend ActiveSupport::Concern
include Course::Assessment::Question::Programming::Python::PackageConcern


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected.

:python
when language.is_a?(Coursemology::Polyglot::Language::JavaScript)
:javascript
else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant else-clause.

@@ -31,18 +33,51 @@ def display_build_log?
Course::Assessment::ProgrammingEvaluationService::Error.name
end

def editor_mode(language)
case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use empty case condition, instead use an if expression.

end
render '_props'
else
render '_props'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move render '_props' out of the conditional.

if @programming_question.import_job
@redirect_url = job_path(@programming_question.import_job)
end
render '_props'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move render '_props' out of the conditional.

render 'edit'
end
}
format.json {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using {...} for multi-line blocks.

end

respond_to do |format|
format.html {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using {...} for multi-line blocks.

@@ -22,18 +39,43 @@ def create
end

def edit
respond_to do |format|
format.html
format.json {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using {...} for multi-line blocks.

@allenwq
Copy link
Member

allenwq commented Dec 20, 2016

The PR is a bit too big. Here I give some general comments first:

  1. The front-end validation is not done yet, I can submit the form and break the UI in some browser.

  2. The fetch API does not seem to work on browsers like Safari, http://caniuse.com/#feat=fetch

  3. Our front-end code is moving to material design, better use the components there for styling for consistency. Think react-select can be replaced with material chip ?

@@ -0,0 +1,128 @@
# frozen_string_literal: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a new service for creating programming packages. Put this logic and the supporting template files with the service.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, call it package generation service or something, so that you can properly test the service.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think you should document the input format of the service and the output package hierarchy.

test_fn = " def test_#{test_type.to_s}_#{index}(self):\n"\
" self.meta['expression'] = '#{escaped_expression}'\n"\

if test[:expected].first == '\'' && test[:expected].last == '\'' ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that quote marks can only appear at the start and end of the string. Is this the intention?


if tests.blank?
zip.print " def test_public_1(self):\n"\
" self.meta['expression'] = 'This testcase has no meaning.'\n"\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These strings need to be translated. I am not sure if we need this, I think the UI should support a mode for non-autograded programming questions ?

t('course.assessment.question.programming.form.import_result.success')
def successful_import_alert(json: false)
cls = ['alert', 'alert-success']
msg = t('course.assessment.question.programming.form.import_result.success')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call it klass and message, We don't use shorthand variable names...

);
};

$.getJSON('', (data) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long does it take to build the data ? If it's not very long it would be better to put the data in the initial request and parse them in the frontend, that will make the user side feels faster.

LessonPlan pages uses 2 requests because the lesson plan data are quite large and it slows down the initial request a lot.

onSubmit={this.onSubmit.bind(this)} ref={(form) => { this.form = form; }} >
<input type='hidden' name='authenticity_token' value={formData.get('auth_token')} />

{ this.renderInputField('title', 'Title', false, 'text', question.get('title') || '') }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these strings need to be translated.


const propTypes = {
data: React.PropTypes.shape({
question: PropTypes.object.isRequired,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to validate as PropTypes.instanceOf(Immutable.Map).isRequired


componentWillReceiveProps(nextProps) {
this.isLoading = nextProps.data.isLoading;
this.isEvaluating = nextProps.data.isEvaluating;
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 needed? Possible to do without the assignment and check this.props.isLoading and this.props.isEvaluating directly?

return <div className="alert alert-warning">Please select a language.</div>;

default:
return <div className="alert alert-info">Not yet implemented :(</div>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some untranslated strings here


case actionTypes.TEMPLATE_TAB_UPDATE:
const { selected } = action;
return $$state.set('package_ui', $$state.get('package_ui').set('selected', selected));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try $$state.setIn

},
editTestsOnlineButton: {
id: 'course.assessment.question.programming.programmingQuestionForm.editTestsOnlineButton',
defaultMessage: 'Edit Tests Online',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text might not be fitting in the case where we create tests. Will "Use Test Case Editor" or just "Test Case Editor" be better?

value={this.props.data.get(field)}
onChange={this.onCodeChange.bind(this, field)}
editorProps={{$blockScrolling: true}}
setOptions={{useSoftTabs: true}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can set this as readOnly when isLoading also.

@kxmbrian
Copy link
Contributor

  1. We are not using the $$ convention anymore and are removing all instance of them. It seems it's like an arbitrary decision by react_on_rails and not a general standard.
  2. Probably better for most users if we switch to the online editor mode when a language that has one is selected.
  3. Run npm run lint from the client folder and fix the lints. You can npm run lint-fix to auto-fix some of them.
  4. Haven't looked into the details, but I can't seem to add a programming question to an autograded assessment. Do you have the same issue?
  5. Not done yet, but we'll need to switch to redux-immutable since we are using immutable.js.

@justwudi justwudi force-pushed the wudi/online-editor branch from dd84736 to 7e8f5c3 Compare January 4, 2017 04:25
meta
end

def python_test_params(params)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method has too many lines. [11/10]

tmp
end

def get_data_files_meta(data_files_to_keep, new_data_files)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment Branch Condition size for get_data_files_meta is too high. [15.3/15]

def generate_zip_file(data_files_to_keep, params)
tmp = Tempfile.new(['package', '.zip'])

Zip::OutputStream.open(tmp.path) do |zip|

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block has too many lines. [41/25]

end
end

def generate_zip_file(data_files_to_keep, params)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment Branch Condition size for generate_zip_file is too high. [66.36/15]
Method has too many lines. [54/10]

@@ -0,0 +1,221 @@
# frozen_string_literal: true
class Course::Assessment::Question::Programming::Python::PythonPackageService

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class has too many lines. [163/100]

Course::Assessment::Question::Programming::Python::PythonPackageService.new
@editor_mode = 'python'
else
raise NotImplementedError

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use fail instead of raise to signal exceptions.

@language = question.language
@current_attachment = question.attachment

if python_language

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a guard clause instead of wrapping the code inside a conditional expression.

@@ -28,6 +30,10 @@ def auto_gradable?
!test_cases.empty?
end

def edit_online?
self.package_type == 'online_editor'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant self detected.

@justwudi
Copy link
Contributor Author

justwudi commented Jan 4, 2017

New Changes

Add support for non-autograded programming question

  • Add checkbox in online editor mode for autograded
  • If unchecked, only submission template field will be shown.
  • Backend will remove any package attachments, clear existing templates, and add in the new submission template.

Add support for data files

  • Frontend
    • Current Data Files
      • Display filename and size of file
      • Checkbox to mark for deletion, default unchecked
    • New Data Files
      • List of input fields
      • New input field appears when user selected a file in the last input field
      • All input fields except for the last one has a delete button
  • Backend
    • Unzips the current package, retrieves the list of files to put in the new package (those that are not mark for deletion)
    • New files will override old files if there are filename conflicts
    • .meta stores a list of { filename, size, hash }, the hash is there so that we can make a proper comparison between the new .meta and the old .meta to determine whether there is a need to re-evaluate the package.

Multiselect

  • Implemented ChipInput, which is basically borrowed from material-ui-chip-input with minor changes to make it less buggy.
  • Kind of works, with a few minor issues:
    • After clicking on an option, the dropdown closes. So the user will have to click it again.
    • Tapping on Enter after typing will clear the textfield and not add anything because no option is selected by default. The user will have to either click the option or use the up/down arrow keys to select an option before clicking on Enter

Front end validation

  • Added validation for maximum_grade, language_id.
  • If in zip upload mode, validate that a package is uploaded if no package exists.
  • If in editor mode and autograded is checked, validate that there exists at least 1 test case and that all test cases have both expression and expected filled in.

@allenwq
Copy link
Member

allenwq commented Jan 4, 2017

@justwudi Thanks for the effort !

Possible to split this big PR into maybe 3+ smaller ones ? You can make them depend on each other or even self contained. It will be easier for us to review and I believe it will make the code be merged faster as well. Also when you fix something, you are supposed to rebase to the previous commit and fix them there.

@allenwq allenwq closed this Jan 20, 2017
@jeremyyap jeremyyap deleted the wudi/online-editor branch October 17, 2017 03:03
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.

Python test creation frontend
5 participants