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

SHS: Import homework help, and show on profile to start #2236

Merged
merged 7 commits into from Nov 5, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/assets/javascripts/helpers/FeedHelpers.js
Expand Up @@ -14,6 +14,7 @@ export function mergedNotes(feed) {
sort_timestamp: intervention.start_date_timestamp
};
});

const eventNotes = feed.event_notes.map(eventNote => {
return {
...eventNote,
Expand All @@ -31,10 +32,20 @@ export function mergedNotes(feed) {
};
});

// SHS
const homeworkHelpSessions = (feed.homework_help_sessions || []).map(homeworkHelpSession => {
return {
...homeworkHelpSession,
type: 'homework_help_sessions',
sort_timestamp: homeworkHelpSession.form_timestamp
};
});

const mergedNotes = [
...eventNotes,
...deprecatedInterventions,
...transitionNotes
...transitionNotes,
...homeworkHelpSessions
];
return _.sortBy(mergedNotes, 'sort_timestamp').reverse();
}
17 changes: 17 additions & 0 deletions app/assets/javascripts/student_profile/NotesList.js
Expand Up @@ -58,6 +58,7 @@ export default class NotesList extends React.Component {
case 'event_notes': return this.renderEventNote(mergedNote);
case 'transition_notes': return this.renderTransitionNote(mergedNote);
case 'deprecated_interventions': return this.renderDeprecatedIntervention(mergedNote);
case 'homework_help_sessions': return this.renderHomeworkHelpSession(mergedNote);
}
})}
{this.renderCleanSlateMessage()}
Expand Down Expand Up @@ -153,6 +154,22 @@ export default class NotesList extends React.Component {
);
}

renderHomeworkHelpSession(homeworkHelpSession) {
const text = 'Went to homework help for ' + homeworkHelpSession.courses.map(course => course.course_description).join(' and ') + '.';
return (
<NoteCard
key={['homework_help_session', homeworkHelpSession.id].join()}
noteMoment={toMomentFromRailsDate(homeworkHelpSession.form_timestamp)}
badge={<span style={styles.badge}>Homework Help</span>}
educatorId={homeworkHelpSession.recorded_by_educator_id}
text={text}
educatorsIndex={this.props.educatorsIndex}
showRestrictedNoteRedaction={false}
urlForRestrictedNoteContent={null}
attachments={[]} />
);
}

