-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Instructors can view previously sent email content #4451
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import requests | ||
| import datetime | ||
| import ddt | ||
| import random | ||
| from urllib import quote | ||
| from django.test import TestCase | ||
| from nose.tools import raises | ||
|
|
@@ -31,6 +32,8 @@ | |
| from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory | ||
| from student.roles import CourseBetaTesterRole | ||
| from microsite_configuration import microsite | ||
| from util.date_utils import get_default_time_display | ||
| from instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo | ||
|
|
||
| from student.models import CourseEnrollment, CourseEnrollmentAllowed | ||
| from courseware.models import StudentModule | ||
|
|
@@ -1321,7 +1324,6 @@ def assert_update_forum_role_membership(self, unique_student_identifier, rolenam | |
| self.assertNotIn(rolename, user_roles) | ||
|
|
||
|
|
||
|
|
||
| @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) | ||
| class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase): | ||
| """ | ||
|
|
@@ -1802,7 +1804,7 @@ def test_list_instructor_tasks_running(self, act): | |
| act.return_value = self.tasks | ||
| url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| mock_factory = MockCompletionInfo() | ||
| with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: | ||
| with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info: | ||
| mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info | ||
| response = self.client.get(url, {}) | ||
| self.assertEqual(response.status_code, 200) | ||
|
|
@@ -1821,7 +1823,7 @@ def test_list_background_email_tasks(self, act): | |
| act.return_value = self.tasks | ||
| url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| mock_factory = MockCompletionInfo() | ||
| with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: | ||
| with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info: | ||
| mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info | ||
| response = self.client.get(url, {}) | ||
| self.assertEqual(response.status_code, 200) | ||
|
|
@@ -1840,7 +1842,7 @@ def test_list_instructor_tasks_problem(self, act): | |
| act.return_value = self.tasks | ||
| url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| mock_factory = MockCompletionInfo() | ||
| with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: | ||
| with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info: | ||
| mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info | ||
| response = self.client.get(url, { | ||
| 'problem_location_str': self.problem_urlname, | ||
|
|
@@ -1861,7 +1863,7 @@ def test_list_instructor_tasks_problem_student(self, act): | |
| act.return_value = self.tasks | ||
| url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| mock_factory = MockCompletionInfo() | ||
| with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: | ||
| with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info: | ||
| mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info | ||
| response = self.client.get(url, { | ||
| 'problem_location_str': self.problem_urlname, | ||
|
|
@@ -1879,6 +1881,104 @@ def test_list_instructor_tasks_problem_student(self, act): | |
| self.assertEqual(actual_tasks, expected_tasks) | ||
|
|
||
|
|
||
| @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) | ||
| @patch.object(instructor_task.api, 'get_instructor_task_history') | ||
| class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCase): | ||
| """ | ||
| Test the instructor email content history endpoint. | ||
| """ | ||
| def setUp(self): | ||
| self.course = CourseFactory.create() | ||
| self.instructor = InstructorFactory(course_key=self.course.id) | ||
| self.client.login(username=self.instructor.username, password='test') | ||
|
|
||
| def tearDown(self): | ||
| """ | ||
| Undo all patches. | ||
| """ | ||
| patch.stopall() | ||
|
|
||
| def setup_fake_email_info(self, num_emails): | ||
| """ Initialize the specified number of fake emails """ | ||
| self.tasks = {} | ||
| self.emails = {} | ||
| self.emails_info = {} | ||
| for email_id in range(num_emails): | ||
| num_sent = random.randint(1, 15401) | ||
| self.tasks[email_id] = FakeContentTask(email_id, num_sent, 'expected') | ||
| self.emails[email_id] = FakeEmail(email_id) | ||
| self.emails_info[email_id] = FakeEmailInfo(self.emails[email_id], num_sent) | ||
|
|
||
| def get_matching_mock_email(self, *args, **kwargs): | ||
| """ Returns the matching mock emails for the given id """ | ||
| email_id = kwargs.get('id', 0) | ||
| return self.emails[email_id] | ||
|
|
||
| def get_email_content_response(self, num_emails, task_history_request): | ||
| """ Calls the list_email_content endpoint and returns the repsonse """ | ||
| self.setup_fake_email_info(num_emails) | ||
| task_history_request.return_value = self.tasks.values() | ||
| url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| with patch('instructor.views.api.CourseEmail.objects.get') as mock_email_info: | ||
| mock_email_info.side_effect = self.get_matching_mock_email | ||
| response = self.client.get(url, {}) | ||
| self.assertEqual(response.status_code, 200) | ||
| return response | ||
|
|
||
| def test_content_list_one_email(self, task_history_request): | ||
| """ Test listing of bulk emails when email list has one email """ | ||
| response = self.get_email_content_response(1, task_history_request) | ||
| self.assertTrue(task_history_request.called) | ||
| email_info = json.loads(response.content)['emails'] | ||
|
|
||
| # Emails list should have one email | ||
| self.assertEqual(len(email_info), 1) | ||
|
|
||
| # Email content should be what's expected | ||
| expected_message = self.emails[0].html_message | ||
| returned_email_info = email_info[0] | ||
| received_message = returned_email_info[u'email'][u'html_message'] | ||
| self.assertEqual(expected_message, received_message) | ||
|
|
||
| def test_content_list_no_emails(self, task_history_request): | ||
| """ Test listing of bulk emails when email list empty """ | ||
| response = self.get_email_content_response(0, task_history_request) | ||
| self.assertTrue(task_history_request.called) | ||
| email_info = json.loads(response.content)['emails'] | ||
|
|
||
| # Emails list should be empty | ||
| self.assertEqual(len(email_info), 0) | ||
|
|
||
| def test_content_list_email_content_many(self, task_history_request): | ||
| """ Test listing of bulk emails sent large amount of emails """ | ||
| response = self.get_email_content_response(50, task_history_request) | ||
| self.assertTrue(task_history_request.called) | ||
| expected_email_info = [email_info.to_dict() for email_info in self.emails_info.values()] | ||
| actual_email_info = json.loads(response.content)['emails'] | ||
|
|
||
| self.assertEqual(len(actual_email_info), 50) | ||
| for exp_email, act_email in zip(expected_email_info, actual_email_info): | ||
| self.assertDictEqual(exp_email, act_email) | ||
|
|
||
| self.assertEqual(actual_email_info, expected_email_info) | ||
|
|
||
| def test_list_email_content_error(self, task_history_request): | ||
| """ Test handling of error retrieving email """ | ||
| self.invalid_task = FakeContentTask(0, 0, 'test') | ||
| self.invalid_task.make_invalid_input() | ||
| task_history_request.return_value = [self.invalid_task] | ||
| url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
| response = self.client.get(url, {}) | ||
| self.assertEqual(response.status_code, 200) | ||
|
|
||
| self.assertTrue(task_history_request.called) | ||
| returned_email_info = json.loads(response.content)['emails'] | ||
| self.assertEqual(len(returned_email_info), 1) | ||
| returned_info = returned_email_info[0] | ||
| for info in ['created', 'sent_to', 'email', 'number_sent']: | ||
| self.assertEqual(returned_info[info], None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This just returns None everywhere? No error messages?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose to have get_instructor_task_history return None for all fields on error, and respond to it on the front end. If there's a better way to handle the possibility for errors loading the email information, I'm open to suggestions
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this comment before I had read through the whole PR. I think as long as it's well documented what the |
||
|
|
||
|
|
||
| @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) | ||
| @override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/") | ||
| @override_settings(ANALYTICS_API_KEY="robot_api_key") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| """ | ||
| Utilities for instructor unit tests | ||
| """ | ||
| import datetime | ||
| import json | ||
| import random | ||
| from django.utils.timezone import utc | ||
| from util.date_utils import get_default_time_display | ||
|
|
||
|
|
||
| class FakeInfo(object): | ||
| """Parent class for faking objects used in tests""" | ||
| FEATURES = [] | ||
|
|
||
| def __init__(self): | ||
| for feature in self.FEATURES: | ||
| setattr(self, feature, u'expected') | ||
|
|
||
| def to_dict(self): | ||
| """ Returns a dict representation of the object """ | ||
| return {key: getattr(self, key) for key in self.FEATURES} | ||
|
|
||
|
|
||
| class FakeContentTask(FakeInfo): | ||
| """ Fake task info needed for email content list """ | ||
| FEATURES = [ | ||
| 'task_input', | ||
| 'task_output', | ||
| ] | ||
|
|
||
| def __init__(self, email_id, num_sent, sent_to): | ||
| super(FakeContentTask, self).__init__() | ||
| self.task_input = {'email_id': email_id, 'to_option': sent_to} | ||
| self.task_input = json.dumps(self.task_input) | ||
| self.task_output = {'total': num_sent} | ||
| self.task_output = json.dumps(self.task_output) | ||
|
|
||
| def make_invalid_input(self): | ||
| """Corrupt the task input field to test errors""" | ||
| self.task_input = "THIS IS INVALID JSON" | ||
|
|
||
|
|
||
| class FakeEmail(FakeInfo): | ||
| """ Corresponding fake email for a fake task """ | ||
| FEATURES = [ | ||
| 'subject', | ||
| 'html_message', | ||
| 'id', | ||
| 'created', | ||
| ] | ||
|
|
||
| def __init__(self, email_id): | ||
| super(FakeEmail, self).__init__() | ||
| self.id = unicode(email_id) | ||
| # Select a random data for create field | ||
| year = random.choice(range(1950, 2000)) | ||
| month = random.choice(range(1, 12)) | ||
| day = random.choice(range(1, 28)) | ||
| hour = random.choice(range(0, 23)) | ||
| minute = random.choice(range(0, 59)) | ||
| self.created = datetime.datetime(year, month, day, hour, minute, tzinfo=utc) | ||
|
|
||
|
|
||
| class FakeEmailInfo(FakeInfo): | ||
| """ Fake email information object """ | ||
| FEATURES = [ | ||
| u'created', | ||
| u'sent_to', | ||
| u'email', | ||
| u'number_sent' | ||
| ] | ||
|
|
||
| EMAIL_FEATURES = [ | ||
| u'subject', | ||
| u'html_message', | ||
| u'id' | ||
| ] | ||
|
|
||
| def __init__(self, fake_email, num_sent): | ||
| super(FakeEmailInfo, self).__init__() | ||
| self.created = get_default_time_display(fake_email.created) | ||
| self.number_sent = num_sent | ||
| fake_email_dict = fake_email.to_dict() | ||
| self.email = {feature: fake_email_dict[feature] for feature in self.EMAIL_FEATURES} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do this test of content (L2057-2063) for 10 emails, and now for 50. Why? Is there a risk that 50 will return different results than 10? I would say it makes much more sense to do this test for 1 email (to verify correctness) and then for 50 emails (to verify it can handle a large amount).