def test_code_signup_student_name_strips_extra_lines(db): """Extra lines (eg SMS auto-sig) not included in student name.""" phone = '+13216430987' teacher = factories.ProfileFactory.create( school_staff=True, name="Teacher Jane", code="ABCDEF") factories.TextSignupFactory.create( family__name="John Doe", family__phone=phone, family__invited_by=teacher, state=model.TextSignup.STATE.kidname, teacher=teacher, ) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: hook.receive_sms( phone, settings.DEFAULT_NUMBER, "Jimmy Doe\nLook at me!") parent = model.Profile.objects.get(phone=phone) student = parent.students[0] assert student.name == u"Jimmy Doe" mock_create.assert_any_call( parent, student, "Jimmy Doe\nLook at me!", from_sms=True, notifications=False, )
def test_code_signup_name_strips_extra_lines(db): """Extra lines (e.g. SMS auto-sig) excluded from parent name.""" phone = '+13216430987' teacher_rel = factories.RelationshipFactory.create( from_profile__school_staff=True, from_profile__name="Teacher Jane", from_profile__user__email='*****@*****.**', to_profile__name="Jimmy Doe", ) parent_rel = factories.RelationshipFactory.create( from_profile__name="", from_profile__phone=phone, from_profile__invited_by=teacher_rel.elder, to_profile=teacher_rel.student, ) factories.TextSignupFactory.create( family=parent_rel.elder, teacher=teacher_rel.elder, student=teacher_rel.student, state=model.TextSignup.STATE.name, ) with mock.patch('portfoliyo.sms.hook.model.Post.create'): hook.receive_sms( phone, settings.DEFAULT_NUMBER, "\n John Doe\nI'm a sig too!") parent = model.Profile.objects.get(phone=phone) assert parent.name == "John Doe"
def test_unusually_long_parent_name_tracked(db): """If a family member name is unusually long, it is tracked.""" phone = '+13216430987' teacher_rel = factories.RelationshipFactory.create( from_profile__school_staff=True, from_profile__name="Teacher Jane", from_profile__user__email='*****@*****.**', to_profile__name="Jimmy Doe", ) parent_rel = factories.RelationshipFactory.create( from_profile__name="", from_profile__phone=phone, from_profile__invited_by=teacher_rel.elder, to_profile=teacher_rel.student, ) factories.TextSignupFactory.create( family=parent_rel.elder, teacher=teacher_rel.elder, student=teacher_rel.student, state=model.TextSignup.STATE.name, ) msg = "Hi there Ms. Waggoner this is Joe Smith how is Jimmy doing?" with mock.patch('portfoliyo.sms.hook.model.Post.create'): with mock.patch('portfoliyo.sms.hook.track_sms') as mock_track: hook.receive_sms(phone, settings.DEFAULT_NUMBER, msg) mock_track.assert_called_with('long answer', phone, msg)
def test_multiple_active_signups_logs_warning(db): """If a user has multiple active signups, a warning is logged.""" phone = '+13216430987' signup = factories.TextSignupFactory.create(family__phone=phone) factories.TextSignupFactory.create(family=signup.family) with mock.patch('portfoliyo.sms.hook.logger') as mock_logger: hook.receive_sms(phone, settings.DEFAULT_NUMBER, "Jimmy Doe") mock_logger.warning.assert_called_with( "User %s has multiple active signups!", phone)
def test_code_signup_student_name_dupe_detection(db): """Don't create duplicate students in a teacher's class.""" phone = '+13216430987' rel = factories.RelationshipFactory.create( from_profile__school_staff=True, to_profile__name="Jimmy Doe") factories.TextSignupFactory.create( family__name="John Doe", family__phone=phone, family__invited_by=rel.elder, teacher=rel.elder, state=model.TextSignup.STATE.kidname, ) with mock.patch('portfoliyo.sms.hook.model.Post.create'): reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "Jimmy Doe") assert reply == ( "And what is your relationship to that child " "(mother, father, ...)?" ) parent = model.Profile.objects.get(phone=phone) signup = parent.signups.get() assert len(parent.students) == 1 student = parent.students[0] assert student == rel.student assert signup.student == rel.student
def test_subsequent_signup_when_teacher_already_in_village(db): """If parent sends a second code for a teacher already there, no reply.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, state=model.TextSignup.STATE.done, student=factories.ProfileFactory.create(), teacher__code='ABCDEF', ) factories.RelationshipFactory.create( from_profile=signup.teacher, to_profile=signup.student) factories.RelationshipFactory.create( from_profile=signup.family, to_profile=signup.student) rn_tgt = 'portfoliyo.tasks.record_notification.delay' create_tgt = 'portfoliyo.sms.hook.model.Post.create' with mock.patch(rn_tgt) as mock_record_notification: with mock.patch(create_tgt) as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply is None assert signup.family.signups.count() == 1 # incoming text is recorded in village mock_create.assert_called_with( signup.family, signup.student, "ABCDEF", from_sms=True, ) assert mock_record_notification.call_count == 0
def test_activate_user(db): """Receiving SMS from inactive user activates and gives them more info.""" phone = '+13216430987' profile = factories.ProfileFactory.create( user__is_active=False, phone=phone, declined=True) rel = factories.RelationshipFactory.create( from_profile=profile, to_profile__name="Jimmy Doe") # prevent this from being a "no teachers" situation factories.RelationshipFactory.create( from_profile__school_staff=True, from_profile__name='Ms. Johns', to_profile=rel.student, ) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'foo') profile = utils.refresh(profile) assert profile.user.is_active assert not profile.declined mock_create.assert_any_call( None, rel.student, reply, in_reply_to=phone, notifications=False) assert reply == ( "You can text this number " "to talk with Ms. Johns." )
def test_subsequent_group_signup(db): """A parent can send a group code after completing first signup.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, state=model.TextSignup.STATE.done, student=factories.ProfileFactory.create(), ) factories.RelationshipFactory.create( from_profile=signup.teacher, to_profile=signup.student) factories.RelationshipFactory.create( from_profile=signup.family, to_profile=signup.student) group = factories.GroupFactory.create( code='ABCDEF', owner__name='Ms. Doe') with mock.patch('portfoliyo.sms.hook.track_signup') as mock_track: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply == ( "Ok, thanks! You can text Ms. Doe at this number too.") new_signup = signup.family.signups.exclude(pk=signup.pk).get() assert new_signup.state == model.TextSignup.STATE.done assert new_signup.teacher == group.owner assert new_signup.student == signup.student assert new_signup.group == group assert signup.student in group.owner.students assert group.students.filter(pk=signup.student.pk).exists() mock_track.assert_called_with(signup.family, group.owner, group)
def test_subsequent_signup_when_first_needs_name(db): """If first signup needs name, second takes over where it left off.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, state=model.TextSignup.STATE.name, student=factories.ProfileFactory.create(), ) factories.RelationshipFactory.create( from_profile=signup.teacher, to_profile=signup.student) factories.RelationshipFactory.create( from_profile=signup.family, to_profile=signup.student) other_teacher = factories.ProfileFactory.create( code='ABCDEF', name='Ms. Doe') reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply == ( "Ok, thanks! You can text Ms. Doe at this number too. " "And what's your name?" ) new_signup = signup.family.signups.exclude(pk=signup.pk).get() signup = utils.refresh(signup) assert signup.state == model.TextSignup.STATE.done assert new_signup.state == model.TextSignup.STATE.name assert new_signup.teacher == other_teacher assert new_signup.student == signup.student assert new_signup.group is None
def test_code_signup(db): """Parent can create account by texting teacher code.""" phone = '+13216430987' source_phone = '+13336660000' teacher = factories.ProfileFactory.create( school_staff=True, name="Teacher Jane", code="ABCDEF", country_code='ca') with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: with mock.patch('portfoliyo.sms.hook.track_signup') as mock_track: reply = hook.receive_sms(phone, source_phone, "abcdef") assert reply == ( "Thanks! What is the first and last name of your child in Teacher Jane's class?" ) profile = model.Profile.objects.get(phone=phone) signup = profile.signups.get() assert profile.name == "" assert profile.country_code == 'ca' assert profile.source_phone == source_phone assert signup.state == model.TextSignup.STATE.kidname assert profile.invited_by == teacher assert signup.teacher == teacher assert signup.student is None assert signup.family == profile assert not mock_create.call_count mock_track.assert_called_with(profile, teacher, None)
def test_easter_egg(): phone = '+3216430987' reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'xjgdlw') assert reply == ( "Woah! You actually tried it out? A cookie for you! " "Email [email protected] and we'll send you a cookie." )
def test_unusually_long_student_name_tracked(db): """If a student name is unusually long, it is tracked.""" phone = '+13216430987' teacher = factories.ProfileFactory.create( school_staff=True, name="Teacher Jane", code="ABCDEF") factories.TextSignupFactory.create( family__name="John Doe", family__phone=phone, family__invited_by=teacher, state=model.TextSignup.STATE.kidname, teacher=teacher, ) msg = "Hi there Ms. Waggoner this is Joe Smith how is Jimmy doing?" with mock.patch('portfoliyo.sms.hook.model.Post.create'): with mock.patch('portfoliyo.sms.hook.track_sms') as mock_track: hook.receive_sms(phone, settings.DEFAULT_NUMBER, msg) mock_track.assert_called_with("long answer", phone, msg)
def test_unknown_profile(db): """Reply if profile is unknown.""" reply = hook.receive_sms('123', settings.DEFAULT_NUMBER, 'foo') assert reply == ( "We don't recognize your phone number, " "so we don't know who to send your text to! " "If you are just signing up, " "make sure your invite code is typed correctly." )
def test_no_students(db): """Reply if no associated students.""" phone = '+13216430987' factories.ProfileFactory.create(phone=phone) reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'foo') assert reply == ( "Sorry, we can't find any students connected to your number, " "so we can't deliver your message. " "Please text a teacher code to connect with that teacher." )
def test_subsequent_signup(db): """A parent can send a second code after completing first signup.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, state=model.TextSignup.STATE.done, student=factories.ProfileFactory.create(), ) factories.RelationshipFactory.create( from_profile=signup.teacher, to_profile=signup.student) factories.RelationshipFactory.create( from_profile=signup.family, to_profile=signup.student) other_teacher = factories.ProfileFactory.create( code='ABCDEF', name='Ms. Doe') rn_tgt = 'portfoliyo.tasks.record_notification.delay' create_tgt = 'portfoliyo.sms.hook.model.Post.create' with mock.patch('portfoliyo.sms.hook.track_signup') as mock_track: with mock.patch(rn_tgt) as mock_record_notification: with mock.patch(create_tgt) as mock_create: reply = hook.receive_sms( phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply == ( "Ok, thanks! You can text Ms. Doe at this number too.") new_signup = signup.family.signups.exclude(pk=signup.pk).get() assert new_signup.state == model.TextSignup.STATE.done assert new_signup.teacher == other_teacher assert new_signup.student == signup.student assert new_signup.group is None assert signup.student in other_teacher.students # both incoming text and reply are recorded in village assert mock_create.call_count == 2 mock_create.assert_any_call( signup.family, signup.student, "ABCDEF", from_sms=True, ) mock_create.assert_any_call( None, signup.student, reply, in_reply_to=u'+13216430987', notifications=False, ) assert mock_record_notification.call_count == 2 mock_record_notification.assert_any_call( 'village_additions', signup.family, [other_teacher], [signup.student]) mock_record_notification.assert_any_call( 'new_parent', other_teacher, new_signup) mock_track.assert_called_with(signup.family, other_teacher, None)
def test_code_signup_with_language(db): """Parent can include language code in starting code signup.""" phone = '+13216430987' factories.ProfileFactory.create( school_staff=True, name="Teacher Joe", code="ABCDEF") with mock.patch('portfoliyo.sms.hook.model.Post.create'): reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "abcdef ES") assert reply == ( u"¡Gracias! ¿Cuál es el nombre de su hijo en la clase del Teacher Joe?") profile = model.Profile.objects.get(phone=phone) assert profile.lang_code == 'es'
def test_orphan_student(db): """Reply if no student has a teacher.""" phone = '+13216430987' factories.RelationshipFactory.create( from_profile__phone=phone) reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'foo') assert reply == ( "Sorry, we can't find any teachers connected to your number, " "so we can't deliver your message. " "Please text a teacher code to connect with that teacher." )
def test_create_post(db): """Creates Post (and no reply) if one associated student.""" phone = '+13216430987' profile = factories.ProfileFactory.create(phone=phone) rel = factories.RelationshipFactory.create(from_profile=profile) # prevent this from being a "no teachers" situation factories.RelationshipFactory.create( from_profile__school_staff=True, to_profile=rel.student) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'foo') assert reply is None mock_create.assert_called_once_with( profile, rel.student, 'foo', from_sms=True)
def twilio_receive(request): """Receive an SMS via Twilio.""" source = request.POST['From'] to = request.POST['To'] body = request.POST['Body'] with xact.xact(): reply = hook.receive_sms(source, to, body) response = twiml.Response() if reply: for chunk in split_sms(reply): response.sms(chunk) return response
def test_multiple_students(db): """If multiple associated students, post goes in both villages.""" phone = '+13216430987' profile = factories.ProfileFactory.create(phone=phone) rel1 = factories.RelationshipFactory.create(from_profile=profile) rel2 = factories.RelationshipFactory.create(from_profile=profile) # prevent this from being a "no teachers" situation factories.RelationshipFactory.create( from_profile__school_staff=True, to_profile=rel1.student) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'foo') mock_create.assert_any_call(profile, rel1.student, 'foo', from_sms=True) mock_create.assert_any_call(profile, rel2.student, 'foo', from_sms=True) assert reply is None
def twilio_receive(request): """Receive an SMS via Twilio.""" source = request.POST["From"] to = request.POST["To"] body = request.POST["Body"] with xact.xact(): reply = hook.receive_sms(source, to, body) response = twiml.Response() if reply: for chunk in split_sms(reply): response.sms(chunk) return response
def test_subsequent_signup_when_no_students(db): """If a parent has no students they can text a code to start over.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, teacher__code='ABCDEF', teacher__name='Ms. Doe', state=model.TextSignup.STATE.done, ) reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply == ( "Thanks! What is the first and last name of your child in Ms. Doe's class?") new_signup = signup.family.signups.exclude(pk=signup.pk).get() assert new_signup.state == model.TextSignup.STATE.kidname assert new_signup.teacher == signup.teacher assert new_signup.group is None
def test_group_code_signup_student_name(db): """Parent can continue group code signup by providing student name.""" phone = '+13216430987' group = factories.GroupFactory.create( owner__school_staff=True, owner__name="Teacher Jane", code="ABCDEFG") factories.TextSignupFactory.create( family__name="John Doe", family__phone=phone, family__invited_by=group.owner, group=group, teacher=group.owner, state=model.TextSignup.STATE.kidname, ) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: with mock.patch( 'portfoliyo.pusher.events.student_added') as mock_student_added: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "Jimmy Doe") assert reply == ( "And what is your relationship to that child " "(mother, father, ...)?" ) parent = model.Profile.objects.get(phone=phone) signup = parent.signups.get() assert len(group.owner.students) == 1 assert group.owner.student_relationships[0].level == 'owner' assert len(parent.students) == 1 student = parent.students[0] mock_student_added.assert_any_call(student.id, [group.owner.id]) mock_student_added.assert_any_call(student.id, [parent.id]) assert set(student.student_in_groups.all()) == {group} assert student.name == u"Jimmy Doe" assert student.invited_by == group.owner assert student.school == group.owner.school assert set(student.elders) == set([group.owner, parent]) assert signup.state == model.TextSignup.STATE.relationship assert signup.student == student # and the name is sent on to the village chat as a post mock_create.assert_any_call( parent, student, "Jimmy Doe", from_sms=True, notifications=False) # and the automated reply is also sent on to village chat mock_create.assert_any_call( None, student, reply, in_reply_to=phone, notifications=False)
def test_active_user_decline(db): """If an active user replies with 'stop', they are marked declined.""" phone = '+13216430987' profile = factories.ProfileFactory.create( user__is_active=True, phone=phone) rel = factories.RelationshipFactory.create(from_profile=profile) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'stop') assert not utils.refresh(profile.user).is_active assert utils.refresh(profile).declined assert reply == ( "No problem! Sorry to have bothered you. " "Text this number anytime to re-start." ) mock_create.assert_any_call(profile, rel.student, "stop", from_sms=True) mock_create.assert_any_call( None, rel.student, reply, in_reply_to=phone, notifications=False)
def test_code_signup_name(db): """Parent can finish signup by texting their name.""" phone = '+13216430987' teacher_rel = factories.RelationshipFactory.create( from_profile__school_staff=True, from_profile__notify_new_parent=True, from_profile__name="Teacher Jane", from_profile__user__email='*****@*****.**', to_profile__name="Jimmy Doe", ) parent_rel = factories.RelationshipFactory.create( from_profile__name="", from_profile__phone=phone, from_profile__invited_by=teacher_rel.elder, to_profile=teacher_rel.student, ) factories.TextSignupFactory.create( family=parent_rel.elder, teacher=teacher_rel.elder, student=teacher_rel.student, state=model.TextSignup.STATE.name, ) record_notification_path = 'portfoliyo.tasks.record_notification.delay' with mock.patch(record_notification_path) as mock_record_notification: with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "John Doe") assert reply == ( "All done, thank you! You can text this number " "to talk with Teacher Jane." ) parent = model.Profile.objects.get(phone=phone) signup = parent.signups.get() assert parent.name == "John Doe" assert signup.state == model.TextSignup.STATE.done student = teacher_rel.student mock_create.assert_any_call( parent, student, "John Doe", from_sms=True, notifications=False) # and the automated reply is also sent on to village chat mock_create.assert_any_call( None, student, reply, in_reply_to=phone, notifications=False) mock_record_notification.assert_called_with('new_parent', teacher_rel.elder, signup)
def test_code_signup_role(db): """Parent can continue code signup by providing their role.""" phone = '+13216430987' teacher_rel = factories.RelationshipFactory.create( from_profile__school_staff=True, from_profile__name='Jane Doe', from_profile__user__email='*****@*****.**', to_profile__name="Jimmy Doe") parent_rel = factories.RelationshipFactory.create( from_profile__name="John Doe", from_profile__role="", from_profile__phone=phone, from_profile__invited_by=teacher_rel.elder, to_profile=teacher_rel.student, description="", ) factories.TextSignupFactory.create( family=parent_rel.elder, student=parent_rel.student, teacher=teacher_rel.elder, state=model.TextSignup.STATE.relationship, ) with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "father") assert reply == ( "Last question: what is your name? (So Jane Doe knows who is texting.)") parent = model.Profile.objects.get(phone=phone) signup = parent.signups.get() assert parent.role == "father" assert signup.state == model.TextSignup.STATE.name parent_rel = utils.refresh(parent_rel) assert parent_rel.description == "father" student = teacher_rel.student # and the role is sent on to the village chat as a post mock_create.assert_any_call( parent, student, "father", from_sms=True, notifications=False) # and the automated reply is also sent on to village chat mock_create.assert_any_call( None, student, reply, in_reply_to=phone, notifications=False)
def test_subsequent_group_signup_when_first_needs_student_name(db): """If first signup needs student name, second takes over there.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, state=model.TextSignup.STATE.kidname, ) group = factories.GroupFactory.create( code='ABCDEF', owner__name='Ms. Doe') reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF') assert reply == ( "Ok, thanks! You can text Ms. Doe at this number too. " "And what's the student's first and last name?" ) new_signup = signup.family.signups.exclude(pk=signup.pk).get() signup = utils.refresh(signup) assert signup.state == model.TextSignup.STATE.done assert new_signup.state == model.TextSignup.STATE.kidname assert new_signup.teacher == group.owner assert new_signup.student is None assert new_signup.group == group
def test_subsequent_signup_with_language(db): """A parent can update their language with their second code.""" phone = '+13216430987' signup = factories.TextSignupFactory.create( family__phone=phone, family__lang_code='en', state=model.TextSignup.STATE.done, student=factories.ProfileFactory.create(), ) factories.RelationshipFactory.create( from_profile=signup.teacher, to_profile=signup.student) factories.RelationshipFactory.create( from_profile=signup.family, to_profile=signup.student) factories.ProfileFactory.create( code='ABCDEF', name='Ms. Doe') with mock.patch('portfoliyo.sms.hook.model.Post.create'): reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, 'ABCDEF Es') profile = utils.refresh(signup.family) assert profile.lang_code == 'es' assert reply == ( u"¡Ok, gracias! Usted puede texto del Ms. Doe en este número también.")
def test_group_code_signup(db): """Parent can create account by texting group code.""" phone = '+13216430987' group = factories.GroupFactory.create( owner__school_staff=True, owner__name="Teacher Jane", code="ABCDEFG") with mock.patch('portfoliyo.sms.hook.model.Post.create') as mock_create: with mock.patch('portfoliyo.sms.hook.track_signup') as mock_track: reply = hook.receive_sms(phone, settings.DEFAULT_NUMBER, "abcdefg") assert reply == ( "Thanks! What is the first and last name of your child in Teacher Jane's class?" ) profile = model.Profile.objects.get(phone=phone) signup = profile.signups.get() assert profile.name == "" assert signup.state == model.TextSignup.STATE.kidname assert profile.invited_by == group.owner assert signup.teacher == group.owner assert signup.group == group assert signup.student is None assert signup.family == profile assert not mock_create.call_count mock_track.assert_called_with(profile, group.owner, group)
def test_bogus_signup_state_no_blowup(db): """An unknown signup state acts like no in-process signup.""" phone = '+13216430987' factories.TextSignupFactory.create(family__phone=phone, state='foo') hook.receive_sms(phone, settings.DEFAULT_NUMBER, "Hello")