def post(self, request): """ Audit enrolls the user in a course in edx """ course_id = request.data.get('course_id') if course_id is None: raise ValidationError('course id missing in the request') # get the credentials for the current user for edX user_social = get_social_auth(request.user) try: utils.refresh_user_token(user_social) except utils.InvalidCredentialStored as exc: log.error( "Error while refreshing credentials for user %s", get_social_username(request.user), ) return Response( status=exc.http_status_code, data={'error': str(exc)} ) # create an instance of the client to query edX edx_client = EdxApi(user_social.extra_data, settings.EDXORG_BASE_URL) try: enrollment = edx_client.enrollments.create_audit_student_enrollment(course_id) except HTTPError as exc: if exc.response.status_code == status.HTTP_400_BAD_REQUEST: raise PossiblyImproperlyConfigured( 'Got a 400 status code from edX server while trying to create ' 'audit enrollment. This might happen if the course is improperly ' 'configured on MicroMasters. Course key ' '{course_key}, edX user "{edX_user}"'.format( edX_user=get_social_username(request.user), course_key=course_id, ) ) log.error( "Http error from edX while creating audit enrollment for course key %s for edX user %s", course_id, get_social_username(request.user), ) return Response( status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={'error': str(exc)} ) except Exception as exc: # pylint: disable=broad-except log.exception( "Error creating audit enrollment for course key %s for edX user %s", course_id, get_social_username(request.user), ) return Response( status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={'error': str(exc)} ) CachedEdxDataApi.update_cached_enrollment(request.user, enrollment, enrollment.course_id, index_user=True) return Response( data=enrollment.json )
def post(self, request): """ Audit enrolls the user in a course in edx """ course_id = request.data.get('course_id') if course_id is None: raise ValidationError('course id missing in the request') # get the credentials for the current user for edX user_social = get_social_auth(request.user) try: utils.refresh_user_token(user_social) except utils.InvalidCredentialStored as exc: log.error( "Error while refreshing credentials for user %s", get_social_username(request.user), ) return Response( status=exc.http_status_code, data={'error': str(exc)} ) # create an instance of the client to query edX edx_client = EdxApi(user_social.extra_data, settings.EDXORG_BASE_URL) try: enrollment = edx_client.enrollments.create_audit_student_enrollment(course_id) except HTTPError as exc: if exc.response.status_code == status.HTTP_400_BAD_REQUEST: raise PossiblyImproperlyConfigured( 'Got a 400 status code from edX server while trying to create ' 'audit enrollment. This might happen if the course is improperly ' 'configured on MicroMasters. Course key ' '{course_key}, edX user "{edX_user}"'.format( edX_user=get_social_username(request.user), course_key=course_id, ) ) log.error( "Http error from edX while creating audit enrollment for course key %s for edX user %s", course_id, get_social_username(request.user), ) return Response( status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={'error': str(exc)} ) except Exception as exc: # pylint: disable=broad-except log.exception( "Error creating audit enrollment for course key %s for edX user %s", course_id, get_social_username(request.user), ) return Response( status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={'error': str(exc)} ) CachedEdxDataApi.update_cached_enrollment(request.user, enrollment, enrollment.course_id, index_user=True) return Response( data=enrollment.json )
def test_check_edx_verified_email(self, is_active, mocked_get_json): """ User should be directed to error page if user is unverified on edX """ mock_strategy = mock.Mock() backend = edxorg.EdxOrgOAuth2(strategy=mock_strategy) mocked_content = dict(self.mocked_edx_profile) mocked_content['is_active'] = is_active mocked_get_json.return_value = mocked_content result = pipeline_api.check_edx_verified_email( backend, {'access_token': 'foo_token'}, {'username': get_social_username(self.user)} ) mocked_get_json.assert_called_once_with( urljoin( edxorg.EdxOrgOAuth2.EDXORG_BASE_URL, '/api/user/v1/accounts/{0}'.format(get_social_username(self.user)) ), headers={'Authorization': 'Bearer foo_token'} ) if is_active: assert 'edx_profile' in result else: self.assertEqual(result.status_code, 302)
def get_context(self, request, *args, **kwargs): programs = Program.objects.filter(live=True).select_related('programpage').order_by("id") js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "host": webpack_dev_server_host(request), "environment": settings.ENVIRONMENT, "sentry_dsn": sentry.get_public_dsn(), "release_version": settings.VERSION } username = get_social_username(request.user) context = super(HomePage, self).get_context(request) def get_program_page(program): """Return a None if ProgramPage does not exist, to avoid template errors""" try: return program.programpage except ProgramPage.DoesNotExist: return None program_pairs = [(program, get_program_page(program)) for program in programs] context["programs"] = program_pairs context["is_public"] = True context["has_zendesk_widget"] = True context["google_maps_api"] = False context["authenticated"] = not request.user.is_anonymous context["is_staff"] = has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]) context["username"] = username context["js_settings_json"] = json.dumps(js_settings) context["title"] = self.title context["ga_tracking_id"] = "" context["coupon_code"] = get_coupon_code(request) return context
def get_program_page_context(programpage, request): """ Get context for the program page""" from cms.serializers import ProgramPageSerializer courses_query = ( programpage.program.course_set.all() ) js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "host": webpack_dev_server_host(request), "environment": settings.ENVIRONMENT, "sentry_dsn": sentry.get_public_dsn(), "release_version": settings.VERSION, "user": serialize_maybe_user(request.user), "program": ProgramPageSerializer(programpage).data, } username = get_social_username(request.user) context = super(ProgramPage, programpage).get_context(request) context["is_staff"] = has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]) context["is_public"] = True context["has_zendesk_widget"] = True context["google_maps_api"] = False context["authenticated"] = not request.user.is_anonymous context["username"] = username context["js_settings_json"] = json.dumps(js_settings) context["title"] = programpage.title context["courses"] = courses_query context["ga_tracking_id"] = programpage.program.ga_tracking_id return context
def get_username(self, obj): """ Look up the user's username on edX. We do *not* use the `user.username` field, because the Javascript doesn't need to know anything about that. """ return get_social_username(obj)
def test_limited(self): """ Test limited serializer """ profile = self.create_profile() data = ProfileLimitedSerializer(profile).data assert data == { 'username': get_social_username(profile.user), 'first_name': profile.first_name, 'last_name': profile.last_name, 'full_name': profile.full_name, 'preferred_name': profile.preferred_name, 'gender': profile.gender, 'account_privacy': profile.account_privacy, 'country': profile.country, 'state_or_territory': profile.state_or_territory, 'city': profile.city, 'birth_country': profile.birth_country, 'preferred_language': profile.preferred_language, 'edx_level_of_education': profile.edx_level_of_education, 'education': EducationSerializer(profile.education.all(), many=True).data, 'work_history': ( EmploymentSerializer(profile.work_history.all(), many=True).data ), 'image_medium': profile.image_medium.url, 'about_me': profile.about_me, 'romanized_first_name': profile.romanized_first_name, 'romanized_last_name': profile.romanized_last_name, }
def get_program_page_context(programpage, request): """ Get context for the program page""" from cms.serializers import ProgramPageSerializer courses_query = (programpage.program.course_set.all()) js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "host": webpack_dev_server_host(request), "environment": settings.ENVIRONMENT, "sentry_dsn": settings.SENTRY_DSN, "release_version": settings.VERSION, "user": serialize_maybe_user(request.user), "program": ProgramPageSerializer(programpage).data, } username = get_social_username(request.user) context = super(ProgramPage, programpage).get_context(request) context["is_staff"] = has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]) context["is_public"] = True context["has_zendesk_widget"] = True context["google_maps_api"] = False context["authenticated"] = not request.user.is_anonymous context["username"] = username context["js_settings_json"] = json.dumps(js_settings) context["title"] = programpage.title context["courses"] = courses_query context["ga_tracking_id"] = programpage.program.ga_tracking_id return context
def generate_cybersource_sa_payload(order): """ Generates a payload dict to send to CyberSource for Secure Acceptance Args: order (Order): An order Returns: dict: the payload to send to CyberSource via Secure Acceptance """ # http://apps.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_WM/Secure_Acceptance_WM.pdf # Section: API Fields payload = { 'access_key': settings.CYBERSOURCE_ACCESS_KEY, 'amount': str(order.total_price_paid), 'consumer_id': get_social_username(order.user), 'currency': 'USD', 'locale': 'en-us', # TODO 'override_custom_cancel_page': 'https://micromasters.mit.edu?cancel', 'override_custom_receipt_page': "https://micromasters.mit.edu?receipt", 'reference_number': make_reference_id(order), 'profile_id': settings.CYBERSOURCE_PROFILE_ID, 'signed_date_time': datetime.utcnow().strftime(ISO_8601_FORMAT), 'transaction_type': 'sale', 'transaction_uuid': uuid.uuid4().hex, 'unsigned_field_names': '', } field_names = sorted(list(payload.keys()) + ['signed_field_names']) payload['signed_field_names'] = ','.join(field_names) payload['signature'] = generate_cybersource_sa_signature(payload) return payload
def test_index_context_logged_in_social_auth(self): """ Assert context values when logged in as social auth user """ profile = self.create_and_login_user() user = profile.user ga_tracking_id = FuzzyText().fuzz() with self.settings( GA_TRACKING_ID=ga_tracking_id, ), patch('ui.templatetags.render_bundle._get_bundle') as get_bundle: response = self.client.get('/') bundles = [bundle[0][1] for bundle in get_bundle.call_args_list] assert set(bundles) == { 'common', 'public', 'sentry_client', 'style', 'style_public', 'zendesk_widget', } assert response.context['authenticated'] is True assert response.context['username'] == get_social_username(user) assert response.context['title'] == HomePage.objects.first().title assert response.context['is_public'] is True assert response.context['has_zendesk_widget'] is True assert response.context['is_staff'] is False assert response.context['programs'] == [] self.assertContains(response, 'Share this page') js_settings = json.loads(response.context['js_settings_json']) assert js_settings['gaTrackingID'] == ga_tracking_id
def standard_error_page(request, status_code, template_filename): """ Returns an error page with a given template filename and provides necessary context variables """ name = request.user.profile.preferred_name if not request.user.is_anonymous else "" authenticated = not request.user.is_anonymous username = get_social_username(request.user) response = render( request, template_filename, context={ "has_zendesk_widget": True, "is_public": True, "js_settings_json": json.dumps({ "release_version": settings.VERSION, "environment": settings.ENVIRONMENT, "sentry_dsn": settings.SENTRY_DSN, "user": serialize_maybe_user(request.user), }), "authenticated": authenticated, "name": name, "username": username, "is_staff": has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]), "support_email": settings.EMAIL_SUPPORT, "sentry_dsn": settings.SENTRY_DSN, } ) response.status_code = status_code return response
def standard_error_page(request, status_code, template_filename): """ Returns an error page with a given template filename and provides necessary context variables """ name = request.user.profile.preferred_name if not request.user.is_anonymous else "" authenticated = not request.user.is_anonymous username = get_social_username(request.user) response = render( request, template_filename, context={ "has_zendesk_widget": True, "is_public": True, "js_settings_json": json.dumps({ "release_version": settings.VERSION, "environment": settings.ENVIRONMENT, "sentry_dsn": sentry.get_public_dsn(), "user": serialize_maybe_user(request.user), }), "authenticated": authenticated, "name": name, "username": username, "is_staff": has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]), "support_email": settings.EMAIL_SUPPORT, "sentry_dsn": sentry.get_public_dsn(), } ) response.status_code = status_code return response
def test_dashboard_settings(self): """ Assert settings we pass to dashboard """ profile = self.create_and_login_user() user = profile.user ga_tracking_id = FuzzyText().fuzz() react_ga_debug = FuzzyText().fuzz() edx_base_url = FuzzyText().fuzz() host = FuzzyText().fuzz() with self.settings( GA_TRACKING_ID=ga_tracking_id, REACT_GA_DEBUG=react_ga_debug, EDXORG_BASE_URL=edx_base_url, WEBPACK_DEV_SERVER_HOST=host, ): resp = self.client.get(DASHBOARD_URL) js_settings = json.loads(resp.context['js_settings_json']) assert js_settings == { 'gaTrackingID': ga_tracking_id, 'reactGaDebug': react_ga_debug, 'authenticated': True, 'name': user.profile.preferred_name, 'username': get_social_username(user), 'host': host, 'edx_base_url': edx_base_url, 'roles': [], 'search_url': reverse('search_api', kwargs={"elastic_url": ""}), }
def get_username(self, obj): """ Look up the user's username on edX. We do *not* use the `user.username` field, because the Javascript doesn't need to know anything about that. """ return get_social_username(obj)
def test_index_context_logged_in_social_auth(self): """ Assert context values when logged in as social auth user """ profile = self.create_and_login_user() user = profile.user ga_tracking_id = FuzzyText().fuzz() with self.settings(GA_TRACKING_ID=ga_tracking_id, ), patch( 'ui.templatetags.render_bundle._get_bundle') as get_bundle: response = self.client.get('/') bundles = [bundle[0][1] for bundle in get_bundle.call_args_list] assert set(bundles) == { 'common', 'public', 'sentry_client', 'style', 'style_public', 'zendesk_widget', } assert response.context['authenticated'] is True assert response.context['username'] == get_social_username(user) assert response.context['title'] == HomePage.objects.first().title assert response.context['is_public'] is True assert response.context['has_zendesk_widget'] is True assert response.context['is_staff'] is False assert response.context['programs'] == [] self.assertContains(response, 'Share this page') js_settings = json.loads(response.context['js_settings_json']) assert js_settings['gaTrackingID'] == ga_tracking_id
def get_context(self, request, *args, **kwargs): js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "host": webpack_dev_server_host(request), "environment": settings.ENVIRONMENT, "sentry_dsn": settings.SENTRY_DSN, "release_version": settings.VERSION } username = get_social_username(request.user) context = super().get_context(request) context["is_public"] = True context["has_zendesk_widget"] = True context["google_maps_api"] = False context["authenticated"] = not request.user.is_anonymous context["is_staff"] = has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]) context["username"] = username context["js_settings_json"] = json.dumps(js_settings) context["title"] = self.title context["ga_tracking_id"] = "" context["hubspot_portal_id"] = settings.HUBSPOT_CONFIG.get( "HUBSPOT_PORTAL_ID") context[ "hubspot_ogranizations_form_guid"] = settings.HUBSPOT_CONFIG.get( "HUBSPOT_ORGANIZATIONS_FORM_GUID") return context
def test_no_coupon(self): """ A 404 should be returned if no coupon exists """ resp = self.client.post(reverse('coupon-user-create', kwargs={'code': "missing"}), data={ "username": get_social_username(self.user) }, format='json') assert resp.status_code == status.HTTP_404_NOT_FOUND
def has_permission(self, request, view): """ Returns true if the username in the request body matches the logged in user. """ try: return request.data['username'] == get_social_username(request.user) except KeyError: return False
def test_anonymous_user(self): # pylint: disable=no-self-use """ get_social_username should return None for anonymous users """ is_anonymous = Mock(return_value=True) user = Mock(is_anonymous=is_anonymous) assert get_social_username(user) is None assert is_anonymous.called
def has_permission(self, request, view): """ Returns true if the username in the request body matches the logged in user. """ try: return request.data['username'] == get_social_username( request.user) except KeyError: return False
def test_no_coupon(self): """ A 404 should be returned if no coupon exists """ resp = self.client.post( reverse('coupon-user-create', kwargs={'code': "missing"}), data={"username": get_social_username(self.user)}, format='json') assert resp.status_code == status.HTTP_404_NOT_FOUND
def generate_cybersource_sa_payload(order, dashboard_url): """ Generates a payload dict to send to CyberSource for Secure Acceptance Args: order (Order): An order dashboard_url: (str): The absolute url for the dashboard Returns: dict: the payload to send to CyberSource via Secure Acceptance """ # http://apps.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_WM/Secure_Acceptance_WM.pdf # Section: API Fields # Course key is used only to show the confirmation message to the user course_key = "" line = order.line_set.first() if line is not None: course_key = line.course_key course_run = CourseRun.objects.get(edx_course_key=course_key) # NOTE: be careful about max length here, many (all?) string fields have a max # length of 255. At the moment none of these fields should go over that, due to database # constraints or other reasons payload = { 'access_key': settings.CYBERSOURCE_ACCESS_KEY, 'amount': str(order.total_price_paid), 'consumer_id': get_social_username(order.user), 'currency': 'USD', 'locale': 'en-us', 'item_0_code': 'course', 'item_0_name': '{}'.format(course_run.title), 'item_0_quantity': 1, 'item_0_sku': '{}'.format(course_key), 'item_0_tax_amount': '0', 'item_0_unit_price': str(order.total_price_paid), 'line_item_count': 1, 'override_custom_cancel_page': make_dashboard_receipt_url(dashboard_url, course_key, 'cancel'), 'override_custom_receipt_page': make_dashboard_receipt_url(dashboard_url, course_key, 'receipt'), 'reference_number': make_reference_id(order), 'profile_id': settings.CYBERSOURCE_PROFILE_ID, 'signed_date_time': now_in_utc().strftime(ISO_8601_FORMAT), 'transaction_type': 'sale', 'transaction_uuid': uuid.uuid4().hex, 'unsigned_field_names': '', 'merchant_defined_data1': 'course', 'merchant_defined_data2': '{}'.format(course_run.title), 'merchant_defined_data3': '{}'.format(course_key), } field_names = sorted(list(payload.keys()) + ['signed_field_names']) payload['signed_field_names'] = ','.join(field_names) payload['signature'] = generate_cybersource_sa_signature(payload) return payload
def deserialize_dashboard_data(user, user_data, programs): """ Deserializes enrollment/grade data for a user """ fake_course_runs = CourseRun.objects.filter( course__program__in=programs ).select_related('course__program').all() social_username = get_social_username(user) enrollment_list = user_data.get('_enrollments', []) grade_list = user_data.get('_grades', []) deserialize_enrollment_data(user, social_username, fake_course_runs, enrollment_list) deserialize_grade_data(user, fake_course_runs, grade_list)
def deserialize_dashboard_data(user, user_data, programs): """ Deserializes enrollment/grade data for a user """ fake_course_runs = CourseRun.objects.filter( course__program__in=programs).select_related('course__program').all() social_username = get_social_username(user) enrollment_list = user_data.get('_enrollments', []) grade_list = user_data.get('_grades', []) deserialize_enrollment_data(user, social_username, fake_course_runs, enrollment_list) deserialize_grade_data(user, fake_course_runs, grade_list)
def test_two_social(self): """ get_social_username should return None if there are two social edX accounts for a user """ UserSocialAuthFactory.create(user=self.user, uid='other name') with LogCapture() as log_capture: assert get_social_username(self.user) is None log_capture.check(( 'profiles.api', 'ERROR', 'Unexpected error retrieving social auth username: get() returned more than ' 'one UserSocialAuth -- it returned 2!'))
def get_purchasable_course_run(course_key, user): """ Gets a course run, or raises Http404 if not purchasable. To be purchasable a course run must not already be purchased, must be part of a live program, must be part of a program with financial aid enabled, with a financial aid object, and must have a valid price. Args: course_key (str): An edX course key user (User): The purchaser of the course run Returns: CourseRun: A course run """ # Make sure it's connected to a live program, it has a valid price, and the user is enrolled in the program already try: course_run = get_object_or_404( CourseRun, edx_course_key=course_key, course__program__live=True, course__program__financial_aid_availability=True, ) except Http404: log.warning("Course run %s is not purchasable", course_key) raise if not FinancialAid.objects.filter( tier_program__current=True, tier_program__program__course__courserun=course_run, user=user, status__in=FinancialAidStatus.TERMINAL_STATUSES, ).exists(): log.warning("Course run %s has no attached financial aid for user %s", course_key, get_social_username(user)) raise ValidationError( "Course run {} does not have a current attached financial aid application" .format(course_key)) # Make sure it's not already purchased if Line.objects.filter( order__status__in=Order.FULFILLED_STATUSES, order__user=user, course_key=course_run.edx_course_key, ).exists(): mmtrack = get_mmtrack(user, course_run.course.program) if not has_to_pay_for_exam(mmtrack, course_run.course): log.warning("Course run %s is already purchased by user %s", course_key, user) raise ValidationError( "Course run {} is already purchased".format(course_key)) return course_run
def setUp(self): """ Set up class """ super(EdxPipelineApiTest, self).setUp() self.user = UserFactory(username="******") self.user.social_auth.create( provider='not_edx', ) self.user.social_auth.create( provider=edxorg.EdxOrgOAuth2.name, uid="{}_edx".format(self.user.username), ) self.user_profile = Profile.objects.get(user=self.user) self.mocked_edx_profile = { 'account_privacy': 'all_users', 'bio': 'this is my personal profile text', 'country': 'IT', 'date_joined': '2016-03-17T20:37:51Z', 'email': '*****@*****.**', 'gender': 'f', 'goals': None, 'is_active': True, 'language_proficiencies': [{'code': 'it'}], 'level_of_education': 'p', 'mailing_address': None, 'name': 'dummy user', 'profile_image': { 'has_image': True, 'image_url_full': 'https://edx.org/full.jpg', 'image_url_large': 'https://edx.org/large.jpg', 'image_url_medium': 'https://edx.org/medium.jpg', 'image_url_small': 'https://edx.org/small.jpg' }, 'requires_parental_consent': False, 'username': get_social_username(self.user), 'year_of_birth': 1986, "work_history": [ { "id": 1, "city": "NY", "state_or_territory": "NY", "country": "USA", "company_name": "XYZ-ABC", "position": "SSE", "industry": "IT", "end_date": "2016-05-17T17:14:00Z", "start_date": "2016-05-28T17:14:06Z" } ] }
def setUp(self): """ Set up class """ super(EdxPipelineApiTest, self).setUp() self.user = UserFactory() self.user.social_auth.create( provider='not_edx', ) self.user.social_auth.create( provider=edxorg.EdxOrgOAuth2.name, uid="{}_edx".format(self.user.username), ) self.user_profile = Profile.objects.get(user=self.user) self.mocked_edx_profile = { 'account_privacy': 'all_users', 'bio': 'this is my personal profile text', 'country': 'IT', 'date_joined': '2016-03-17T20:37:51Z', 'email': '*****@*****.**', 'gender': 'f', 'goals': None, 'is_active': True, 'language_proficiencies': [{'code': 'it'}], 'level_of_education': 'p', 'mailing_address': None, 'name': 'dummy user', 'profile_image': { 'has_image': True, 'image_url_full': 'https://edx.org/full.jpg', 'image_url_large': 'https://edx.org/large.jpg', 'image_url_medium': 'https://edx.org/medium.jpg', 'image_url_small': 'https://edx.org/small.jpg' }, 'requires_parental_consent': False, 'username': get_social_username(self.user), 'year_of_birth': 1986, "work_history": [ { "id": 1, "city": "NY", "state_or_territory": "NY", "country": "USA", "company_name": "XYZ-ABC", "position": "SSE", "industry": "IT", "end_date": "2016-05-17T17:14:00Z", "start_date": "2016-05-28T17:14:06Z" } ] }
def _fill_in_missing_data(cls, data, user, course_run): data['user'] = get_social_username(user) data['course_details'].update({ 'course_start': course_run.start_date.isoformat(), 'course_end': course_run.end_date.isoformat(), 'enrollment_start': course_run.enrollment_start.isoformat(), 'enrollment_end': course_run.enrollment_end.isoformat() }) return data
def test_index_context_logged_in_social_auth(self): """ Assert context values when logged in as social auth user """ profile = self.create_and_login_user() user = profile.user ga_tracking_id = FuzzyText().fuzz() with self.settings(GA_TRACKING_ID=ga_tracking_id, ): response = self.client.get('/') assert response.context['authenticated'] is True assert response.context['username'] == get_social_username(user) assert response.context['title'] == HomePage.objects.first().title js_settings = json.loads(response.context['js_settings_json']) assert js_settings['gaTrackingID'] == ga_tracking_id
def test_check_edx_verified_email(self, is_active, mocked_get_json): """ User should be directed to error page if user is unverified on edX """ mock_strategy = mock.Mock() backend = edxorg.EdxOrgOAuth2(strategy=mock_strategy) mocked_content = dict(self.mocked_edx_profile) mocked_content['is_active'] = is_active mocked_get_json.return_value = mocked_content result = pipeline_api.check_edx_verified_email( backend, {'access_token': 'foo_token'}, {'username': get_social_username(self.user)}) mocked_get_json.assert_called_once_with( urljoin( edxorg.EdxOrgOAuth2.EDXORG_BASE_URL, '/api/user/v1/accounts/{0}'.format( get_social_username(self.user))), headers={'Authorization': 'Bearer foo_token'}) if is_active: assert 'edx_profile' in result else: self.assertEqual(result.status_code, 302)
def test_dashboard_states(browser, override_allowed_hosts, seeded_database_loader, django_db_blocker, test_data): """Iterate through all possible dashboard states and save screenshots/API results of each one""" output_directory = DASHBOARD_STATES_OPTIONS.get('output_directory') use_learner_page = DASHBOARD_STATES_OPTIONS.get('learner') os.makedirs(output_directory, exist_ok=True) use_mobile = DASHBOARD_STATES_OPTIONS.get('mobile') if use_mobile: browser.driver.set_window_size(480, 854) dashboard_states = DashboardStates(test_data['user']) dashboard_state_iter = enumerate(dashboard_states) match = DASHBOARD_STATES_OPTIONS.get('match') if match is not None: dashboard_state_iter = filter( lambda scenario: match in make_filename(scenario[0], scenario[1][1]), dashboard_state_iter ) LoginPage(browser).log_in_via_admin(dashboard_states.user, DEFAULT_PASSWORD) for num, (run_scenario, name) in dashboard_state_iter: skip_screenshot = False with django_db_blocker.unblock(): dashboard_states.user.refresh_from_db() if use_learner_page: for program in Program.objects.all(): Role.objects.create(role=Staff.ROLE_ID, user=dashboard_states.user, program=program) filename = make_filename(num, name, output_directory=output_directory, use_mobile=use_mobile) new_url = run_scenario() if new_url is None: if use_learner_page: new_url = '/learner' else: new_url = '/dashboard' elif use_learner_page: # the new_url is only for the dashboard page, skip skip_screenshot = True if not skip_screenshot: browser.get(new_url) browser.store_api_results( get_social_username(dashboard_states.user), filename=filename ) if use_learner_page: browser.wait_until_loaded(By.CLASS_NAME, 'user-page') else: browser.wait_until_loaded(By.CLASS_NAME, 'course-list') browser.take_screenshot(filename=filename) with django_db_blocker.unblock(): terminate_db_connections() seeded_database_loader.load_backup()
def test_limited(self): # pylint: disable=no-self-use """ Test limited serializer """ profile = self.create_profile() assert ProfileLimitedSerializer().to_representation(profile) == { 'username': get_social_username(profile.user), 'first_name': profile.first_name, 'last_name': profile.last_name, 'preferred_name': profile.preferred_name, 'gender': profile.gender, 'account_privacy': profile.account_privacy, 'has_profile_image': profile.has_profile_image, 'profile_url_full': format_gravatar_url(profile.user.email, GravatarImgSize.FULL), 'profile_url_large': format_gravatar_url(profile.user.email, GravatarImgSize.LARGE), 'profile_url_medium': format_gravatar_url(profile.user.email, GravatarImgSize.MEDIUM), 'profile_url_small': format_gravatar_url(profile.user.email, GravatarImgSize.SMALL), 'country': profile.country, 'state_or_territory': profile.state_or_territory, 'city': profile.city, 'birth_country': profile.birth_country, 'preferred_language': profile.preferred_language, 'edx_level_of_education': profile.edx_level_of_education, 'education': [ EducationSerializer().to_representation(education) for education in profile.education.all() ], 'work_history': [ EmploymentSerializer().to_representation(work_history) for work_history in profile.work_history.all() ] }
def test_coupon_not_redeemable(self): """ A 404 should be returned if coupon is not redeemable """ with patch('ecommerce.views.is_coupon_redeemable', autospec=True) as _is_redeemable_mock: _is_redeemable_mock.return_value = False resp = self.client.post( reverse('coupon-user-create', kwargs={'code': self.coupon.coupon_code}), data={"username": get_social_username(self.user)}, format='json', ) assert resp.status_code == status.HTTP_404_NOT_FOUND _is_redeemable_mock.assert_called_with(self.coupon, self.user)
def test_two_social(self): """ get_social_username should return None if there are two social edX accounts for a user """ UserSocialAuthFactory.create(user=self.user, uid='other name') with LogCapture() as log_capture: assert get_social_username(self.user) is None log_capture.check( ( 'profiles.api', 'ERROR', 'Unexpected error retrieving social auth username: get() returned more than ' 'one UserSocialAuth -- it returned 2!' ) )
def test_users_logged_in(self): """ Assert settings we pass to dashboard """ profile = self.create_and_login_user() user = profile.user username = get_social_username(user) ga_tracking_id = FuzzyText().fuzz() react_ga_debug = FuzzyText().fuzz() edx_base_url = FuzzyText().fuzz() host = FuzzyText().fuzz() with self.settings( GA_TRACKING_ID=ga_tracking_id, REACT_GA_DEBUG=react_ga_debug, EDXORG_BASE_URL=edx_base_url, WEBPACK_DEV_SERVER_HOST=host, ): # Mock has_permission so we don't worry about testing permissions here has_permission = Mock(return_value=True) with patch( 'profiles.permissions.CanSeeIfNotPrivate.has_permission', has_permission): resp = self.client.get( reverse('ui-users', kwargs={'user': username})) assert resp.status_code == 200 js_settings = json.loads(resp.context['js_settings_json']) assert js_settings == { 'gaTrackingID': ga_tracking_id, 'reactGaDebug': react_ga_debug, 'authenticated': True, 'name': user.profile.preferred_name, 'username': username, 'host': host, 'edx_base_url': edx_base_url, 'roles': [], 'search_url': reverse('search_api', kwargs={"elastic_url": ""}), } assert has_permission.called
def test_limited(self): """ Test limited serializer """ profile = self.create_profile() data = ProfileLimitedSerializer(profile).data assert data == { 'username': get_social_username(profile.user), 'first_name': profile.first_name, 'last_name': profile.last_name, 'full_name': profile.full_name, 'preferred_name': profile.preferred_name, 'gender': profile.gender, 'account_privacy': profile.account_privacy, 'country': profile.country, 'state_or_territory': profile.state_or_territory, 'city': profile.city, 'birth_country': profile.birth_country, 'preferred_language': profile.preferred_language, 'edx_level_of_education': profile.edx_level_of_education, 'education': EducationSerializer(profile.education.all(), many=True).data, 'work_history': (EmploymentSerializer(profile.work_history.all(), many=True).data), 'image_medium': profile.image_medium.url, 'about_me': profile.about_me, 'romanized_first_name': profile.romanized_first_name, 'romanized_last_name': profile.romanized_last_name, }
def test_coupon_not_redeemable(self): """ A 404 should be returned if coupon is not redeemable """ with patch( 'ecommerce.views.is_coupon_redeemable', autospec=True ) as _is_redeemable_mock: _is_redeemable_mock.return_value = False resp = self.client.post( reverse('coupon-user-create', kwargs={'code': self.coupon.coupon_code}), data={ "username": get_social_username(self.user) }, format='json', ) assert resp.status_code == status.HTTP_404_NOT_FOUND _is_redeemable_mock.assert_called_with(self.coupon, self.user)
def test_create_user_coupon(self, already_exists): """ Test happy case for creating a UserCoupon """ previous_modified = self.coupon.user_coupon_qset( self.user).first().updated_on if not already_exists: # Won't change anything if it already exists UserCoupon.objects.all().delete() data = { 'username': get_social_username(self.user), } with patch('ecommerce.views.is_coupon_redeemable', autospec=True) as _is_redeemable_mock: _is_redeemable_mock.return_value = True resp = self.client.post( reverse('coupon-user-create', kwargs={'code': self.coupon.coupon_code}), data=data, format='json', ) _is_redeemable_mock.assert_called_with(self.coupon, self.user) assert resp.status_code == status.HTTP_200_OK assert UserCoupon.objects.count() == 1 user_coupon = UserCoupon.objects.get(user=self.user, coupon=self.coupon) assert user_coupon.updated_on > previous_modified assert resp.json() == { 'message': 'Attached user to coupon successfully.', 'coupon': { 'amount': str(self.coupon.amount), 'amount_type': self.coupon.amount_type, 'content_type': self.coupon.content_type.model, 'coupon_type': self.coupon.coupon_type, 'coupon_code': self.coupon.coupon_code, 'object_id': self.coupon.object_id, 'program_id': self.coupon.program.id, } } assert UserCouponAudit.objects.count() == 1 audit = UserCouponAudit.objects.first() assert audit.user_coupon == user_coupon assert audit.data_after == serialize_model_object(user_coupon)
def test_full(self): """ Test full serializer """ birthdate = date(1980, 1, 2) profile = self.create_profile(date_of_birth=birthdate) data = ProfileSerializer(profile).data assert data == { 'username': get_social_username(profile.user), 'first_name': profile.first_name, 'full_name': profile.full_name, 'filled_out': profile.filled_out, 'agreed_to_terms_of_service': profile.agreed_to_terms_of_service, 'last_name': profile.last_name, 'preferred_name': profile.preferred_name, 'email_optin': profile.email_optin, 'email': profile.email, 'gender': profile.gender, 'date_of_birth': "1980-01-02", 'account_privacy': profile.account_privacy, 'country': profile.country, 'state_or_territory': profile.state_or_territory, 'city': profile.city, 'address': profile.address, 'postal_code': profile.postal_code, 'birth_country': profile.birth_country, 'nationality': profile.nationality, 'preferred_language': profile.preferred_language, 'pretty_printed_student_id': profile.pretty_printed_student_id, 'edx_level_of_education': profile.edx_level_of_education, 'education': EducationSerializer(profile.education.all(), many=True).data, 'work_history': ( EmploymentSerializer(profile.work_history.all(), many=True).data ), 'image': profile.image.url, 'image_small': profile.image_small.url, 'image_medium': profile.image_medium.url, 'about_me': profile.about_me, 'romanized_first_name': profile.romanized_first_name, 'romanized_last_name': profile.romanized_last_name, 'phone_number': profile.phone_number, 'student_id': profile.student_id, }
def test_create_user_coupon(self, already_exists): """ Test happy case for creating a UserCoupon """ previous_modified = self.coupon.user_coupon_qset(self.user).first().updated_on if not already_exists: # Won't change anything if it already exists UserCoupon.objects.all().delete() data = { 'username': get_social_username(self.user), } with patch( 'ecommerce.views.is_coupon_redeemable', autospec=True ) as _is_redeemable_mock: _is_redeemable_mock.return_value = True resp = self.client.post( reverse('coupon-user-create', kwargs={'code': self.coupon.coupon_code}), data=data, format='json', ) _is_redeemable_mock.assert_called_with(self.coupon, self.user) assert resp.status_code == status.HTTP_200_OK assert UserCoupon.objects.count() == 1 user_coupon = UserCoupon.objects.get(user=self.user, coupon=self.coupon) assert user_coupon.updated_on > previous_modified assert resp.json() == { 'message': 'Attached user to coupon successfully.', 'coupon': { 'amount': str(self.coupon.amount), 'amount_type': self.coupon.amount_type, 'content_type': self.coupon.content_type.model, 'coupon_type': self.coupon.coupon_type, 'coupon_code': self.coupon.coupon_code, 'object_id': self.coupon.object_id, 'program_id': self.coupon.program.id, } } assert UserCouponAudit.objects.count() == 1 audit = UserCouponAudit.objects.first() assert audit.user_coupon == user_coupon assert audit.data_after == serialize_model_object(user_coupon)
def get(self, request, *args, **kwargs): """ Handle GET requests to templates using React """ user = request.user username = get_social_username(user) name = "" roles = [] if not user.is_anonymous(): name = user.profile.preferred_name roles = [{ 'program': role.program.id, 'role': role.role, 'permissions': [ perm for perm, value in available_perm_status(user).items() if value is True ] } for role in user.role_set.all()] js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "reactGaDebug": settings.REACT_GA_DEBUG, "authenticated": not user.is_anonymous(), "name": name, "username": username, "host": webpack_dev_server_host(request), "edx_base_url": settings.EDXORG_BASE_URL, "roles": roles, "search_url": reverse('search_api', kwargs={"elastic_url": ""}), } return render(request, "dashboard.html", context={ "style_src": get_bundle_url(request, "style.js"), "dashboard_src": get_bundle_url(request, "dashboard.js"), "js_settings_json": json.dumps(js_settings), })
def enroll_user_on_success(order): """ Enroll user after they made a successful purchase. Args: order (Order): An order to be fulfilled Returns: None """ user_social = get_social_auth(order.user) enrollments_client = EdxApi(user_social.extra_data, settings.EDXORG_BASE_URL).enrollments existing_enrollments = enrollments_client.get_student_enrollments() exceptions = [] enrollments = [] for line in order.line_set.all(): course_key = line.course_key try: if not existing_enrollments.is_enrolled_in(course_key): enrollments.append( enrollments_client.create_audit_student_enrollment( course_key)) except Exception as ex: # pylint: disable=broad-except log.exception( "Error creating audit enrollment for course key %s for user %s", course_key, get_social_username(order.user), ) exceptions.append(ex) for enrollment in enrollments: CachedEdxDataApi.update_cached_enrollment( order.user, enrollment, enrollment.course_id, index_user=True, ) if exceptions: raise EcommerceEdxApiException(exceptions)
def standard_error_page(request, status_code, template_filename): """ Returns an error page with a given template filename and provides necessary context variables """ name = request.user.profile.preferred_name if not request.user.is_anonymous( ) else "" authenticated = not request.user.is_anonymous() username = get_social_username(request.user) response = render(request, template_filename, context={ "style_src": get_bundle_url(request, "style.js"), "dashboard_src": get_bundle_url(request, "dashboard.js"), "js_settings_json": "{}", "authenticated": authenticated, "name": name, "username": username }) response.status_code = status_code return response
def test_update_profile(self, mocked_get_json): """ Happy path """ mocked_content = self.mocked_edx_profile mocked_get_json.return_value = mocked_content pipeline_api.update_profile_from_edx( edxorg.EdxOrgOAuth2, self.user, {'access_token': 'foo_token'}, True) mocked_get_json.assert_called_once_with( urljoin( edxorg.EdxOrgOAuth2.EDXORG_BASE_URL, '/api/user/v1/accounts/{0}'.format(get_social_username(self.user)) ), headers={'Authorization': 'Bearer foo_token'} ) first_name, last_name = split_name(mocked_content['name']) all_fields = [ ('account_privacy', Profile.PUBLIC_TO_MM), ('edx_name', mocked_content['name']), ('first_name', first_name), ('last_name', last_name), ('preferred_name', None), ('edx_bio', mocked_content['bio']), ('country', mocked_content['country']), ('has_profile_image', mocked_content['profile_image']['has_image']), ('edx_requires_parental_consent', mocked_content['requires_parental_consent']), ('edx_level_of_education', mocked_content['level_of_education']), ('edx_goals', mocked_content['goals']), ('edx_language_proficiencies', mocked_content['language_proficiencies']), ('preferred_language', mocked_content['language_proficiencies'][0]['code']), ('gender', mocked_content['gender']), ('edx_mailing_address', mocked_content['mailing_address']), ] self.check_profile_fields(self.user_profile, all_fields) # We do not set the date_of_birth using year_of_birth assert self.user_profile.date_of_birth is None
def get_context(self, request, *args, **kwargs): programs = Program.objects.filter( live=True).select_related('programpage').order_by("id") benefits_page = BenefitsPage.objects.filter(live=True).first() js_settings = { "gaTrackingID": settings.GA_TRACKING_ID, "host": webpack_dev_server_host(request), "environment": settings.ENVIRONMENT, "sentry_dsn": settings.SENTRY_DSN, "release_version": settings.VERSION } username = get_social_username(request.user) context = super(HomePage, self).get_context(request) def get_program_page(program): """Return a None if ProgramPage does not exist, to avoid template errors""" try: return program.programpage except ProgramPage.DoesNotExist: return None program_pairs = [(program, get_program_page(program)) for program in programs] context["programs"] = program_pairs context["is_public"] = True context["has_zendesk_widget"] = True context["google_maps_api"] = False context["authenticated"] = not request.user.is_anonymous context["is_staff"] = has_role(request.user, [Staff.ROLE_ID, Instructor.ROLE_ID]) context["username"] = username context["js_settings_json"] = json.dumps(js_settings) context["title"] = self.title context["ga_tracking_id"] = "" context["coupon_code"] = get_coupon_code(request) context["benefits_url"] = benefits_page.get_url( ) if benefits_page else "" return context
def create_unfulfilled_order(course_key, user): """ Create a new Order which is not fulfilled for a purchasable course run. If course run is not purchasable, it raises an Http404 Args: course_key (str): A course key user (User): The purchaser of the course run Returns: Order: A newly created Order for the CourseRun with the given course_id """ course_run = get_purchasable_course_run(course_key, user) price, coupon = calculate_run_price(course_run, user) if price < 0: log.error( "Price to be charged for course run %s for user %s is less than zero: %s", course_key, get_social_username(user), price, ) raise ImproperlyConfigured("Price to be charged is less than zero") order = Order.objects.create( status=Order.CREATED, total_price_paid=price, user=user, ) Line.objects.create( order=order, course_key=course_key, description='Seat for {}'.format(course_run.title), price=price, ) if coupon is not None: redeemed_coupon = RedeemedCoupon(order=order, coupon=coupon) redeemed_coupon.save_and_log(user) order.save_and_log(user) return order
def enroll_user_on_success(order): """ Enroll user after they made a successful purchase. Args: order (Order): An order to be fulfilled Returns: None """ user_social = get_social_auth(order.user) enrollments_client = EdxApi(user_social.extra_data, settings.EDXORG_BASE_URL).enrollments existing_enrollments = enrollments_client.get_student_enrollments() exceptions = [] enrollments = [] for line in order.line_set.all(): course_key = line.course_key try: if not existing_enrollments.is_enrolled_in(course_key): enrollments.append(enrollments_client.create_audit_student_enrollment(course_key)) except Exception as ex: # pylint: disable=broad-except log.exception( "Error creating audit enrollment for course key %s for user %s", course_key, get_social_username(order.user), ) exceptions.append(ex) for enrollment in enrollments: CachedEdxDataApi.update_cached_enrollment( order.user, enrollment, enrollment.course_id, index_user=True, ) if exceptions: raise EcommerceEdxApiException(exceptions)
def update_cached_current_grades(cls, user, edx_client): """ Updates cached current grade data. Args: user (django.contrib.auth.models.User): A user edx_client (EdxApi): EdX client to retrieve enrollments Returns: None """ course_ids = models.CachedEnrollment.active_course_ids(user) # Current Grades are out of date, so fetch new data from edX. current_grades = edx_client.current_grades.get_student_current_grades( get_social_username(user), course_ids) # the update must be done atomically with transaction.atomic(): all_grade_course_ids = current_grades.all_course_ids for course_run in CourseRun.objects.filter(edx_course_key__in=all_grade_course_ids): current_grade = current_grades.get_current_grade(course_run.edx_course_key) updated_values = { 'user': user, 'course_run': course_run, 'data': current_grade.json, } models.CachedCurrentGrade.objects.update_or_create( user=user, course_run=course_run, defaults=updated_values ) # delete anything is not in the current grades models.CachedCurrentGrade.delete_all_but(user, all_grade_course_ids) # update the last refresh timestamp cls.update_cache_last_access(user, cls.CURRENT_GRADE) # submit a celery task to reindex the user tasks.index_users.delay([user.id], check_if_changed=True)
def test_anonymous_user(self): """ get_social_username should return None for anonymous users """ user = Mock(is_anonymous=True) assert get_social_username(user) is None
def get_username(self, obj): """Getter for the username field""" return get_social_username(obj.user)
def test_zero_social(self): """ get_social_username should return None if there is no edX account associated yet """ self.user.social_auth.all().delete() assert get_social_username(self.user) is None
def test_one_social(self): """ get_social_username should return the social username, not the Django username """ assert get_social_username(self.user) == self.user.social_auth.first().uid
def test_users_anonymous(self): """ Assert settings we pass to dashboard """ profile = self.create_and_login_user() user = profile.user self.client.logout() username = get_social_username(user) ga_tracking_id = FuzzyText().fuzz() react_ga_debug = FuzzyText().fuzz() edx_base_url = FuzzyText().fuzz() host = FuzzyText().fuzz() email_support = FuzzyText().fuzz() open_discussions_redirect_url = FuzzyText().fuzz() with self.settings( GA_TRACKING_ID=ga_tracking_id, REACT_GA_DEBUG=react_ga_debug, EDXORG_BASE_URL=edx_base_url, WEBPACK_DEV_SERVER_HOST=host, EMAIL_SUPPORT=email_support, VERSION='0.0.1', RAVEN_CONFIG={'dsn': ''}, ELASTICSEARCH_DEFAULT_PAGE_SIZE=10, EXAMS_SSO_CLIENT_CODE='itsacode', EXAMS_SSO_URL='url', OPEN_DISCUSSIONS_REDIRECT_URL=open_discussions_redirect_url ): # Mock has_permission so we don't worry about testing permissions here has_permission = Mock(return_value=True) with patch( 'profiles.permissions.CanSeeIfNotPrivate.has_permission', has_permission, ), patch('ui.templatetags.render_bundle._get_bundle') as get_bundle: resp = self.client.get(reverse('ui-users', kwargs={'user': username})) assert resp.status_code == 200 assert resp.context['is_public'] is False assert resp.context['has_zendesk_widget'] is True self.assertNotContains(resp, 'Share this page') js_settings = json.loads(resp.context['js_settings_json']) assert js_settings == { 'gaTrackingID': ga_tracking_id, 'reactGaDebug': react_ga_debug, 'user': None, 'host': host, 'edx_base_url': edx_base_url, 'roles': [], 'search_url': reverse('search_api', kwargs={"elastic_url": ""}), 'support_email': email_support, 'environment': 'dev', 'release_version': '0.0.1', 'sentry_dsn': None, 'es_page_size': 10, 'public_path': '/static/bundles/', 'EXAMS_SSO_CLIENT_CODE': 'itsacode', 'EXAMS_SSO_URL': 'url', 'FEATURES': { 'PROGRAM_LEARNERS': False, 'DISCUSSIONS_POST_UI': False, 'DISCUSSIONS_CREATE_CHANNEL_UI': False, 'PROGRAM_RECORD_LINK': False, 'ENABLE_PROGRAM_LETTER': False, }, 'open_discussions_redirect_url': open_discussions_redirect_url } assert has_permission.called bundles = [bundle[0][1] for bundle in get_bundle.call_args_list] assert set(bundles) == { 'common', 'dashboard', 'sentry_client', 'style', 'zendesk_widget', }
def get_purchasable_course_run(course_key, user): """ Gets a course run, or raises Http404 if not purchasable. To be purchasable a course run must not already be purchased, must be part of a live program, must be part of a program with financial aid enabled, with a financial aid object, and must have a valid price. Args: course_key (str): An edX course key user (User): The purchaser of the course run Returns: CourseRun: A course run """ # Make sure it's connected to a live program, it has a valid price, and the user is enrolled in the program already try: course_run = get_object_or_404( CourseRun, edx_course_key=course_key, course__program__live=True, course__program__financial_aid_availability=True, ) except Http404: log.warning("Course run %s is not purchasable", course_key) raise if not FinancialAid.objects.filter( tier_program__current=True, tier_program__program__course__courserun=course_run, user=user, status__in=FinancialAidStatus.TERMINAL_STATUSES, ).exists(): log.warning("Course run %s has no attached financial aid for user %s", course_key, get_social_username(user)) raise ValidationError( "Course run {} does not have a current attached financial aid application".format(course_key) ) # Make sure it's not already purchased if Line.objects.filter( order__status__in=Order.FULFILLED_STATUSES, order__user=user, course_key=course_run.edx_course_key, ).exists(): mmtrack = get_mmtrack(user, course_run.course.program) if not has_to_pay_for_exam(mmtrack, course_run.course): log.warning("Course run %s is already purchased by user %s", course_key, user) raise ValidationError("Course run {} is already purchased".format(course_key)) return course_run