diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 0c7ae47997cd..8cd555e3f2e0 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -66,6 +66,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.request_cache import clear_cache, get_cache +from openedx.core.djangoapps.signals.signals import USER_ACCOUNT_ACTIVATED from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager from openedx.core.djangolib.model_mixins import DeletableByUserValue @@ -728,6 +729,7 @@ def activate(self): self.user.is_active = True self._track_activation() self.user.save() + USER_ACCOUNT_ACTIVATED.send_robust(self.__class__, user=self.user) log.info(u'User %s (%s) account is successfully activated.', self.user.username, self.user.email) def _track_activation(self): diff --git a/common/djangoapps/student/tests/test_activate_account.py b/common/djangoapps/student/tests/test_activate_account.py index 099516b53a4f..29c74a6cddd9 100644 --- a/common/djangoapps/student/tests/test_activate_account.py +++ b/common/djangoapps/student/tests/test_activate_account.py @@ -98,6 +98,19 @@ def test_activation_with_keys(self, mock_segment_identify): expected_segment_mailchimp_list ) + @patch('student.models.USER_ACCOUNT_ACTIVATED') + def test_activation_signal(self, mock_signal): + """ + Verify that USER_ACCOUNT_ACTIVATED is emitted upon account email activation. + + Appsembler: This is a custom code to be pushed upstream. + """ + assert not self.user.is_active, 'Ensure that the user starts inactive' + assert not mock_signal.send_robust.call_count, 'Ensure no signal is fired before activation' + self.registration.activate() # Until you explicitly activate it + assert self.user.is_active, 'Sanity check for .activate()' + mock_signal.send_robust.assert_called_once_with(Registration, user=self.user), 'Ensure the signal is emitted' + @override_settings(LMS_SEGMENT_KEY="testkey") @patch('student.models.analytics.identify') def test_activation_without_mailchimp_key(self, mock_segment_identify): diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index c13567c82d3e..78ae3b4daa92 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -60,6 +60,7 @@ # These should be removed from HTML output, including all subelements html_problem_semantics = [ + "additional_answer", "codeparam", "responseparam", "answer", diff --git a/common/lib/capa/capa/tests/test_capa_problem.py b/common/lib/capa/capa/tests/test_capa_problem.py index 59ca1d442144..85016058bb93 100644 --- a/common/lib/capa/capa/tests/test_capa_problem.py +++ b/common/lib/capa/capa/tests/test_capa_problem.py @@ -185,6 +185,23 @@ def test_multiple_descriptions(self): } ) + def test_additional_answer_is_skipped_from_resulting_html(self): + """Tests that additional_answer element is not present in transformed HTML""" + xml = """ + +

Be sure to check your spelling.

