Skip to content

Commit

Permalink
Merge branch 'master' into iamsobanjaved/upgrade-social-auth-app-django
Browse files Browse the repository at this point in the history
  • Loading branch information
iamsobanjaved authored Jul 9, 2024
2 parents 1b9948a + 96da990 commit 43e6aa8
Show file tree
Hide file tree
Showing 48 changed files with 1,001 additions and 253 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

- name: Get pip cache dir
id: pip-cache-dir
run: echo "::set-output name=dir::$(pip cache dir)"
run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT

- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/js-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:

- name: Get pip cache dir
id: pip-cache-dir
run: echo "::set-output name=dir::$(pip cache dir)"
run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT

- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/migrations-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pylint-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/quality-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip dependencies
id: cache-dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/static-assets-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip dependencies
id: cache-dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


import datetime

import ddt
import pytz
from django.test import RequestFactory
Expand Down
3 changes: 2 additions & 1 deletion cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,8 @@
ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = {}

# Setting for Open API key and prompts used by edx-enterprise.
OPENAI_API_KEY = ''
CHAT_COMPLETION_API = 'https://example.com/chat/completion'
CHAT_COMPLETION_API_KEY = 'i am a key'
LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ''
LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ''
LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT = ''
Expand Down
3 changes: 2 additions & 1 deletion cms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ def get_env_setting(setting):
AUTHORING_API_URL = ENV_TOKENS.get('AUTHORING_API_URL', '')
# Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.

