From 20b82af5d834dab4043180f2258f7e8f04a5fb25 Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Wed, 5 Feb 2020 18:38:12 +0300 Subject: [PATCH 1/4] Users API: Filter by email --- .../appsembler/api/tests/test_user_api.py | 38 ++++++++++++++++++- .../djangoapps/appsembler/api/v1/filters.py | 13 +++++++ .../djangoapps/appsembler/api/v1/views.py | 4 +- 3 files changed, 53 insertions(+), 2 deletions(-) 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) From 37420a25eedaab7184d6b041c0dd35e6b628d932 Mon Sep 17 00:00:00 2001 From: Awais Jibran Date: Tue, 11 Feb 2020 17:27:31 +0200 Subject: [PATCH 2/4] [PATCH] Fix CAPA Problems --- common/lib/capa/capa/capa_problem.py | 1 + common/lib/capa/capa/tests/test_capa_problem.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) 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. From d5a6d81f6bf8dbb0a896d972831a081d30a15e12 Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Fri, 24 Jan 2020 12:37:29 +0300 Subject: [PATCH 3/4] Add USER_ACCOUNT_ACTIVATED signal This helps plugins to listen to this signal and perform custom logic without the need to hardcode it inside the platform. --- common/djangoapps/student/models.py | 2 ++ .../student/tests/test_activate_account.py | 13 +++++++++++++ openedx/core/djangoapps/signals/signals.py | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) 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/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 From 17f748d5b005bfb7bbaeab1dcf7ecb52f23a8d5c Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Sun, 16 Feb 2020 12:36:44 +0200 Subject: [PATCH 4/4] Add account activation tests to tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) 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