+ + + Anyone who looks the world as if it was a game of chess deserves to lose. + + + +
+ """ + problem = new_loncapa_problem(xml) + self.assertEqual(len(problem.extracted_tree.xpath('//additional_answer')), 0) + self.assertNotIn('additional_answer', problem.get_html()) + def test_non_accessible_inputtype(self): """ Verify that tag with question text is not removed when inputtype is not fully accessible. diff --git a/openedx/core/djangoapps/appsembler/api/tests/test_user_api.py b/openedx/core/djangoapps/appsembler/api/tests/test_user_api.py index cf50d520608c..e23d05c7c160 100644 --- a/openedx/core/djangoapps/appsembler/api/tests/test_user_api.py +++ b/openedx/core/djangoapps/appsembler/api/tests/test_user_api.py @@ -32,6 +32,11 @@ @mock.patch(APPSEMBLER_API_VIEWS_MODULE + '.UserIndexViewSet.throttle_classes', []) class UserIndexViewSetTest(TestCase): + # Fixtures to be used for filtering + JANE_DUE_USERNAME = 'jane.due' + JANE_DUE_EMAIL = '{username}@user.api.example.com'.format(username=JANE_DUE_USERNAME) + NON_USER_EMAIL = 'not.for.a.user@user.api.example.com' + def setUp(self): """ Set up test data for site isolation @@ -51,7 +56,12 @@ def setUp(self): self.my_site_org = OrganizationFactory(sites=[self.my_site]) # Set up users and enrollments for 'my site' - self.my_site_users = [UserFactory() for i in range(3)] + self.my_site_users = [ + UserFactory.create(email=self.JANE_DUE_EMAIL, username=self.JANE_DUE_USERNAME), + UserFactory.create(), + UserFactory.create(), + ] + for user in self.my_site_users: UserOrganizationMappingFactory(user=user, organization=self.my_site_org) @@ -90,6 +100,32 @@ def test_get_all_users_for_site(self): user_ids = [rec['id'] for rec in results] assert set(user_ids) == set([obj.id for obj in expected_users]) + @ddt.unpack + @ddt.data( + {'email': JANE_DUE_EMAIL.lower(), 'expected_count': 1, 'msg': 'Should find Jane (lower case) in the users'}, + {'email': JANE_DUE_EMAIL.upper(), 'expected_count': 1, 'msg': 'Should find Jane (upper case) in the users'}, + {'email': JANE_DUE_USERNAME, 'expected_count': 0, 'msg': 'Should not do partial matching'}, + {'email': NON_USER_EMAIL, 'expected_count': 0, 'msg': 'Should not match any user.'}, + ) + def test_filter_by_email(self, email, expected_count, msg): + """ + Test the email filters matching. + """ + url = reverse('tahoe-api:v1:users-list') + request = APIRequestFactory().get(url, {'email_exact': email}) + request.META['HTTP_HOST'] = self.my_site.domain + force_authenticate(request, user=self.caller) + + view = resolve(url).func + response = view(request) + response.render() + results = response.data['results'] + + assert len(results) == expected_count, msg + if expected_count: + # Ignore the email case + assert results[0]['email'].lower() == email.lower(), msg + @skip("Need to implement user filter") def test_get_all_enrolled_learners_for_site(self): diff --git a/openedx/core/djangoapps/appsembler/api/v1/filters.py b/openedx/core/djangoapps/appsembler/api/v1/filters.py index e68621905d9d..904d62e86fa5 100644 --- a/openedx/core/djangoapps/appsembler/api/v1/filters.py +++ b/openedx/core/djangoapps/appsembler/api/v1/filters.py @@ -1,5 +1,6 @@ import django_filters +from django.contrib.auth import get_user_model from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content.course_overviews.models import ( CourseOverview, @@ -68,3 +69,15 @@ def filter_course_id(self, queryset, name, value): class Meta: model = CourseEnrollment fields = ['course_id', 'user_id', 'username', 'is_active', ] + + +class UserIndexFilter(django_filters.FilterSet): + '''Provides filtering for the User model objects in the UserIndexViewSet. + + ''' + + email_exact = django_filters.CharFilter(name='email', lookup_expr='iexact') + + class Meta: + model = get_user_model() + fields = ['email_exact'] diff --git a/openedx/core/djangoapps/appsembler/api/v1/views.py b/openedx/core/djangoapps/appsembler/api/v1/views.py index a74f29c6f17c..e399de1109d6 100644 --- a/openedx/core/djangoapps/appsembler/api/v1/views.py +++ b/openedx/core/djangoapps/appsembler/api/v1/views.py @@ -50,7 +50,8 @@ from openedx.core.djangoapps.appsembler.api.v1.api import enroll_learners_in_course from openedx.core.djangoapps.appsembler.api.v1.filters import ( CourseEnrollmentFilter, - CourseOverviewFilter + CourseOverviewFilter, + UserIndexFilter, ) from openedx.core.djangoapps.appsembler.api.v1.pagination import ( TahoeLimitOffsetPagination @@ -397,6 +398,7 @@ class UserIndexViewSet(TahoeAuthMixin, viewsets.ReadOnlyModelViewSet): serializer_class = UserIndexSerializer throttle_classes = (TahoeAPIUserThrottle,) filter_backends = (DjangoFilterBackend, ) + filter_class = UserIndexFilter def get_queryset(self): site = django.contrib.sites.shortcuts.get_current_site(self.request) diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py index 90b9fa08a61c..26592fa32667 100644 --- a/openedx/core/djangoapps/signals/signals.py +++ b/openedx/core/djangoapps/signals/signals.py @@ -21,5 +21,7 @@ ] ) -# Signal that indicates that a user has become verified +# Signal that indicates that a user has become verified for certificate purposes LEARNER_NOW_VERIFIED = Signal(providing_args=['user']) + +USER_ACCOUNT_ACTIVATED = Signal(providing_args=["user"]) # Signal indicating email verification diff --git a/tox.ini b/tox.ini index d82678c70f02..79de0948c9e5 100644 --- a/tox.ini +++ b/tox.ini @@ -68,6 +68,7 @@ commands = bash scripts/upgrade_pysqlite.sh {env:TRAVIS_FIXES} pytest \ + common/djangoapps/student/tests/test_activate_account.py \ common/djangoapps/util/tests/test_milestones_helpers.py \ common/lib/xmodule/xmodule/modulestore/tests/test_split_mongo_mongo_connection.py