Merge branch 'master' into release-mergeback-to-master
This commit is contained in:
@@ -15,16 +15,17 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import six
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.template import engines
|
||||
from django.urls import reverse
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site
|
||||
from xmodule.util.xmodule_django import get_current_request_hostname
|
||||
|
||||
from . import Engines
|
||||
|
||||
@@ -70,9 +71,13 @@ def marketing_link(name):
|
||||
elif not enable_mktg_site and name in link_map:
|
||||
# don't try to reverse disabled marketing links
|
||||
if link_map[name] is not None:
|
||||
return reverse(link_map[name])
|
||||
host_name = get_current_request_hostname()
|
||||
if all([host_name and 'edge' in host_name, 'http' in link_map[name]]):
|
||||
return link_map[name]
|
||||
else:
|
||||
return reverse(link_map[name])
|
||||
else:
|
||||
log.debug("Cannot find corresponding link for name: %s", name)
|
||||
log.debug(u"Cannot find corresponding link for name: %s", name)
|
||||
return '#'
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.core.mail.message import EmailMultiAlternatives
|
||||
from django.template.loader import get_template
|
||||
from pytz import utc
|
||||
from os import remove
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student.models import CourseAccessRole
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Example usage:
|
||||
$ ./manage.py lms export_staff_users -d 7 --settings=devstack_docker
|
||||
$ ./manage.py lms export_staff_users --days 7 --settings=devstack_docker
|
||||
$ ./manage.py lms export_staff_users --days 7 --dry true --settings=devstack_docker
|
||||
"""
|
||||
|
||||
help = """
|
||||
This command will export a csv of all users who have logged in within the given days and
|
||||
have staff access role in active courses (Courses with end date in the future).
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--days',
|
||||
type=int,
|
||||
default=7,
|
||||
help='Indicate the login time period in days starting from today'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r',
|
||||
'--dry',
|
||||
type=str,
|
||||
help='Indicate that the email should not be sent to author-support'
|
||||
)
|
||||
|
||||
subject = 'Staff users CSV'
|
||||
to_addresses = ['author-support@edx.org']
|
||||
from_address = settings.DEFAULT_FROM_EMAIL
|
||||
txt_template_path = 'email/export_staff_users.txt'
|
||||
html_template_path = 'email/export_staff_users.html'
|
||||
csv_filename = 'staff_users.csv'
|
||||
|
||||
def write_csv(self, query_set, filename):
|
||||
"""
|
||||
Writes the queryset into a csv file with the given filename
|
||||
|
||||
Arguments:
|
||||
query_set: query_set to be converted
|
||||
filename: filename for the csv
|
||||
"""
|
||||
writer = csv.DictWriter(
|
||||
filename,
|
||||
fieldnames=['id', 'user__username', 'user__email', 'role']
|
||||
)
|
||||
writer.writeheader()
|
||||
for data_item in query_set:
|
||||
writer.writerow(data_item)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
days = kwargs['days']
|
||||
dry = kwargs.get('dry')
|
||||
if dry:
|
||||
self.to_addresses = ['sustaining-mavericks@edx.org']
|
||||
current_date = datetime.now(tz=utc)
|
||||
starting_date = current_date - timedelta(days=days)
|
||||
active_courses = CourseOverview.objects.filter(end__gte=current_date).values_list('id', flat=True)
|
||||
course_access_roles = CourseAccessRole.objects.filter(
|
||||
role__in=['staff', 'instructor'],
|
||||
user__last_login__range=(starting_date, current_date),
|
||||
course_id__in=active_courses,
|
||||
user__is_staff=False
|
||||
).values('id', 'user__username', 'user__email', 'role')
|
||||
if not course_access_roles:
|
||||
return
|
||||
with open(self.csv_filename, 'a+') as csv_file:
|
||||
self.write_csv(
|
||||
query_set=course_access_roles,
|
||||
filename=csv_file
|
||||
)
|
||||
context = {'time_period': days}
|
||||
try:
|
||||
self.send_email(context)
|
||||
logger.info(
|
||||
'Sent staff users email for the period {} to {}. Staff users count:{}'.format(
|
||||
starting_date,
|
||||
current_date,
|
||||
course_access_roles.count()
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to send staff users email for the period {}-{}'.format(starting_date, current_date)
|
||||
)
|
||||
|
||||
def send_email(self, context):
|
||||
"""
|
||||
Sends an email to admin containing a csv of all users who have logged in within the given days and
|
||||
have staff access role in active courses (Courses with end date in the future).
|
||||
|
||||
Arguments:
|
||||
context: context for the email template
|
||||
"""
|
||||
plain_content = self.render_template(self.txt_template_path, context)
|
||||
html_content = self.render_template(self.html_template_path, context)
|
||||
|
||||
with open(self.csv_filename, 'r') as csv_file:
|
||||
email_message = EmailMultiAlternatives(self.subject, plain_content, self.from_address, to=self.to_addresses)
|
||||
email_message.attach_alternative(html_content, 'text/html')
|
||||
email_message.attach(self.csv_filename, csv_file.read(), 'text/csv')
|
||||
email_message.send()
|
||||
|
||||
remove(self.csv_filename)
|
||||
|
||||
def render_template(self, path, context):
|
||||
"""
|
||||
Takes a template path and context and returns a rendered template
|
||||
|
||||
Arguments:
|
||||
path: path of the file
|
||||
context: context for the template
|
||||
"""
|
||||
txt_template = get_template(path)
|
||||
return txt_template.render(context)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Unit tests for export_staff_users management command.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core import mail
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from student.tests.factories import CourseAccessRoleFactory, UserFactory
|
||||
|
||||
|
||||
class TestExportStaffUsers(TestCase):
|
||||
"""
|
||||
Tests the `export_staff_users` command.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_users_data():
|
||||
staff_user = UserFactory(last_login=now() - timedelta(days=5))
|
||||
instructor_user = UserFactory(last_login=now() - timedelta(days=5))
|
||||
course = CourseOverviewFactory(end=now() + timedelta(days=30))
|
||||
archived_course = CourseOverviewFactory(end=now() - timedelta(days=30))
|
||||
course_ids = [course.id, archived_course.id]
|
||||
for course_id in course_ids:
|
||||
CourseAccessRoleFactory.create(course_id=course_id, user=staff_user, role="staff")
|
||||
CourseAccessRoleFactory.create(course_id=course_id, user=instructor_user, role="instructor")
|
||||
|
||||
def test_export_staff_users(self):
|
||||
self.create_users_data()
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
call_command('export_staff_users', days=7)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
21
common/djangoapps/student/templates/email/email_base.html
Normal file
21
common/djangoapps/student/templates/email/email_base.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1.0"> <!-- So that mobile webkit will display zoomed in -->
|
||||
<meta name="format-detection" content="telephone=no"> <!-- disable auto telephone linking in iOS -->
|
||||
</head>
|
||||
<body style="font-family:Arial,'Helvetica Neue',Helvetica,sans-serif;font-size:14px;line-height:150%;margin:auto">
|
||||
|
||||
<table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" style="padding: 5px;">
|
||||
<tr>
|
||||
<td align="" valign="top">
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "email/email_base.html" %}
|
||||
{% block body %}
|
||||
<!-- Message Body -->
|
||||
<p>
|
||||
Dear Admin,
|
||||
<p>
|
||||
<p>
|
||||
Please find the attached CSV containing a list of all staff users
|
||||
who have logged in within the last {{ time_period }} days
|
||||
</p>
|
||||
|
||||
<p>Thanks,</p>
|
||||
<p>The edX Team</p>
|
||||
<!-- End Message Body -->
|
||||
{% endblock body %}
|
||||
@@ -0,0 +1,7 @@
|
||||
Dear Admin,
|
||||
|
||||
Please find the attached CSV containing a list of all staff users who have logged in within the last {{ time_period }} days
|
||||
|
||||
|
||||
Thanks,
|
||||
The edX Team
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from mock import patch
|
||||
@@ -103,6 +104,10 @@ class TestActivateAccount(TestCase):
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
self.assertNotContains(response, expected_message)
|
||||
|
||||
def _assert_user_active_state(self, expected_active_state):
|
||||
user = User.objects.get(username=self.user.username)
|
||||
self.assertEqual(user.is_active, expected_active_state)
|
||||
|
||||
def test_account_activation_notification_on_logistration(self):
|
||||
"""
|
||||
Verify that logistration page displays success/error/info messages
|
||||
@@ -112,15 +117,19 @@ class TestActivateAccount(TestCase):
|
||||
login_url=reverse('signin_user'),
|
||||
redirect_url=reverse('dashboard'),
|
||||
)
|
||||
self._assert_user_active_state(expected_active_state=False)
|
||||
|
||||
# Access activation link, message should say that account has been activated.
|
||||
response = self.client.get(reverse('activate', args=[self.registration.activation_key]), follow=True)
|
||||
self.assertRedirects(response, login_page_url)
|
||||
self.assertContains(response, 'Success! You have activated your account.')
|
||||
self._assert_user_active_state(expected_active_state=True)
|
||||
|
||||
# Access activation link again, message should say that account is already active.
|
||||
response = self.client.get(reverse('activate', args=[self.registration.activation_key]), follow=True)
|
||||
self.assertRedirects(response, login_page_url)
|
||||
self.assertContains(response, 'This account has already been activated.')
|
||||
self._assert_user_active_state(expected_active_state=True)
|
||||
|
||||
# Open account activation page with an invalid activation link,
|
||||
# there should be an error message displayed.
|
||||
@@ -137,4 +146,4 @@ class TestActivateAccount(TestCase):
|
||||
response = self.client.get(reverse('activate', args=[self.registration.activation_key]), follow=True)
|
||||
self.assertRedirects(response, login_page_url)
|
||||
self.assertContains(response, SYSTEM_MAINTENANCE_MSG)
|
||||
assert not self.user.is_active
|
||||
self._assert_user_active_state(expected_active_state=False)
|
||||
|
||||
@@ -12,30 +12,19 @@ from datetime import datetime, timedelta
|
||||
import ddt
|
||||
import six
|
||||
from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing
|
||||
from course_modes.models import CourseMode
|
||||
from django.conf import settings
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pyquery import PyQuery as pq
|
||||
from six.moves import range
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from openedx.core.djangoapps.user_authn.cookies import _get_user_info_cookie_data
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES
|
||||
from student.models import CourseEnrollment, UserProfile
|
||||
from student.signals import REFUND_ORDER
|
||||
@@ -46,6 +35,17 @@ from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
|
||||
PASSWORD = 'test'
|
||||
|
||||
|
||||
@@ -222,6 +222,33 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
response = self.client.get(self.path)
|
||||
self.assertRedirects(response, reverse('account_settings'))
|
||||
|
||||
def test_grade_doesnt_appears_before_course_end_date(self):
|
||||
"""
|
||||
Verify that learners are not able to see their final grade before the end
|
||||
of course in the learner dashboard
|
||||
"""
|
||||
self.course = CourseFactory.create(end=self.TOMORROW, emit_signals=True)
|
||||
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
|
||||
GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
# The final grade does not appear before the course has ended
|
||||
self.assertNotContains(response, 'Your final grade:')
|
||||
self.assertNotContains(response, '<span class="grade-value">45%</span>')
|
||||
|
||||
def test_grade_appears_after_course_has_ended(self):
|
||||
"""
|
||||
Verify that learners are able to see their final grade of the course in
|
||||
the learner dashboard after the course had ended
|
||||
"""
|
||||
self.course = CourseFactory.create(end=self.THREE_YEARS_AGO, emit_signals=True)
|
||||
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
|
||||
GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
self.assertContains(response, 'Your final grade:')
|
||||
self.assertContains(response, '<span class="grade-value">45%</span>')
|
||||
|
||||
@patch.multiple('django.conf.settings', **MOCK_SETTINGS)
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
|
||||
@@ -18,7 +18,6 @@ urlpatterns = [
|
||||
url(r'^accounts/disable_account_ajax$', views.disable_account_ajax, name="disable_account_ajax"),
|
||||
url(r'^accounts/manage_user_standing', views.manage_user_standing, name='manage_user_standing'),
|
||||
|
||||
url(r'^change_setting$', views.change_setting, name='change_setting'),
|
||||
url(r'^change_email_settings$', views.change_email_settings, name='change_email_settings'),
|
||||
|
||||
url(r'^course_run/{}/refund_status$'.format(settings.COURSE_ID_PATTERN),
|
||||
|
||||
@@ -468,24 +468,6 @@ def disable_account_ajax(request):
|
||||
return JsonResponse(context)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def change_setting(request):
|
||||
"""
|
||||
JSON call to change a profile setting: Right now, location
|
||||
"""
|
||||
# TODO (vshnayder): location is no longer used
|
||||
u_prof = UserProfile.objects.get(user=request.user) # request.user.profile_cache
|
||||
if 'location' in request.POST:
|
||||
u_prof.location = request.POST['location']
|
||||
u_prof.save()
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"location": u_prof.location,
|
||||
})
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_signup_handler(sender, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
|
||||
@@ -209,6 +209,11 @@ def get(request):
|
||||
"""Gets the running pipeline's data from the passed request."""
|
||||
strategy = social_django.utils.load_strategy(request)
|
||||
token = strategy.session_get('partial_pipeline_token')
|
||||
|
||||
if not token:
|
||||
strategy.session_set('partial_pipeline_token', strategy.session_get('partial_pipeline_token_'))
|
||||
token = strategy.session_get('partial_pipeline_token')
|
||||
|
||||
partial_object = strategy.partial_load(token)
|
||||
pipeline_data = None
|
||||
if partial_object:
|
||||
@@ -560,6 +565,10 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
|
||||
return (current_provider and
|
||||
current_provider.slug in [saml_provider.slug for saml_provider in saml_providers_list])
|
||||
|
||||
if current_partial:
|
||||
strategy.session_set('partial_pipeline_token_', current_partial.token)
|
||||
strategy.storage.partial.store(current_partial)
|
||||
|
||||
if not user:
|
||||
# Use only email for user existence check in case of saml provider
|
||||
if is_provider_saml():
|
||||
|
||||
@@ -123,18 +123,17 @@ class HelperMixin(object):
|
||||
|
||||
def assert_json_failure_response_is_inactive_account(self, response):
|
||||
"""Asserts failure on /login for inactive account looks right."""
|
||||
self.assertEqual(200, response.status_code) # Yes, it's a 200 even though it's a failure.
|
||||
self.assertEqual(400, response.status_code)
|
||||
payload = json.loads(response.content.decode('utf-8'))
|
||||
self.assertFalse(payload.get('success'))
|
||||
self.assertIn('In order to sign in, you need to activate your account.', payload.get('value'))
|
||||
|
||||
def assert_json_failure_response_is_missing_social_auth(self, response):
|
||||
"""Asserts failure on /login for missing social auth looks right."""
|
||||
self.assertContains(
|
||||
response,
|
||||
u"successfully signed in to your %s account, but this account isn't linked" % self.provider.name,
|
||||
status_code=403,
|
||||
)
|
||||
self.assertEqual(403, response.status_code)
|
||||
payload = json.loads(response.content.decode('utf-8'))
|
||||
self.assertFalse(payload.get('success'))
|
||||
self.assertEqual(payload.get('error_code'), 'third-party-auth-with-no-linked-account')
|
||||
|
||||
def assert_json_failure_response_is_username_collision(self, response):
|
||||
"""Asserts the json response indicates a username collision."""
|
||||
@@ -542,6 +541,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
|
||||
request.user = self.create_user_models_for_existing_account(
|
||||
strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True)
|
||||
partial_pipeline_token = strategy.session_get('partial_pipeline_token')
|
||||
partial_data = strategy.storage.partial.load(partial_pipeline_token)
|
||||
|
||||
# Instrument the pipeline to get to the dashboard with the full
|
||||
# expected state.
|
||||
@@ -561,24 +562,14 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
# the "logged in" cookie for the marketing site.
|
||||
self.assert_logged_in_cookie_redirect(actions.do_complete(
|
||||
request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request
|
||||
))
|
||||
self.assert_logged_in_cookie_redirect(self.do_complete(strategy, request, partial_pipeline_token, partial_data))
|
||||
|
||||
# Set the cookie and try again
|
||||
self.set_logged_in_cookies(request)
|
||||
|
||||
# Fire off the auth pipeline to link.
|
||||
self.assert_redirect_after_pipeline_completes(
|
||||
actions.do_complete(
|
||||
request.backend,
|
||||
social_views._do_login, # pylint: disable=protected-access
|
||||
request.user,
|
||||
None,
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME,
|
||||
request=request
|
||||
)
|
||||
self.do_complete(strategy, request, partial_pipeline_token, partial_data)
|
||||
)
|
||||
|
||||
# Now we expect to be in the linked state, with a backend entry.
|
||||
@@ -694,6 +685,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
|
||||
user = self.create_user_models_for_existing_account(
|
||||
strategy, 'user@example.com', 'password', self.get_username())
|
||||
partial_pipeline_token = strategy.session_get('partial_pipeline_token')
|
||||
partial_data = strategy.storage.partial.load(partial_pipeline_token)
|
||||
|
||||
self.assert_social_auth_exists_for_user(user, strategy)
|
||||
self.assertTrue(user.is_active)
|
||||
|
||||
@@ -734,7 +728,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
self.set_logged_in_cookies(request)
|
||||
|
||||
self.assert_redirect_after_pipeline_completes(
|
||||
actions.do_complete(request.backend, social_views._do_login, user=user, request=request))
|
||||
self.do_complete(strategy, request, partial_pipeline_token, partial_data, user)
|
||||
)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request))
|
||||
|
||||
def test_signin_fails_if_account_not_active(self):
|
||||
@@ -793,6 +788,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
request, strategy = self.get_request_and_strategy(
|
||||
auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete')
|
||||
strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
|
||||
partial_pipeline_token = strategy.session_get('partial_pipeline_token')
|
||||
partial_data = strategy.storage.partial.load(partial_pipeline_token)
|
||||
|
||||
# Begin! Grab the registration page and check the login control on it.
|
||||
self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register'))
|
||||
@@ -846,15 +843,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
# the "logged in" cookie for the marketing site.
|
||||
self.assert_logged_in_cookie_redirect(actions.do_complete(
|
||||
request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request
|
||||
))
|
||||
self.assert_logged_in_cookie_redirect(self.do_complete(strategy, request, partial_pipeline_token, partial_data))
|
||||
|
||||
# Set the cookie and try again
|
||||
self.set_logged_in_cookies(request)
|
||||
self.assert_redirect_after_pipeline_completes(
|
||||
actions.do_complete(strategy.request.backend, social_views._do_login, user=created_user, request=request))
|
||||
self.do_complete(strategy, request, partial_pipeline_token, partial_data, created_user)
|
||||
)
|
||||
# Now the user has been redirected to the dashboard. Their third party account should now be linked.
|
||||
self.assert_social_auth_exists_for_user(created_user, strategy)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
|
||||
@@ -974,6 +969,19 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def do_complete(self, strategy, request, partial_pipeline_token, partial_data, user=None):
|
||||
"""
|
||||
Makes sure that strategy store includes the partial data object before
|
||||
calling actions.do_complete
|
||||
"""
|
||||
strategy.storage.partial.store(partial_data)
|
||||
if not user:
|
||||
user = request.user
|
||||
return actions.do_complete(
|
||||
request.backend, social_views._do_login, user, None, # pylint: disable=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request, partial_token=partial_pipeline_token
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
@django_utils.override_settings(ECOMMERCE_API_URL=TEST_API_URL)
|
||||
|
||||
@@ -4,6 +4,15 @@ from __future__ import absolute_import
|
||||
from third_party_auth.tests.specs import base
|
||||
|
||||
|
||||
def get_localized_name(name):
|
||||
"""Returns the localizedName from the name object"""
|
||||
locale = "{}_{}".format(
|
||||
name["preferredLocale"]["language"],
|
||||
name["preferredLocale"]["country"]
|
||||
)
|
||||
return name['localized'].get(locale, '')
|
||||
|
||||
|
||||
class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest):
|
||||
"""Integration tests for provider.LinkedInOauth2."""
|
||||
|
||||
@@ -21,11 +30,29 @@ class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest):
|
||||
'expires_in': 'expires_in_value',
|
||||
}
|
||||
USER_RESPONSE_DATA = {
|
||||
'lastName': 'lastName_value',
|
||||
'lastName': {
|
||||
"localized": {
|
||||
"en_US": "Doe"
|
||||
},
|
||||
"preferredLocale": {
|
||||
"country": "US",
|
||||
"language": "en"
|
||||
}
|
||||
},
|
||||
'id': 'id_value',
|
||||
'firstName': 'firstName_value',
|
||||
'firstName': {
|
||||
"localized": {
|
||||
"en_US": "Doe"
|
||||
},
|
||||
"preferredLocale": {
|
||||
"country": "US",
|
||||
"language": "en"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def get_username(self):
|
||||
response_data = self.get_response_data()
|
||||
return response_data.get('firstName') + response_data.get('lastName')
|
||||
first_name = get_localized_name(response_data.get('firstName'))
|
||||
last_name = get_localized_name(response_data.get('lastName'))
|
||||
return first_name + last_name
|
||||
|
||||
@@ -98,7 +98,7 @@ class ThirdPartyOAuthTestMixinFacebook(object):
|
||||
class ThirdPartyOAuthTestMixinGoogle(object):
|
||||
"""Tests oauth with the Google backend"""
|
||||
BACKEND = "google-oauth2"
|
||||
USER_URL = "https://www.googleapis.com/plus/v1/people/me"
|
||||
USER_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
# In google-oauth2 responses, the "email" field is used as the user's identifier
|
||||
UID_FIELD = "email"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import six.moves.urllib.error
|
||||
import six.moves.urllib.parse
|
||||
import six.moves.urllib.request
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
|
||||
@@ -73,3 +74,9 @@ def has_certificates_enabled(course):
|
||||
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
|
||||
return False
|
||||
return course.cert_html_view_enabled
|
||||
|
||||
|
||||
def should_display_grade(end_date):
|
||||
if end_date and end_date < now().replace(hour=0, minute=0, second=0, microsecond=0):
|
||||
return True
|
||||
return False
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user