From 25b5c20259ca6e9ddf5f494281fb6424a5a0c443 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 10:10:17 -0500 Subject: [PATCH 1/8] WIP --- app/lib/feed.rb | 68 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/app/lib/feed.rb b/app/lib/feed.rb index d88a302ea9..6053ea1dad 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -83,6 +83,29 @@ def incident_cards(time_now, limit) incidents.map {|incident| incident_card(incident) } end + # Find all students with cards, so they can link + # into profile + def student_voice_cards(time_now, limit) + # TODO(kr) need only latest by form_key + imported_forms = ImportedForm + .select('DISTINCT ON ("form_key") *') + .order(:form_key, form_timestamp: :desc, updated_at: :desc, id: :desc) + # t = ImportedForm.arel_table + # tuples = ImportedForm.find_by_sql( + # ImportedForm.project(t[Arel.star]) + # .distinct_on(t[:form_key]) + # .order(t[:form_key], t[:form_timestamp].desc, t[:updated_at].desc) + # ) + + imported_forms = ImportedForm + .where(student_id: @authorized_students.map(&:id)) + .where('form_timestamp < ?', time_now) + .order(form_timestamp: :desc) + .limit(limit) + .includes(student: [:homeroom, :school]) + imported_forms.map {|imported_form| student_voice_card(imported_form)} + end + # Merge cards of different types together, sorted by most recent timestamp # first, and then truncate them to `limit`. def merge_sort_and_limit_cards(card_sets, limit) @@ -100,18 +123,18 @@ def birthday_card(student, time_now) def incident_card(incident) json = incident.as_json({ - :only => [:id, :incident_code, :incident_location, :incident_description, :occurred_at, :has_exact_time], - :include => { - :student => { - :only => [:id, :email, :first_name, :last_name, :grade, :house], - :include => { - :school => { - :only => [:local_id, :school_type] + only: [:id, :incident_code, :incident_location, :incident_description, :occurred_at, :has_exact_time], + include: { + student: { + only: [:id, :email, :first_name, :last_name, :grade, :house], + include: { + school: { + only: [:local_id, :school_type] }, - :homeroom => { - :only => [:id, :name], - :include => { - :educator => {:only => [:id, :full_name, :email]} + homeroom: { + only: [:id, :name], + include: { + educator: {:only => [:id, :full_name, :email]} } } } @@ -120,4 +143,27 @@ def incident_card(incident) }) FeedCard.new(:incident_card, incident.occurred_at, json) end + + # Merge so it's flattened form + def student_voice_card(imported_form) + imported_form.as_flattened_form.merge(imported_form.as_json({ + only: [:id], + include: { + student: { + only: [:id, :email, :first_name, :last_name, :grade, :house], + include: { + school: { + only: [:local_id, :school_type] + }, + homeroom: { + only: [:id, :name], + include: { + educator: {:only => [:id, :full_name, :email]} + } + } + } + } + } + }) + end end From bb6ff3254e19486cf24b4bd51ebf1183a938240e Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 10:59:20 -0500 Subject: [PATCH 2/8] Working query and specs for student voice cards in feed --- app/config_objects/per_district.rb | 4 + app/lib/feed.rb | 75 ++++++++----------- .../mid_year_survey_importer_spec.rb | 2 +- .../q2_self_reflection_importer_spec.rb | 2 +- spec/lib/feed_spec.rb | 48 ++++++++++-- spec/support/test_pals.rb | 38 ++++++++++ 6 files changed, 120 insertions(+), 49 deletions(-) diff --git a/app/config_objects/per_district.rb b/app/config_objects/per_district.rb index 6cf65cce99..a7a2fb158d 100644 --- a/app/config_objects/per_district.rb +++ b/app/config_objects/per_district.rb @@ -114,6 +114,10 @@ def include_incident_cards? EnvironmentVariable.is_true('FEED_INCLUDE_INCIDENT_CARDS') || false end + def include_student_voice_cards? + EnvironmentVariable.is_true('FEED_INCLUDE_STUDENT_VOICE_CARDS') || false + end + def high_school_enabled? @district_key == SOMERVILLE end diff --git a/app/lib/feed.rb b/app/lib/feed.rb index 6053ea1dad..1d89081ab7 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -37,10 +37,16 @@ def all_cards(time_now, limit) else [] end + student_voice_cards = if PerDistrict.new.include_student_voice_cards? + self.student_voice_cards(time_now) + else + [] + end self.merge_sort_and_limit_cards([ event_note_cards, birthday_cards, - incident_cards + incident_cards, + student_voice_cards ], limit) end @@ -84,26 +90,19 @@ def incident_cards(time_now, limit) end # Find all students with cards, so they can link - # into profile - def student_voice_cards(time_now, limit) - # TODO(kr) need only latest by form_key - imported_forms = ImportedForm - .select('DISTINCT ON ("form_key") *') - .order(:form_key, form_timestamp: :desc, updated_at: :desc, id: :desc) - # t = ImportedForm.arel_table - # tuples = ImportedForm.find_by_sql( - # ImportedForm.project(t[Arel.star]) - # .distinct_on(t[:form_key]) - # .order(t[:form_key], t[:form_timestamp].desc, t[:updated_at].desc) - # ) - - imported_forms = ImportedForm - .where(student_id: @authorized_students.map(&:id)) - .where('form_timestamp < ?', time_now) - .order(form_timestamp: :desc) - .limit(limit) - .includes(student: [:homeroom, :school]) - imported_forms.map {|imported_form| student_voice_card(imported_form)} + # into profile. Always one one card per day, aligned to end of day + # so that as more come in during the day it stays pegged as fresh. + def student_voice_cards(time_now) + imported_forms = imported_forms_for_card(time_now) + grouped_by_date = imported_forms.group_by {|form| form.form_timestamp.to_date } + grouped_by_date.map do |date, imported_forms_for_date| + students = imported_forms_for_date.map(&:student).uniq + latest_form_timestamp = imported_forms_for_date.map(&:form_timestamp).max + json = { + students: students.as_json(only: [:id, :first_name, :last_name]) + } + FeedCard.new(:student_voice, latest_form_timestamp, json) + end end # Merge cards of different types together, sorted by most recent timestamp @@ -144,26 +143,18 @@ def incident_card(incident) FeedCard.new(:incident_card, incident.occurred_at, json) end - # Merge so it's flattened form - def student_voice_card(imported_form) - imported_form.as_flattened_form.merge(imported_form.as_json({ - only: [:id], - include: { - student: { - only: [:id, :email, :first_name, :last_name, :grade, :house], - include: { - school: { - only: [:local_id, :school_type] - }, - homeroom: { - only: [:id, :name], - include: { - educator: {:only => [:id, :full_name, :email]} - } - } - } - } - } - }) + # This uniques by (student_id, form_key), taking the most recent + # by (form_timestamp, updated_at, id). + # + # Using Arel.sql is safe for strings without user input, see https://github.com/rails/rails/issues/32995 + # for more background. + def imported_forms_for_card(time_now) + ImportedForm + .where(student_id: @authorized_students.map(&:id)) + .where('form_timestamp < ?', time_now) + .includes(student: [:homeroom, :school]) + .select(Arel.sql 'DISTINCT ON(CONCAT(form_key, student_id)) form_key, student_id, form_timestamp, updated_at, id') + .order(Arel.sql 'CONCAT(form_key, student_id), form_key ASC, student_id ASC, form_timestamp DESC, updated_at DESC, id DESC') + .compact end end diff --git a/spec/importers/student_voice_surveys/mid_year_survey_importer_spec.rb b/spec/importers/student_voice_surveys/mid_year_survey_importer_spec.rb index c9bab08b16..162e448325 100644 --- a/spec/importers/student_voice_surveys/mid_year_survey_importer_spec.rb +++ b/spec/importers/student_voice_surveys/mid_year_survey_importer_spec.rb @@ -6,7 +6,7 @@ def fixture_file_text end describe 'integration test' do - let!(:pals) { TestPals.create! } + let!(:pals) { TestPals.create!(skip_imported_forms: true) } it 'works for importing notes' do log = LogHelper::FakeLog.new diff --git a/spec/importers/student_voice_surveys/q2_self_reflection_importer_spec.rb b/spec/importers/student_voice_surveys/q2_self_reflection_importer_spec.rb index 1ae5bb875e..8e98482b58 100644 --- a/spec/importers/student_voice_surveys/q2_self_reflection_importer_spec.rb +++ b/spec/importers/student_voice_surveys/q2_self_reflection_importer_spec.rb @@ -6,7 +6,7 @@ def fixture_file_text end describe 'integration test' do - let!(:pals) { TestPals.create! } + let!(:pals) { TestPals.create!(skip_imported_forms: true) } it 'works for importing notes' do log = LogHelper::FakeLog.new diff --git a/spec/lib/feed_spec.rb b/spec/lib/feed_spec.rb index 52437d2505..a5efebaf1c 100644 --- a/spec/lib/feed_spec.rb +++ b/spec/lib/feed_spec.rb @@ -14,9 +14,16 @@ def feed_for(educator) let!(:time_now) { pals.time_now } # Preserve global app config - before { @FEED_INCLUDE_INCIDENT_CARDS = ENV['FEED_INCLUDE_INCIDENT_CARDS'] } - before { ENV['FEED_INCLUDE_INCIDENT_CARDS'] = 'true' } - after { ENV['FEED_INCLUDE_INCIDENT_CARDS'] = @FEED_INCLUDE_INCIDENT_CARDS } + before do + @FEED_INCLUDE_STUDENT_VOICE_CARDS = ENV['FEED_INCLUDE_STUDENT_VOICE_CARDS'] + @FEED_INCLUDE_INCIDENT_CARDS = ENV['FEED_INCLUDE_INCIDENT_CARDS'] + ENV['FEED_INCLUDE_INCIDENT_CARDS'] = 'true' + ENV['FEED_INCLUDE_STUDENT_VOICE_CARDS'] = 'true' + end + after do + ENV['FEED_INCLUDE_INCIDENT_CARDS'] = @FEED_INCLUDE_INCIDENT_CARDS + ENV['FEED_INCLUDE_STUDENT_VOICE_CARDS'] = @FEED_INCLUDE_STUDENT_VOICE_CARDS + end describe '.students_for_feed' do it 'can apply counselor-based filter' do @@ -63,7 +70,7 @@ def create_event_note(time_now, options = {}) })) end - it 'works end-to-end for event_note, incident and birthday' do + it 'works end-to-end for event_note, incident, birthday, student voice' do limit = 4 event_note = create_event_note(time_now, { student: pals.shs_freshman_mari, @@ -76,7 +83,7 @@ def create_event_note(time_now, options = {}) }) feed_cards = feed_for(pals.shs_jodi).all_cards(time_now, limit) - expect(feed_cards.size).to eq 3 + expect(feed_cards.size).to eq 4 expect(feed_cards.as_json).to eq([{ "type"=>"birthday_card", "timestamp"=>"2018-03-12T00:00:00.000Z", @@ -86,6 +93,16 @@ def create_event_note(time_now, options = {}) "last_name"=>"Kenobi", "date_of_birth"=>"2004-03-12T00:00:00.000Z" } + }, { + "type"=>"student_voice", + "timestamp"=>"2018-03-11T11:03:00.000Z", + "json"=>{ + "students"=>[{ + "id"=>pals.shs_freshman_mari.id, + "first_name"=>"Mari", + "last_name"=>"Kenobi" + }] + } }, { "type"=>"incident_card", "timestamp"=>"2018-03-09T11:03:00.000Z", @@ -240,4 +257,25 @@ def create_event_note(time_now, options = {}) }) end end + + describe '#student_voice_cards' do + it 'works correctly, with one per day' do + feed = feed_for(pals.shs_jodi) + cards = feed.student_voice_cards(time_now) + expect(cards.size).to eq 1 + expect(cards.first.type).to eq(:student_voice) + expect(cards.first.timestamp.to_date).to eq(Date.parse('2018-03-11')) + expect(cards.as_json).to eq([{ + 'type' => "student_voice", + 'timestamp' => "2018-03-11T11:03:00.000Z", + 'json' => { + 'students' => [{ + 'id' => pals.shs_freshman_mari.id, + 'first_name' => "Mari", + 'last_name' => "Kenobi" + }] + } + }]) + end + end end diff --git a/spec/support/test_pals.rb b/spec/support/test_pals.rb index ce562d9b8f..ea006363d8 100644 --- a/spec/support/test_pals.rb +++ b/spec/support/test_pals.rb @@ -76,6 +76,7 @@ def create!(options = {}) email_domain = options.fetch(:email_domain, 'demo.studentinsights.org') skip_team_memberships = options.fetch(:skip_team_memberships, false) + skip_imported_forms = options.fetch(:skip_imported_forms, false) # Uri works in the central office, and is the admin for the # project at the district. @@ -470,6 +471,7 @@ def create!(options = {}) ) add_team_memberships unless skip_team_memberships + add_student_voice_surveys unless skip_imported_forms reindex! self @@ -495,6 +497,42 @@ def add_team_memberships }) end + # time_now, not wall clock + def add_student_voice_surveys + ImportedForm.create!({ + "educator_id"=>shs_jodi.id, + "student_id"=>shs_freshman_mari.id, + 'form_timestamp' => time_now - 2.days, + "form_key"=>"shs_what_i_want_my_teacher_to_know_mid_year", + 'form_url' => 'https://example.com/mid_year_survey_form_url', + 'form_json' => { + "What was the high point for you in school this year so far?"=>"A high point has been my grade in Biology since I had to work a lot for it", + "I am proud that I..."=>"Have good grades in my classes", + "My best qualities are..."=>"helping others when they don't know how to do homework assignments", + "My activities and interests outside of school are..."=>"cheering", + "I get nervous or stressed in school when..."=>"I get a low grade on an assignment that I thought I would do well on", + "I learn best when my teachers..."=>"show me each step of what I have to do" + } + }) + ImportedForm.create!({ + "educator_id"=>shs_jodi.id, + "student_id"=>shs_freshman_mari.id, + 'form_timestamp' => time_now - 2.days, + "form_key"=>"shs_q2_self_reflection", + 'form_url' => 'https://example.com/q2_self_reflection_form_url', + 'form_json' => { + "What classes are you doing well in?"=>"Computer Science, French", + "Why are you doing well in those classes?"=>"I make time in my afternoon each day for doing homework and stick to it", + "What courses are you struggling in?"=>"Nothing really", + "Why are you struggling in those courses?"=>"I have to work really hard ", + "In the classes that you are struggling in, how can your teachers support you so that your grades, experience, work load, etc, improve?"=>"Change the way homework works, it's too much", + "When you are struggling, who do you go to for support, encouragement, advice, etc?"=>"Being able to stay after school and work with teachers when I need help", + "At the end of the quarter 3, what would make you most proud of your accomplishments in your course?"=>"Keeping grades high in all classes since I'm worried about college", + "What other information is important for your teachers to know so that we can support you and your learning? (For example, tutor, mentor, before school HW help, study group, etc)"=>"Help in the morning before school" + } + }) + end + def create_section_assignment(educator, sections) sections.each do |section| EducatorSectionAssignment.create!(educator: educator, section: section) From a2ca97f6366620fba54118b508c6693cb3bc955c Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 11:15:08 -0500 Subject: [PATCH 3/8] WIP on UI for student voice cards --- app/assets/javascripts/feed/FeedView.js | 17 +++- .../javascripts/feed/StudentVoiceCard.js | 83 +++++++++++++++++++ app/lib/feed.rb | 2 + spec/lib/feed_spec.rb | 4 + 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/feed/StudentVoiceCard.js diff --git a/app/assets/javascripts/feed/FeedView.js b/app/assets/javascripts/feed/FeedView.js index 11e8a4c2a3..92a68e3151 100644 --- a/app/assets/javascripts/feed/FeedView.js +++ b/app/assets/javascripts/feed/FeedView.js @@ -3,6 +3,7 @@ import React from 'react'; import EventNoteCard from './EventNoteCard'; import BirthdayCard from './BirthdayCard'; import IncidentCard from './IncidentCard'; +import StudentVoiceCard from './StudentVoiceCard'; // Pure UI component for rendering feed cards (eg, on the home page) @@ -16,7 +17,8 @@ export default class FeedView extends React.Component { const cardProps = cardPropsFn ? cardPropsFn(type, json) : {}; if (type === 'event_note_card') return this.renderEventNoteCard(json, cardProps); if (type === 'birthday_card') return this.renderBirthdayCard(json, cardProps); - if (type === 'incident_card') return this.renderIncidenCard(json, cardProps); + if (type === 'incident_card') return this.renderIncidentCard(json, cardProps); + if (type === 'student_voice') return this.renderStudentVoiceCard(json, cardProps); console.warn('Unexpected card type: ', type); // eslint-disable-line no-console })} @@ -41,14 +43,23 @@ export default class FeedView extends React.Component { />; } - renderIncidenCard(json, cardProps = {}) { + renderIncidentCard(json, cardProps = {}) { return ; - } + } + + renderStudentVoiceCard(json, cardProps = {}) { + return ; + } } FeedView.propTypes = { feedCards: PropTypes.arrayOf(PropTypes.shape({ diff --git a/app/assets/javascripts/feed/StudentVoiceCard.js b/app/assets/javascripts/feed/StudentVoiceCard.js new file mode 100644 index 0000000000..13fd2469a9 --- /dev/null +++ b/app/assets/javascripts/feed/StudentVoiceCard.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Card from '../components/Card'; +import {toMomentFromTimestamp} from '../helpers/toMoment'; + + +// Render a card in the feed showing there are new student voice surveys in, +// and allow clicking to see list of students and then jump to their profiles. +export default class StudentVoiceCard extends React.Component { + constructor(props) { + super(props); + this.state = { + isExpanded: false + }; + } + + onExpand(e) { + e.preventDefault(); + this.setState({isExpanded: true}); + } + + render() { + const {style = {}} = this.props; + // const {isExpanded} = this.state; + // const now = this.context.nowFn(); + // const thisYearBirthdateMoment = toMomentFromTimestamp(studentBirthdayCard.date_of_birth).year(now.year()); + // const isWas = (thisYearBirthdateMoment.isBefore(now.clone().startOf('day'))) ? 'was' : 'is'; + return ( + + + + ); + } + + renderStudentsAndCount() { + const {studentVoiceCardJson} = this.props; + const {students} = studentVoiceCardJson; + + // if it's <=3 students, just show them each with links + // if it's more, list two with links and then +n more, which expands + if (students.length === 0) { + return null; // shouldn't happen, but guard + } else if (students.length === 1) { + return
New student voice surveys are in for {this.renderShortList(students)}.
; + } else if (students.length === 2) { + return
New student voice surveys are in for {this.renderShortList(students)}.
; + } else if (students.length === 3) { + return
New student voice surveys are in for {this.renderShortList(students)}.
; + } else { + return
New student voice surveys are in for {this.renderExpandableList(students)}.
; + } + } + + renderShortList(students) { + // const nStudentsText = (count === 1) ? 'one student' : `${count} students`; + } + + renderExpandableList() { + // } href={`/students/${studentStudentVoiceCard.id}`}>{studentStudentVoiceCard.first_name} {studentStudentVoiceCard.last_name} + } +} +StudentVoiceCard.contextTypes = { + nowFn: PropTypes.func.isRequired +}; +StudentVoiceCard.propTypes = { + studentVoiceCardJson: PropTypes.shape({ + latest_form_timestamp: PropTypes.string.isRequired, + imported_forms_for_date_count: PropTypes.number.isRequired, + students: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.number.isRequired, + first_name: PropTypes.string.isRequired, + last_name: PropTypes.string.isRequired + })).isRequired + }).isRequired, + style: PropTypes.object +}; + + +const styles = { + person: { + fontWeight: 'bold' + } +}; diff --git a/app/lib/feed.rb b/app/lib/feed.rb index 1d89081ab7..65e8ce7507 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -99,6 +99,8 @@ def student_voice_cards(time_now) students = imported_forms_for_date.map(&:student).uniq latest_form_timestamp = imported_forms_for_date.map(&:form_timestamp).max json = { + latest_form_timestamp: latest_form_timestamp, + imported_forms_for_date_count: imported_forms_for_date.size, students: students.as_json(only: [:id, :first_name, :last_name]) } FeedCard.new(:student_voice, latest_form_timestamp, json) diff --git a/spec/lib/feed_spec.rb b/spec/lib/feed_spec.rb index a5efebaf1c..d6075db89e 100644 --- a/spec/lib/feed_spec.rb +++ b/spec/lib/feed_spec.rb @@ -97,6 +97,8 @@ def create_event_note(time_now, options = {}) "type"=>"student_voice", "timestamp"=>"2018-03-11T11:03:00.000Z", "json"=>{ + "latest_form_timestamp"=>"2018-03-11T11:03:00.000Z", + "imported_forms_for_date_count"=>2, "students"=>[{ "id"=>pals.shs_freshman_mari.id, "first_name"=>"Mari", @@ -269,6 +271,8 @@ def create_event_note(time_now, options = {}) 'type' => "student_voice", 'timestamp' => "2018-03-11T11:03:00.000Z", 'json' => { + "latest_form_timestamp"=>"2018-03-11T11:03:00.000Z", + "imported_forms_for_date_count"=>2, 'students' => [{ 'id' => pals.shs_freshman_mari.id, 'first_name' => "Mari", From 55e7ab7a4602d4ef1b9874aec11b53ae004ee07f Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 13:35:34 -0500 Subject: [PATCH 4/8] Stories and snapshots for StudentVoiceCard --- .../javascripts/feed/IncidentCard.story.js | 2 +- .../javascripts/feed/StudentVoiceCard.js | 70 +++--- .../feed/StudentVoiceCard.story.js | 15 ++ .../javascripts/feed/StudentVoiceCard.test.js | 86 +++++++ .../StudentVoiceCard.test.js.snap | 220 ++++++++++++++++++ ui/config/.storybook/config.js | 3 + 6 files changed, 366 insertions(+), 30 deletions(-) create mode 100644 app/assets/javascripts/feed/StudentVoiceCard.story.js create mode 100644 app/assets/javascripts/feed/StudentVoiceCard.test.js create mode 100644 app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap diff --git a/app/assets/javascripts/feed/IncidentCard.story.js b/app/assets/javascripts/feed/IncidentCard.story.js index cdc70c5b31..578c68c1e4 100644 --- a/app/assets/javascripts/feed/IncidentCard.story.js +++ b/app/assets/javascripts/feed/IncidentCard.story.js @@ -4,5 +4,5 @@ import {withDefaultNowContext} from '../testing/NowContainer'; import IncidentCard from './IncidentCard'; import {testProps} from './IncidentCard.test'; -storiesOf('home/IncidentCard', module) // eslint-disable-line no-undef +storiesOf('feed/IncidentCard', module) // eslint-disable-line no-undef .add('all', () => withDefaultNowContext()); diff --git a/app/assets/javascripts/feed/StudentVoiceCard.js b/app/assets/javascripts/feed/StudentVoiceCard.js index 13fd2469a9..628f31a436 100644 --- a/app/assets/javascripts/feed/StudentVoiceCard.js +++ b/app/assets/javascripts/feed/StudentVoiceCard.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; +import _ from 'lodash'; +import hash from 'object-hash'; import Card from '../components/Card'; -import {toMomentFromTimestamp} from '../helpers/toMoment'; // Render a card in the feed showing there are new student voice surveys in, @@ -10,24 +11,23 @@ export default class StudentVoiceCard extends React.Component { constructor(props) { super(props); this.state = { - isExpanded: false + isExpanded: false, + shuffleSeed: props.shuffleSeed || _.random(0, 32) }; + + this.onExpandClicked = this.onExpandClicked.bind(this); } - onExpand(e) { + onExpandClicked(e) { e.preventDefault(); this.setState({isExpanded: true}); } render() { const {style = {}} = this.props; - // const {isExpanded} = this.state; - // const now = this.context.nowFn(); - // const thisYearBirthdateMoment = toMomentFromTimestamp(studentBirthdayCard.date_of_birth).year(now.year()); - // const isWas = (thisYearBirthdateMoment.isBefore(now.clone().startOf('day'))) ? 'was' : 'is'; return ( - + {this.renderStudentsAndCount()} ); } @@ -35,33 +35,51 @@ export default class StudentVoiceCard extends React.Component { renderStudentsAndCount() { const {studentVoiceCardJson} = this.props; const {students} = studentVoiceCardJson; + const {isExpanded, shuffleSeed} = this.state; + const emoji = 💬; - // if it's <=3 students, just show them each with links - // if it's more, list two with links and then +n more, which expands + // If the list is <=3 students, show them all inline with links. + // If it's more, pick two random to show and then +n more, which expands. if (students.length === 0) { return null; // shouldn't happen, but guard } else if (students.length === 1) { - return
New student voice surveys are in for {this.renderShortList(students)}.
; + return
{emoji} {this.renderStudentLink(students[0])} shared a new student voice survey.
; } else if (students.length === 2) { - return
New student voice surveys are in for {this.renderShortList(students)}.
; + return
{emoji} {this.renderStudentLink(students[0])} and {this.renderStudentLink(students[1])} shared new student voice surveys.
; } else if (students.length === 3) { - return
New student voice surveys are in for {this.renderShortList(students)}.
; + return
{emoji} {this.renderStudentLink(students[0])}, {this.renderStudentLink(students[1])} and {this.renderStudentLink(students[2])} shared new student voice surveys.
; + } + + const shuffledStudents = _.sortBy(students, student => hash({...student, shuffleSeed})); + if (!isExpanded) { + return
{emoji} {this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderExpandableListLink(shuffledStudents.slice(2))} shared new student voice surveys.
; } else { - return
New student voice surveys are in for {this.renderExpandableList(students)}.
; + return
{emoji} {this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderMoreStudentsText(shuffledStudents.slice(2))} shared new student voice surveys.{this.renderExpandedList(shuffledStudents.slice(2))}
; } } - renderShortList(students) { - // const nStudentsText = (count === 1) ? 'one student' : `${count} students`; + renderMoreStudentsText(restOfStudents) { + return `${restOfStudents.length} more students`; + } + + renderExpandableListLink(restOfStudents) { + return {this.renderMoreStudentsText(restOfStudents)}; + } + + renderExpandedList(restOfStudents) { + return ( +
+ {restOfStudents.map(student => ( +
{this.renderStudentLink(student)}
+ ))} +
+ ); } - renderExpandableList() { - // } href={`/students/${studentStudentVoiceCard.id}`}>{studentStudentVoiceCard.first_name} {studentStudentVoiceCard.last_name} + renderStudentLink(student) { + return {student.first_name} {student.last_name}; } } -StudentVoiceCard.contextTypes = { - nowFn: PropTypes.func.isRequired -}; StudentVoiceCard.propTypes = { studentVoiceCardJson: PropTypes.shape({ latest_form_timestamp: PropTypes.string.isRequired, @@ -72,12 +90,6 @@ StudentVoiceCard.propTypes = { last_name: PropTypes.string.isRequired })).isRequired }).isRequired, - style: PropTypes.object -}; - - -const styles = { - person: { - fontWeight: 'bold' - } + style: PropTypes.object, + shuffleSeed: PropTypes.number }; diff --git a/app/assets/javascripts/feed/StudentVoiceCard.story.js b/app/assets/javascripts/feed/StudentVoiceCard.story.js new file mode 100644 index 0000000000..20fc1fbceb --- /dev/null +++ b/app/assets/javascripts/feed/StudentVoiceCard.story.js @@ -0,0 +1,15 @@ +import React from 'react'; +import {storiesOf} from '@storybook/react'; +import {testEl, propsWithNStudents} from './StudentVoiceCard.test'; + +storiesOf('feed/StudentVoiceCard', module) // eslint-disable-line no-undef + .add('combinations', () => ( +
+ {testEl(propsWithNStudents(1))} + {testEl(propsWithNStudents(2))} + {testEl(propsWithNStudents(3))} + {testEl(propsWithNStudents(4))} + {testEl({...propsWithNStudents(4), shuffleSeed: 100})} + {testEl({...propsWithNStudents(4), shuffleSeed: 200})} +
+ )); diff --git a/app/assets/javascripts/feed/StudentVoiceCard.test.js b/app/assets/javascripts/feed/StudentVoiceCard.test.js new file mode 100644 index 0000000000..b0ab50c26d --- /dev/null +++ b/app/assets/javascripts/feed/StudentVoiceCard.test.js @@ -0,0 +1,86 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import renderer from 'react-test-renderer'; +import StudentVoiceCard from './StudentVoiceCard'; +import {withDefaultNowContext} from '../testing/NowContainer'; + +function testStudents() { + return [{ + id: 4, + first_name: 'Luke', + last_name: 'Skywalker' + }, { + id: 5, + first_name: 'Mari', + last_name: 'Kenobi' + }, { + id: 6, + first_name: 'Kylo', + last_name: 'Ren' + }, { + id: 7, + first_name: 'Darth', + last_name: 'Tater' + }]; +} + +export function testProps(props = {}) { + return { + shuffleSeed: 42, + studentVoiceCardJson: { + latest_form_timestamp: '2018-03-11T11:03:00.000Z', + imported_forms_for_date_count: 2, + students: testStudents() + }, + ...props + }; +} + +export function propsWithNStudents(n) { + const props = testProps(); + return { + ...props, + studentVoiceCardJson: { + ...props.studentVoiceCardJson, + students: testStudents().slice(0, n) + } + }; +} + + +export function testEl(props) { + return withDefaultNowContext(); +} + +it('renders without crashing', () => { + const el = document.createElement('div'); + ReactDOM.render(testEl(testProps()), el); + expect($(el).text()).toContain('💬 Darth Tater, Kylo Ren and 2 more students shared new student voice surveys.'); + expect($(el).find('a').toArray().map(el => $(el).attr('href'))).toEqual([ + '/students/7', + '/students/6', + '#' + ]); +}); + +describe('snapshots', () => { + it('works with one student', () => { + const tree = renderer.create(testEl(propsWithNStudents(1))).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('works with two students', () => { + const tree = renderer.create(testEl(propsWithNStudents(2))).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('works with three students', () => { + const tree = renderer.create(testEl(propsWithNStudents(3))).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('works with four students', () => { + const tree = renderer.create(testEl(propsWithNStudents(4))).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap b/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap new file mode 100644 index 0000000000..c13bdc16ee --- /dev/null +++ b/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap @@ -0,0 +1,220 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshots works with four students 1`] = ` +
+
+ + 💬 + + + + Darth + + Tater + + , + + Kylo + + Ren + + and + + 2 more students + + shared new student voice surveys. +
+
+`; + +exports[`snapshots works with one student 1`] = ` +
+
+ + 💬 + + + + Luke + + Skywalker + + shared a new student voice survey. +
+
+`; + +exports[`snapshots works with three students 1`] = ` +
+
+ + 💬 + + + + Luke + + Skywalker + + , + + Mari + + Kenobi + + and + + Kylo + + Ren + + shared new student voice surveys. +
+
+`; + +exports[`snapshots works with two students 1`] = ` +
+
+ + 💬 + + + + Luke + + Skywalker + + and + + Mari + + Kenobi + + shared new student voice surveys. +
+
+`; diff --git a/ui/config/.storybook/config.js b/ui/config/.storybook/config.js index f6a0b0427b..cd23d70a76 100644 --- a/ui/config/.storybook/config.js +++ b/ui/config/.storybook/config.js @@ -18,7 +18,10 @@ function loadStories() { // home require('../../../app/assets/javascripts/home/CheckStudentsWithHighAbsences.story'); + + // feed require('../../../app/assets/javascripts/feed/IncidentCard.story'); + require('../../../app/assets/javascripts/feed/StudentVoiceCard.story'); // student profile require('../../../app/assets/javascripts/student_profile/TakeNotes.story'); From b71bb1f0a603a874031cd0dab39cb9887653a6a7 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 13:42:18 -0500 Subject: [PATCH 5/8] Add label branching --- app/lib/feed.rb | 2 +- app/models/educator_label.rb | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/lib/feed.rb b/app/lib/feed.rb index 65e8ce7507..3cb9df9da3 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -37,7 +37,7 @@ def all_cards(time_now, limit) else [] end - student_voice_cards = if PerDistrict.new.include_student_voice_cards? + student_voice_cards = if PerDistrict.new.include_student_voice_cards? && current_educator.labels.include?('enable_student_voice_cards_in_feed') self.student_voice_cards(time_now) else [] diff --git a/app/models/educator_label.rb b/app/models/educator_label.rb index 61f866ed8b..f0d3620eba 100644 --- a/app/models/educator_label.rb +++ b/app/models/educator_label.rb @@ -8,25 +8,31 @@ class EducatorLabel < ApplicationRecord uniqueness: { scope: [:label_key, :educator] }, inclusion: { in: [ + # deprecated 'shs_experience_team', # deprecated - 'k8_counselor', - 'high_school_house_master', - 'class_list_maker_finalizer_principal', + 'enable_viewing_504_data_in_profile', # deprecated + + # feed 'use_counselor_based_feed', 'use_housemaster_based_feed', 'use_section_based_feed', 'use_ell_based_feed', 'use_community_school_based_feed', + 'enable_student_voice_cards_in_feed', + + # reading + 'profile_enable_minimal_reading_data', + 'enable_reading_benchmark_data_entry', + + # other + 'k8_counselor', + 'high_school_house_master', + 'class_list_maker_finalizer_principal', 'enable_class_lists_override', 'can_upload_student_voice_surveys', 'should_show_levels_shs_link', 'enable_searching_notes', - 'enable_viewing_504_data_in_profile', # deprecated - 'can_mark_notes_as_restricted', - - # reading - 'profile_enable_minimal_reading_data', - 'enable_reading_benchmark_data_entry' + 'can_mark_notes_as_restricted' ] } } From c6b2fdb26592378a70a86035f2ce92015d9001bc Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 13:46:28 -0500 Subject: [PATCH 6/8] Remove label --- app/lib/feed.rb | 2 +- app/models/educator_label.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/feed.rb b/app/lib/feed.rb index 3cb9df9da3..65e8ce7507 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -37,7 +37,7 @@ def all_cards(time_now, limit) else [] end - student_voice_cards = if PerDistrict.new.include_student_voice_cards? && current_educator.labels.include?('enable_student_voice_cards_in_feed') + student_voice_cards = if PerDistrict.new.include_student_voice_cards? self.student_voice_cards(time_now) else [] diff --git a/app/models/educator_label.rb b/app/models/educator_label.rb index f0d3620eba..e94b067ccc 100644 --- a/app/models/educator_label.rb +++ b/app/models/educator_label.rb @@ -18,7 +18,6 @@ class EducatorLabel < ApplicationRecord 'use_section_based_feed', 'use_ell_based_feed', 'use_community_school_based_feed', - 'enable_student_voice_cards_in_feed', # reading 'profile_enable_minimal_reading_data', From f9f117af14d9d97b83f8cc5f447eecf3dc77713d Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 14:26:14 -0500 Subject: [PATCH 7/8] Update NotesList to not show educator name for student voice surveys, since it was just placeholder --- .../javascripts/feed/StudentVoiceCard.js | 19 ++-- .../javascripts/feed/StudentVoiceCard.test.js | 2 +- .../StudentVoiceCard.test.js.snap | 92 +++++++++---------- .../javascripts/student_profile/NoteCard.js | 15 ++- .../javascripts/student_profile/NotesList.js | 6 +- .../student_profile/NotesList.test.js | 3 +- app/lib/feed.rb | 2 +- 7 files changed, 71 insertions(+), 68 deletions(-) diff --git a/app/assets/javascripts/feed/StudentVoiceCard.js b/app/assets/javascripts/feed/StudentVoiceCard.js index 628f31a436..c051c735a3 100644 --- a/app/assets/javascripts/feed/StudentVoiceCard.js +++ b/app/assets/javascripts/feed/StudentVoiceCard.js @@ -5,7 +5,7 @@ import hash from 'object-hash'; import Card from '../components/Card'; -// Render a card in the feed showing there are new student voice surveys in, +// Render a card in the feed showing there are student voice surveys in, // and allow clicking to see list of students and then jump to their profiles. export default class StudentVoiceCard extends React.Component { constructor(props) { @@ -43,18 +43,18 @@ export default class StudentVoiceCard extends React.Component { if (students.length === 0) { return null; // shouldn't happen, but guard } else if (students.length === 1) { - return
{emoji} {this.renderStudentLink(students[0])} shared a new student voice survey.
; + return
{this.renderStudentLink(students[0])} shared their perspective in a student voice survey. {emoji}
; } else if (students.length === 2) { - return
{emoji} {this.renderStudentLink(students[0])} and {this.renderStudentLink(students[1])} shared new student voice surveys.
; + return
{this.renderStudentLink(students[0])} and {this.renderStudentLink(students[1])} shared their perspective in student voice surveys. {emoji}
; } else if (students.length === 3) { - return
{emoji} {this.renderStudentLink(students[0])}, {this.renderStudentLink(students[1])} and {this.renderStudentLink(students[2])} shared new student voice surveys.
; + return
{this.renderStudentLink(students[0])}, {this.renderStudentLink(students[1])} and {this.renderStudentLink(students[2])} shared their perspective in student voice surveys. {emoji}
; } const shuffledStudents = _.sortBy(students, student => hash({...student, shuffleSeed})); if (!isExpanded) { - return
{emoji} {this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderExpandableListLink(shuffledStudents.slice(2))} shared new student voice surveys.
; + return
{this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderExpandableListLink(shuffledStudents.slice(2))} shared their perspective in student voice surveys. {emoji}
; } else { - return
{emoji} {this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderMoreStudentsText(shuffledStudents.slice(2))} shared new student voice surveys.{this.renderExpandedList(shuffledStudents.slice(2))}
; + return
{this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderMoreStudentsText(shuffledStudents.slice(2))} shared their perspective in student voice surveys. {emoji} {this.renderExpandedList(shuffledStudents.slice(2))}
; } } @@ -67,10 +67,11 @@ export default class StudentVoiceCard extends React.Component { } renderExpandedList(restOfStudents) { + const sortedStudents = _.sortBy(restOfStudents, student => `${student.last_name} ${student.first_name}`); return ( -
- {restOfStudents.map(student => ( -
{this.renderStudentLink(student)}
+
+ {sortedStudents.map(student => ( +
{this.renderStudentLink(student)}
))}
); diff --git a/app/assets/javascripts/feed/StudentVoiceCard.test.js b/app/assets/javascripts/feed/StudentVoiceCard.test.js index b0ab50c26d..9ec909c846 100644 --- a/app/assets/javascripts/feed/StudentVoiceCard.test.js +++ b/app/assets/javascripts/feed/StudentVoiceCard.test.js @@ -55,7 +55,7 @@ export function testEl(props) { it('renders without crashing', () => { const el = document.createElement('div'); ReactDOM.render(testEl(testProps()), el); - expect($(el).text()).toContain('💬 Darth Tater, Kylo Ren and 2 more students shared new student voice surveys.'); + expect($(el).text()).toContain('Darth Tater, Kylo Ren and 2 more students shared their perspective in student voice surveys. 💬'); expect($(el).find('a').toArray().map(el => $(el).attr('href'))).toEqual([ '/students/7', '/students/6', diff --git a/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap b/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap index c13bdc16ee..351a61a4b8 100644 --- a/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap +++ b/app/assets/javascripts/feed/__snapshots__/StudentVoiceCard.test.js.snap @@ -12,17 +12,6 @@ exports[`snapshots works with four students 1`] = ` } >
- - 💬 - - 2 more students - shared new student voice surveys. + shared their perspective in student voice surveys. + + 💬 +
`; @@ -72,17 +71,6 @@ exports[`snapshots works with one student 1`] = ` } > `; @@ -112,17 +110,6 @@ exports[`snapshots works with three students 1`] = ` } > `; @@ -178,17 +175,6 @@ exports[`snapshots works with two students 1`] = ` } > `; diff --git a/app/assets/javascripts/student_profile/NoteCard.js b/app/assets/javascripts/student_profile/NoteCard.js index 7ea08c8b92..b37a39bfbd 100644 --- a/app/assets/javascripts/student_profile/NoteCard.js +++ b/app/assets/javascripts/student_profile/NoteCard.js @@ -18,7 +18,9 @@ export default class NoteCard extends React.Component { } educator() { - return this.props.educatorsIndex[this.props.educatorId]; + const {educatorId, educatorsIndex} = this.props; + if (!educatorId) return null; + return educatorsIndex[educatorId]; } // No feedback, fire and forget @@ -38,6 +40,7 @@ export default class NoteCard extends React.Component { render() { const {includeStudentPanel} = this.props; + const educator = this.educator(); return (
{includeStudentPanel && this.renderStudentCard()} @@ -47,9 +50,11 @@ export default class NoteCard extends React.Component { {this.props.noteMoment.format('MMMM D, YYYY')} {this.props.badge} - - - + {educator && ( + + + + )}
{this.renderNoteSubstanceOrRedaction()} {this.renderAttachmentUrls()} @@ -210,7 +215,7 @@ export default class NoteCard extends React.Component { NoteCard.propTypes = { attachments: PropTypes.array.isRequired, badge: PropTypes.element.isRequired, - educatorId: PropTypes.number.isRequired, + educatorId: PropTypes.number, educatorsIndex: PropTypes.object.isRequired, noteMoment: PropTypes.instanceOf(moment).isRequired, text: PropTypes.string.isRequired, diff --git a/app/assets/javascripts/student_profile/NotesList.js b/app/assets/javascripts/student_profile/NotesList.js index c4c41e7e60..2d51c6067e 100644 --- a/app/assets/javascripts/student_profile/NotesList.js +++ b/app/assets/javascripts/student_profile/NotesList.js @@ -177,9 +177,9 @@ export default class NotesList extends React.Component { key={['flattened_form', flattenedForm.id].join()} noteMoment={toMomentFromRailsDate(flattenedForm.form_timestamp)} badge={{flattenedForm.form_title}} - educatorId={flattenedForm.educator_id} - text={flattenedForm.text} - educatorsIndex={this.props.educatorsIndex} + text={`💬 From the "${flattenedForm.form_title}" student voice survey 💬\n\n${flattenedForm.text}`} + educatorId={null} + educatorsIndex={{}} showRestrictedNoteRedaction={false} urlForRestrictedNoteContent={null} attachments={[]} /> diff --git a/app/assets/javascripts/student_profile/NotesList.test.js b/app/assets/javascripts/student_profile/NotesList.test.js index 0f14ec94d2..cd714c2793 100644 --- a/app/assets/javascripts/student_profile/NotesList.test.js +++ b/app/assets/javascripts/student_profile/NotesList.test.js @@ -231,6 +231,7 @@ describe('flattened forms', () => { expect($(el).find('.NoteText').length).toEqual(1); expect($(el).find('.EditableNoteText').length).toEqual(0); expect($(el).text()).toContain('What I want my teachers to know'); - expect($(el).find('.NoteText').text()).toEqual(''); + expect($(el).find('.NoteCard a').length).toEqual(0); + expect($(el).find('.NoteText').text()).toEqual('💬 From the "What I want my teachers to know" student voice survey 💬\n\n'); }); }); \ No newline at end of file diff --git a/app/lib/feed.rb b/app/lib/feed.rb index 65e8ce7507..cddd5bcde7 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -147,7 +147,7 @@ def incident_card(incident) # This uniques by (student_id, form_key), taking the most recent # by (form_timestamp, updated_at, id). - # + # # Using Arel.sql is safe for strings without user input, see https://github.com/rails/rails/issues/32995 # for more background. def imported_forms_for_card(time_now) From 97a53337b01f6ebbf8427a3890b9a858ed161c8c Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Tue, 29 Jan 2019 16:46:12 -0500 Subject: [PATCH 8/8] Use label for feed --- app/controllers/home_controller.rb | 4 +++- app/lib/feed.rb | 6 ++++-- app/models/educator_label.rb | 1 + spec/lib/feed_spec.rb | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 0377056571..1b9fa793de 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -10,7 +10,9 @@ def feed_json Feed.students_for_feed(view_as_educator) end feed = Feed.new(authorized_students) - feed_cards = feed.all_cards(time_now, limit) + feed_cards = feed.all_cards(time_now, limit, { + include_student_voice_cards: current_educator.labels.include?('enable_student_voice_cards_in_feed') + }) render json: { feed_cards: feed_cards } diff --git a/app/lib/feed.rb b/app/lib/feed.rb index cddd5bcde7..cc2ce44031 100644 --- a/app/lib/feed.rb +++ b/app/lib/feed.rb @@ -25,7 +25,9 @@ def initialize(authorized_students) # we query and combine them. Ideally we'd query in parallel but we'd # need to push this out to the client to do that (and still would have to # delay rendering until both came back and were merged anyway). - def all_cards(time_now, limit) + def all_cards(time_now, limit, options = {}) + include_student_voice_cards = options.fetch(:include_student_voice_cards, false) + event_note_cards = self.event_note_cards(time_now, limit) birthday_cards = self.birthday_cards(time_now, limit, { limit: 3, @@ -37,7 +39,7 @@ def all_cards(time_now, limit) else [] end - student_voice_cards = if PerDistrict.new.include_student_voice_cards? + student_voice_cards = if PerDistrict.new.include_student_voice_cards? && include_student_voice_cards self.student_voice_cards(time_now) else [] diff --git a/app/models/educator_label.rb b/app/models/educator_label.rb index e94b067ccc..f0d3620eba 100644 --- a/app/models/educator_label.rb +++ b/app/models/educator_label.rb @@ -18,6 +18,7 @@ class EducatorLabel < ApplicationRecord 'use_section_based_feed', 'use_ell_based_feed', 'use_community_school_based_feed', + 'enable_student_voice_cards_in_feed', # reading 'profile_enable_minimal_reading_data', diff --git a/spec/lib/feed_spec.rb b/spec/lib/feed_spec.rb index d6075db89e..814fb44b45 100644 --- a/spec/lib/feed_spec.rb +++ b/spec/lib/feed_spec.rb @@ -82,7 +82,8 @@ def create_event_note(time_now, options = {}) student: pals.shs_freshman_mari }) - feed_cards = feed_for(pals.shs_jodi).all_cards(time_now, limit) + feed = feed_for(pals.shs_jodi) + feed_cards = feed.all_cards(time_now, limit, include_student_voice_cards: true) expect(feed_cards.size).to eq 4 expect(feed_cards.as_json).to eq([{ "type"=>"birthday_card",