renderCleanSlateMessage() {
const {forceShowingAllNotes, defaultSchoolYearsBack} = this.props;
const {isViewingAllNotes} = this.state;
Expand Down
39 changes: 39 additions & 0 deletions app/assets/javascripts/student_profile/NotesList.test.js
Expand Up @@ -22,6 +22,7 @@ function testProps(props = {}) {
function feedWithEventNotesJson(eventNotesJson) {
return {
transition_notes: [],
homework_help_sessions: [],
services: {
active: [],
discontinued: []
Expand Down Expand Up @@ -55,6 +56,33 @@ function testPropsForRestrictedNote(props = {}) {
});
}

function testPropsForHomeworkHelp(props = {}) {
return testProps({
feed: {
...feedWithEventNotesJson([]),
homework_help_sessions: [{
"id": 5,
"student_id": 5,
"form_timestamp": "2018-09-25T13:41:43.000Z",
"recorded_by_educator_id": 1,
"courses": [
{
"id": 4,
"course_number": "111",
"course_description": "US HISTORY 1 HONORS"
},
{
"id": 5,
"course_number": "212",
"course_description": "ALGEBRA 1 CP"
}
]
}]
},
...props
});
}

function testRender(props) {
const el = document.createElement('div');
ReactDOM.render(withDefaultNowContext(<NotesList {...props} />), el);
Expand Down Expand Up @@ -173,3 +201,14 @@ describe('props impacting restricted notes', () => {
expect($(el).find('.EditableNoteText').length).toEqual(1);
});
});


describe('homework help', () => {
it('works on happy path', () => {
const el = testRender(testPropsForHomeworkHelp());
expect($(el).find('.NoteText').length).toEqual(1);
expect($(el).find('.EditableNoteText').length).toEqual(0);
expect($(el).text()).toContain('Homework Help');
expect($(el).find('.NoteText').text()).toEqual('Went to homework help for US HISTORY 1 HONORS and ALGEBRA 1 CP.');
});
});
1 change: 1 addition & 0 deletions app/controllers/profile_controller.rb
Expand Up @@ -67,6 +67,7 @@ def student_feed(student)
event_notes: student.event_notes
.map {|event_note| EventNoteSerializer.safe(event_note).serialize_event_note },
transition_notes: student.transition_notes,
homework_help_sessions: student.homework_help_sessions.as_json(except: [:course_ids], methods: [:courses]),
services: {
active: student.services.active.map {|service| ServiceSerializer.new(service).serialize_service },
discontinued: student.services.discontinued.map {|service| ServiceSerializer.new(service).serialize_service }
Expand Down
81 changes: 81 additions & 0 deletions app/importers/homework_help_importer/homework_help_importer.rb
@@ -0,0 +1,81 @@
# Used on the console to import data about homework help sessions.
#
# Usage:
# file_text = <<EOD
# ...
# EOD
# educator = Educator.find_by_login_name('...')
# output = HomeworkHelpImporter.new(educator.id).import(file_text);nil
class HomeworkHelpImporter
def initialize(educator_id, options = {})
@educator_id = educator_id
@log = options.fetch(:log, Rails.env.test? ? LogHelper::Redirect.instance.file : STDOUT)
@matcher = options.fetch(:matcher, ::ImportMatcher.new(strptime_format: ImportMatcher::GOOGLE_FORM_EXPORTED_TO_GOOGLE_SHEETS_TIMESTAMP_FORMAT))
end

def import(file_text, options = {})
# parse
rows = []
create_streaming_csv(file_text).each_with_index do |row, index|
maybe_row = maybe_parse_row(row.to_h, index)
next if maybe_row.nil?
rows << maybe_row
@matcher.count_valid_row
end
log "matcher#stats: #{@matcher.stats}"

# write to database
homework_help_sessions = nil
HomeworkHelpSession.transaction do
HomeworkHelpSession.destroy_all
homework_help_sessions = rows.map {|row| HomeworkHelpSession.create!(row) }
end
homework_help_sessions
end

private
def maybe_parse_row(row, index)
course_ids = parse_and_match_courses(row)
if course_ids.size == 0
@matcher.count_invalid_row
return nil
end

{
recorded_by_educator_id: @educator_id,
student_id: @matcher.find_student_id(row['Student Local ID Number']),
form_timestamp: @matcher.parse_timestamp(row['Timestamp']),
course_ids: course_ids
}
end

# Read all courses, map to numbers, lookup
def parse_and_match_courses(row)
course_names = [
row['English classes'].try(:split, ','),
row['ELE Classes'].try(:split, ','),
row['Science classes'].try(:split, ','),
row['Social Studies classes'].try(:split, ','),
row['Math classes'].try(:split, ',')
].flatten.compact

course_numbers = course_names.flat_map do |course_name|
match = course_name.match(/\((\d+)\)/)
if match.nil? then [] else [match[1]] end
end

course_numbers.map {|course_number| @matcher.find_course_id(course_number) }.compact
end

def create_streaming_csv(file_text)
csv_transformer = StreamingCsvTransformer.new(@log, {
csv_options: { header_converters: nil }
})
csv_transformer.transform(file_text)
end

def log(msg)
text = if msg.class == String then msg else JSON.pretty_generate(msg) end
@log.puts "HomeworkHelpImporter: #{text}"
end
end
84 changes: 84 additions & 0 deletions app/importers/homework_help_importer/import_matcher.rb
@@ -0,0 +1,84 @@
# Helper functions for doing an import, and matching different values in an imported row to
# the database.
class ImportMatcher
# Timestamps have differnet formats if you download a Google Form as a CSV
# versus if you export that same form to Sheets (and then download that).
GOOGLE_FORM_CSV_TIMESTAMP_FORMAT = '%Y/%m/%d %l:%M:%S %p %Z'
GOOGLE_FORM_EXPORTED_TO_GOOGLE_SHEETS_TIMESTAMP_FORMAT = '%m/%d/%Y %k:%M:%S'

def initialize(options = {})
@strptime_format = options.fetch(:strptime_format, GOOGLE_FORM_CSV_TIMESTAMP_FORMAT)
@google_email_address_mapping = options.fetch(:google_email_address_mapping, PerDistrict.new.google_email_address_mapping)
reset_counters!
end

# student?
def find_student_id(value)
student_local_id = value.try(:strip)
student_id = Student.find_by_local_id(student_local_id).try(:id) unless student_local_id.nil?
if student_id.nil?
@invalid_rows_count += 1
@invalid_student_local_ids = (@invalid_student_local_ids + [student_local_id]).uniq
return nil
end
student_id
end

# educator? also support mapping from Google email to SIS/LDAP/Insights email
def find_educator_id(value)
google_educator_email = value.try(:strip)
educator_email = @google_email_address_mapping.fetch(google_educator_email, google_educator_email)
educator_id = Educator.find_by_email(educator_email).try(:id) unless student_local_id.nil?
if educator_id.nil?
@invalid_rows_count += 1
@invalid_educator_emails = (@invalid_educator_emails + [educator_email]).uniq
return nil
end
educator_id
end

# HS course?
def find_course_id(value)
course_number = value.try(:strip).upcase
course_id = Course.find_by_course_number(course_number).try(:id)
if course_id.nil?
@invalid_rows_count += 1
@invalid_course_numbers = (@invalid_course_numbers + [course_number]).uniq
return nil
end
course_id
end

# parse timestamp into DateTime
def parse_timestamp(value)
DateTime.strptime(value, @strptime_format)
end

def count_valid_row
@valid_rows_count += 1
end

def count_invalid_row
@invalid_rows_count += 1
end

# for debugging and testing
def stats
{
valid_rows_count: @valid_rows_count,
invalid_rows_count: @invalid_rows_count,
invalid_student_local_ids: @invalid_student_local_ids,
invalid_educator_emails: @invalid_educator_emails,
invalid_course_numbers: @invalid_course_numbers
}
end

private
def reset_counters!
@valid_rows_count = 0
@invalid_rows_count = 0
@invalid_student_local_ids = []
@invalid_educator_emails = []
@invalid_course_numbers = []
end
end
12 changes: 12 additions & 0 deletions app/models/homework_help_session.rb
@@ -0,0 +1,12 @@
# A student's attendance at a homework help session
class HomeworkHelpSession < ApplicationRecord
belongs_to :student

validates :student, presence: true
validates :form_timestamp, presence: true
validates :course_ids, presence: true

def courses
Course.where(id: course_ids)
end
end
1 change: 1 addition & 0 deletions app/models/student.rb
Expand Up @@ -26,6 +26,7 @@ class Student < ApplicationRecord
has_many :absences, dependent: :destroy
has_many :discipline_incidents, dependent: :destroy
has_many :iep_documents, dependent: :destroy
has_many :homework_help_sessions, dependent: :destroy
has_many :star_math_results, -> { order(date_taken: :desc) }, dependent: :destroy
has_many :star_reading_results, -> { order(date_taken: :desc) }, dependent: :destroy
has_many :dibels_results, -> { order(date_taken: :desc) }, dependent: :destroy
Expand Down
1 change: 1 addition & 0 deletions config/application.rb
Expand Up @@ -29,6 +29,7 @@ class Application < Rails::Application
"#{config.root}/app/importers/data_transformers",
"#{config.root}/app/importers/file_importers",
"#{config.root}/app/importers/helpers",
"#{config.root}/app/importers/homework_help_importer",
"#{config.root}/app/importers/iep_import",
"#{config.root}/app/importers/photo_import",
"#{config.root}/app/importers/precompute",
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20181105192430_homework_help.rb
@@ -0,0 +1,10 @@
class HomeworkHelp < ActiveRecord::Migration[5.2]
def change
create_table :homework_help_sessions do |t|
t.integer :student_id
t.datetime :form_timestamp
t.json :course_ids
t.timestamps
end
end
end
5 changes: 5 additions & 0 deletions db/migrate/20181105200422_homework_help_educator_id.rb
@@ -0,0 +1,5 @@
class HomeworkHelpEducatorId < ActiveRecord::Migration[5.2]
def change
add_column :homework_help_sessions, :recorded_by_educator_id, :integer, null: false
end
end
6 changes: 6 additions & 0 deletions db/migrate/20181105204625_homework_help_sessions_student.rb
@@ -0,0 +1,6 @@
class HomeworkHelpSessionsStudent < ActiveRecord::Migration[5.2]
def change
add_foreign_key "homework_help_sessions", "students", name: "homework_help_sessions_student_id_fk"
add_foreign_key "homework_help_sessions", "educators", column: 'recorded_by_educator_id', name: "homework_help_sessions_recorded_by_educator_id_fk"
end
end
13 changes: 12 additions & 1 deletion db/schema.rb
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2018_10_22_191915) do
ActiveRecord::Schema.define(version: 2018_11_05_204625) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -237,6 +237,15 @@
t.index ["slug"], name: "index_homerooms_on_slug", unique: true
end

create_table "homework_help_sessions", force: :cascade do |t|
t.integer "student_id"
t.datetime "form_timestamp"
t.json "course_ids"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "recorded_by_educator_id", null: false
end

create_table "house_educator_mappings", force: :cascade do |t|
t.text "house_field_text", null: false
t.integer "educator_id"
Expand Down Expand Up @@ -567,6 +576,8 @@
add_foreign_key "homerooms", "educators", name: "homerooms_for_educator_id_fk"
add_foreign_key "homerooms", "schools", name: "homerooms_for_school_id_fk"
add_foreign_key "homerooms", "schools", name: "homerooms_school_id_fk"
add_foreign_key "homework_help_sessions", "educators", column: "recorded_by_educator_id", name: "homework_help_sessions_recorded_by_educator_id_fk"
add_foreign_key "homework_help_sessions", "students", name: "homework_help_sessions_student_id_fk"
add_foreign_key "house_educator_mappings", "educators", name: "house_educator_mappings_educator_id_fk"
add_foreign_key "iep_documents", "students", name: "iep_documents_student_id_fk"
add_foreign_key "interventions", "educators", name: "interventions_educator_id_fk"
Expand Down