OPENAI_API_KEY = ENV_TOKENS.get('OPENAI_API_KEY', '')
CHAT_COMPLETION_API = ENV_TOKENS.get('CHAT_COMPLETION_API', '')
CHAT_COMPLETION_API_KEY = ENV_TOKENS.get('CHAT_COMPLETION_API_KEY', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get(
'LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT',
Expand Down
71 changes: 66 additions & 5 deletions common/djangoapps/third_party_auth/admin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"""
Admin site configuration for third party authentication
"""

import csv

from config_models.admin import KeyedConfigurationModelAdmin
from django import forms
from django.contrib import admin
from django.contrib import admin, messages
from django.db import transaction
from django.urls import reverse
from django.http import Http404, HttpResponseRedirect
from django.urls import path, reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt

from .models import (
_PSA_OAUTH2_BACKENDS,
Expand All @@ -21,7 +23,7 @@
SAMLProviderConfig,
SAMLProviderData
)
from .tasks import fetch_saml_metadata
from .tasks import fetch_saml_metadata, update_saml_users_social_auth_uid


class OAuth2ProviderConfigForm(forms.ModelForm):
Expand Down Expand Up @@ -72,7 +74,7 @@ def get_list_display(self, request):
""" Don't show every single field in the admin change list """
return (
'name_with_update_link', 'enabled', 'site', 'entity_id', 'metadata_source',
'has_data', 'mode', 'saml_configuration', 'change_date', 'changed_by',
'has_data', 'mode', 'saml_configuration', 'change_date', 'changed_by', 'csv_uuid_update_button',
)

list_display_links = None
Expand Down Expand Up @@ -135,6 +137,65 @@ def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
fetch_saml_metadata.apply_async((), countdown=2)

def get_urls(self):
""" Extend the admin URLs to include the custom CSV upload URL. """
urls = super().get_urls()
custom_urls = [
path('<slug:slug>/upload-csv/', self.admin_site.admin_view(self.upload_csv), name='upload_csv'),

]
return custom_urls + urls

@csrf_exempt
def upload_csv(self, request, slug):
""" Handle CSV upload and update UserSocialAuth model. """
if not request.user.is_staff:
raise Http404
if request.method == 'POST':
csv_file = request.FILES.get('csv_file')
if not csv_file or not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a valid CSV file.", level=messages.ERROR)
else:
try:
decoded_file = csv_file.read().decode('utf-8').splitlines()
reader = csv.DictReader(decoded_file)
update_saml_users_social_auth_uid(reader, slug)
self.message_user(request, "CSV file has been processed successfully.")
except Exception as e: # pylint: disable=broad-except
self.message_user(request, f"Failed to process CSV file: {e}", level=messages.ERROR)

# Always redirect back to the SAMLProviderConfig listing page
return HttpResponseRedirect(reverse('admin:third_party_auth_samlproviderconfig_changelist'))

def change_view(self, request, object_slug, form_url='', extra_context=None):
""" Extend the change view to include CSV upload. """
extra_context = extra_context or {}
extra_context['show_csv_upload'] = True
return super().change_view(request, object_slug, form_url, extra_context)

def csv_uuid_update_button(self, obj):
""" Add CSV upload button to the form. """
if obj:
form_url = reverse('admin:upload_csv', args=[obj.slug])
return format_html(
'<form method="post" enctype="multipart/form-data" action="{}">'
'<input type="file" name="csv_file" accept=".csv" style="margin-bottom: 10px;">'
'<button type="submit" class="button">Upload CSV</button>'
'</form>',
form_url
)
return ""

csv_uuid_update_button.short_description = 'UUID UPDATE CSV'
csv_uuid_update_button.allow_tags = True

def get_readonly_fields(self, request, obj=None):
""" Conditionally add csv_uuid_update_button to readonly fields. """
readonly_fields = list(super().get_readonly_fields(request, obj))
if obj:
readonly_fields.append('csv_uuid_update_button')
return readonly_fields

admin.site.register(SAMLProviderConfig, SAMLProviderConfigAdmin)


Expand Down
15 changes: 10 additions & 5 deletions common/djangoapps/third_party_auth/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ def B(*args, **kwargs):
from openedx.core.djangoapps.user_api import accounts
from openedx.core.djangoapps.user_api.accounts.utils import username_suffix_generator
from openedx.core.djangoapps.user_authn import cookies as user_authn_cookies
from openedx.core.djangoapps.user_authn.toggles import is_auto_generated_username_enabled
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
from openedx.core.djangoapps.user_authn.views.utils import get_auto_generated_username
from common.djangoapps.third_party_auth.utils import (
get_associated_user_by_email_response,
get_user_from_email,
Expand Down Expand Up @@ -991,12 +993,15 @@ def get_username(strategy, details, backend, user=None, *args, **kwargs): # lin
else:
slug_func = lambda val: val

if email_as_username and details.get('email'):
username = details['email']
elif details.get('username'):
username = details['username']
if is_auto_generated_username_enabled():
username = get_auto_generated_username(details)
else:
username = uuid4().hex
if email_as_username and details.get('email'):
username = details['email']
elif details.get('username'):
username = details['username']
else:
username = uuid4().hex

input_username = username
final_username = slug_func(clean_func(username[:max_length]))
Expand Down
62 changes: 62 additions & 0 deletions common/djangoapps/third_party_auth/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import requests
from celery import shared_task
from django.core.exceptions import ObjectDoesNotExist
from edx_django_utils.monitoring import set_code_owner_attribute
from lxml import etree
from requests import exceptions
from social_django.models import UserSocialAuth

from common.djangoapps.third_party_auth.models import SAMLConfiguration, SAMLProviderConfig
from common.djangoapps.third_party_auth.utils import (
Expand Down Expand Up @@ -127,3 +129,63 @@ def fetch_saml_metadata():

# Return counts for total, skipped, attempted, updated, and failed, along with any failure messages
return num_total, num_skipped, num_attempted, num_updated, len(failure_messages), failure_messages


@shared_task
@set_code_owner_attribute
def update_saml_users_social_auth_uid(reader, slug):
"""
Update the UserSocialAuth UID for users based on a CSV reader input.
This function reads old and new UIDs from a CSV reader, fetches the corresponding
SAMLProviderConfig object using the provided slug, and updates the UserSocialAuth
records accordingly.
Args:
reader (csv.DictReader): A CSV reader object that iterates over rows containing 'old-uid' and 'new-uid'.
slug (str): The slug of the SAMLProviderConfig object to be fetched.
Returns:
None
"""
log_prefix = "UpdateSamlUsersAuthUID"
log.info(f"{log_prefix}: Updated user UID request received with slug: {slug}")

try:
# Fetching the SAMLProviderConfig object with slug
saml_provider_config = SAMLProviderConfig.objects.current_set().get(slug=slug)
except SAMLProviderConfig.DoesNotExist:
log.error(f"{log_prefix}: SAMLProviderConfig with slug {slug} does not exist")
return
except Exception as e: # pylint: disable=broad-except
log.error(f"{log_prefix}: An error occurred while fetching SAMLProviderConfig: {str(e)}")
return

success_count = 0
error_count = 0

for row in reader:
old_uid = row.get('old-uid')
new_uid = row.get('new-uid')

# Construct the UID using the SAML provider slug and old UID
uid = f'{saml_provider_config.slug}:{old_uid}'

try:
user_social_auth = UserSocialAuth.objects.get(uid=uid)
user_social_auth.uid = f'{saml_provider_config.slug}:{new_uid}'
user_social_auth.save()
log.info(f"{log_prefix}: Updated UID from {old_uid} to {new_uid} for user:{user_social_auth.user.id}.")
success_count += 1

except ObjectDoesNotExist:
log.error(f"{log_prefix}: UserSocialAuth with UID {uid} does not exist for old UID {old_uid}")
error_count += 1

except Exception as e: # pylint: disable=broad-except
log.error(f"{log_prefix}: An error occurred while updating UID for old UID {old_uid}"
f" to new UID {new_uid}: {str(e)}")
error_count += 1

log.info(f"{log_prefix}: Process completed for SAML configuration with slug: {slug}, {success_count} records"
f" successfully processed, {error_count} records encountered errors")
Binary file modified common/static/data/geoip/GeoLite2-Country.mmdb
Binary file not shown.
33 changes: 0 additions & 33 deletions lms/djangoapps/commerce/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,39 +134,6 @@ def setUp(self):
self.user = UserFactory()
self.base_url = get_ecommerce_api_base_url()

@httpretty.activate
def test_tracking_context(self):
"""
Ensure the tracking context is set up in the api client correctly and automatically.
"""
with freeze_time('2015-7-2'):
# fake an E-Commerce API request.
httpretty.register_uri(
httpretty.POST,
f"{settings.ECOMMERCE_API_URL.strip('/')}/baskets/1/",
status=200, body='{}',
adding_headers={'Content-Type': JSON}
)

mock_tracker = mock.Mock()
mock_tracker.resolve_context = mock.Mock(return_value={'ip': '127.0.0.1'})
with mock.patch('openedx.core.djangoapps.commerce.utils.tracker.get_tracker', return_value=mock_tracker):
api_url = urljoin(f"{self.base_url}/", "baskets/1/")
get_ecommerce_api_client(self.user).post(api_url)

# Verify the JWT includes the tracking context for the user
actual_header = httpretty.last_request().headers['Authorization']

claims = {
'tracking_context': {
'lms_user_id': self.user.id,
'lms_ip': '127.0.0.1',
}
}
expected_jwt = create_jwt_for_user(self.user, additional_claims=claims, scopes=self.SCOPES)
expected_header = f'JWT {expected_jwt}'
assert actual_header == expected_header

@httpretty.activate
def test_client_unicode(self):
"""
Expand Down
3 changes: 2 additions & 1 deletion lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4682,7 +4682,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
]

# Setting for Open API key and prompts used by edx-enterprise.
OPENAI_API_KEY = ''
CHAT_COMPLETION_API = 'https://example.com/chat/completion'
CHAT_COMPLETION_API_KEY = 'i am a key'
LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ''
LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ''
LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT = ''
Expand Down
3 changes: 2 additions & 1 deletion lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,8 @@ def get_env_setting(setting):
ENTERPRISE_CATALOG_INTERNAL_ROOT_URL
)

OPENAI_API_KEY = ENV_TOKENS.get('OPENAI_API_KEY', '')
CHAT_COMPLETION_API = ENV_TOKENS.get('CHAT_COMPLETION_API', '')
CHAT_COMPLETION_API_KEY = ENV_TOKENS.get('CHAT_COMPLETION_API_KEY', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get(
'LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT',
Expand Down
12 changes: 12 additions & 0 deletions lms/templates/video.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ <h4 class="hd hd-5">${_('How is the transcript quality ?')}</h4>
</div>
</div>
% endif
<div class="google-disclaimer">
<span className="text-dark-300 x-small">Powered by
<a href="https://translate.google.com/" target="_blank">
<img
width="100"
id="google-translate-logo"
src="https://learning.edx.org/d4ab1b25143ecad62d69d855b00e7313.png"
alt="Translated by Google logo"
>
</a>
</span>
</div>
</div>
</div>

Expand Down
Loading

0 comments on commit 43e6aa8

Please sign in to comment.