Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge in previous release changes for BigCommerce from Feature.nutmeg/bigcommerce #217

Merged
merged 5 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pylint-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
include:
- module-name: lms-1
path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/ lms/djangoapps/save_for_later/"
path: "lms/djangoapps/badges/ lms/djangoapps/save_for_later/ lms/djangoapps/bigcommerce_app/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/"
- module-name: lms-2
path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py"
- module-name: openedx-1
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/unit-test-shards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"settings": "lms.envs.test",
"paths": [
"lms/djangoapps/badges/",
"lms/djangoapps/bigcommerce_app/",
"lms/djangoapps/branding/",
"lms/djangoapps/bulk_email/",
"lms/djangoapps/bulk_enroll/",
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# and so should not be destroyed by "make clean".
# start-noclean
requirements/private.txt
requirements/edx/private.in
requirements/edx/private.txt
# requirements/edx/private.in
# requirements/edx/private.txt
lms/envs/private.py
cms/envs/private.py
# end-noclean
Expand Down
4 changes: 4 additions & 0 deletions common/djangoapps/student/views/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.experiments.utils import get_dashboard_course_info, get_experiment_user_metadata_context
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.bigcommerce_app.utils import BigCommerceAPI
from openedx.core.djangoapps.catalog.utils import (
get_programs,
get_pseudo_session_for_entitlement,
Expand Down Expand Up @@ -167,6 +168,9 @@ def get_course_enrollments(user, org_whitelist, org_blacklist, course_limit=None
generator[CourseEnrollment]: a sequence of enrollments to be displayed
on the user's dashboard.
"""

BigCommerceAPI.get_bc_course_enrollments(user)

for enrollment in CourseEnrollment.enrollments_for_user_with_overviews_preload(user, course_limit):

# If the course is missing or broken, log an error and skip it.
Expand Down
21 changes: 21 additions & 0 deletions common/djangoapps/third_party_auth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from django.utils.translation import gettext_lazy as _

from .models import (
_PSA_EMAIL_BACKENDS,
_PSA_OAUTH2_BACKENDS,
_PSA_SAML_BACKENDS,
EmailProviderConfig,
LTIProviderConfig,
OAuth2ProviderConfig,
SAMLConfiguration,
Expand All @@ -23,6 +25,25 @@
from .tasks import fetch_saml_metadata


class EmailProviderConfigForm(forms.ModelForm):
""" Django Admin form class for EmailProviderConfig """
backend_name = forms.ChoiceField(choices=((name, name) for name in _PSA_EMAIL_BACKENDS))


class EmailProviderConfigAdmin(KeyedConfigurationModelAdmin):
""" Django Admin class for EmailProviderConfig """
form = EmailProviderConfigForm

def get_list_display(self, request):
""" Don't show every single field in the admin change list """
return (
'name', 'enabled', 'slug', 'site', 'backend_name', 'secondary', 'skip_registration_form',
'skip_email_verification', 'change_date', 'changed_by', 'edit_link',
)

admin.site.register(EmailProviderConfig, EmailProviderConfigAdmin)


class OAuth2ProviderConfigForm(forms.ModelForm):
""" Django Admin form class for OAuth2ProviderConfig """
backend_name = forms.ChoiceField(choices=((name, name) for name in _PSA_OAUTH2_BACKENDS))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 3.2.10 on 2022-01-14 20:50

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('sites', '0002_alter_domain_unique'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('organizations', '0003_historicalorganizationcourse'),
('third_party_auth', '0005_auto_20210723_1527'),
]

operations = [
migrations.CreateModel(
name='EmailProviderConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('icon_class', models.CharField(blank=True, default='fa-sign-in', help_text='The Font Awesome (or custom) icon class to use on the login button for this provider. Examples: fa-google-plus, fa-facebook, fa-linkedin, fa-sign-in, fa-university', max_length=50)),
('icon_image', models.FileField(blank=True, help_text='If there is no Font Awesome icon available for this provider, upload a custom image. SVG images are recommended as they can scale to any size.', upload_to='')),
('name', models.CharField(help_text='Name of this provider (shown to users)', max_length=50)),
('slug', models.SlugField(default='default', help_text='A short string uniquely identifying this provider. Cannot contain spaces and should be a usable as a CSS class. Examples: "ubc", "mit-staging"', max_length=30)),
('secondary', models.BooleanField(default=False, help_text='Secondary providers are displayed less prominently, in a separate list of "Institution" login providers.')),
('skip_hinted_login_dialog', models.BooleanField(default=False, help_text='If this option is enabled, users that visit a "TPA hinted" URL for this provider (e.g. a URL ending with `?tpa_hint=[provider_name]`) will be forwarded directly to the login URL of the provider instead of being first prompted with a login dialog.')),
('skip_registration_form', models.BooleanField(default=False, help_text='If this option is enabled, users will not be asked to confirm their details (name, email, etc.) during the registration process. Only select this option for trusted providers that are known to provide accurate user information.')),
('skip_email_verification', models.BooleanField(default=False, help_text='If this option is selected, users will not be required to confirm their email, and their account will be activated immediately upon registration.')),
('send_welcome_email', models.BooleanField(default=False, help_text='If this option is selected, users will be sent a welcome email upon registration.')),
('visible', models.BooleanField(default=False, help_text='If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.')),
('max_session_length', models.PositiveIntegerField(blank=True, default=None, help_text='If this option is set, then users logging in using this SSO provider will have their session length limited to no longer than this value. If set to 0 (zero), the session will expire upon the user closing their browser. If left blank, the Django platform session default length will be used.', null=True, verbose_name='Max session length (seconds)')),
('send_to_registration_first', models.BooleanField(default=False, help_text='If this option is selected, users will be directed to the registration page immediately after authenticating with the third party instead of the login page.')),
('sync_learner_profile_data', models.BooleanField(default=False, help_text='Synchronize user profile data received from the identity provider with the edX user account on each SSO login. The user will be notified if the email address associated with their account is changed as a part of this synchronization.')),
('enable_sso_id_verification', models.BooleanField(default=False, help_text='Use the presence of a profile from a trusted third party as proof of identity verification.')),
('disable_for_enterprise_sso', models.BooleanField(default=False, help_text='IDPs with this set to True will be excluded from the dropdown IDP selection in the EnterpriseCustomer Django Admin form.', verbose_name='Disabled for Enterprise TPA')),
('backend_name', models.CharField(db_index=True, help_text='Which python-social-auth Email provider backend to use. The list of backend choices is determined by the THIRD_PARTY_AUTH_BACKENDS setting.', max_length=50)),
('other_settings', models.TextField(blank=True, help_text='Optional JSON object with advanced settings, if any.')),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
('organization', models.ForeignKey(blank=True, help_text='optional. If this provider is an Organization, this attribute can be used reference users in that Organization', null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.organization')),
('site', models.ForeignKey(default=1, help_text='The Site that this provider configuration belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='emailproviderconfigs', to='sites.site')),
],
options={
'verbose_name': 'Provider Configuration (Email)',
'verbose_name_plural': 'Provider Configuration (Email)',
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.10 on 2022-01-21 19:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0006_emailproviderconfig'),
]

operations = [
migrations.AddField(
model_name='emailproviderconfig',
name='key',
field=models.TextField(blank=True, verbose_name='Client ID'),
),
migrations.AddField(
model_name='emailproviderconfig',
name='secret',
field=models.TextField(blank=True, help_text='For increased security, you can avoid storing this in your database by leaving this field blank and setting SOCIAL_AUTH_OAUTH_SECRETS = {"(backend name)": "secret", ...} in your instance\'s Django settings (or lms.auth.json)', verbose_name='Client Secret'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.2.16 on 2024-01-03 15:32

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0007_auto_20220121_1956'),
('third_party_auth', '0008_auto_20220324_1422'),
]

operations = [
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2024-01-03 15:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0009_merge_0007_auto_20220121_1956_0008_auto_20220324_1422'),
]

operations = [
migrations.AddField(
model_name='emailproviderconfig',
name='was_valid_at',
field=models.DateTimeField(blank=True, help_text='Timestamped field that indicates a user has successfully logged in using this configuration at least once.', null=True),
),
migrations.AlterField(
model_name='emailproviderconfig',
name='name',
field=models.CharField(blank=True, help_text='Name of this provider (shown to users)', max_length=50),
),
]
91 changes: 91 additions & 0 deletions common/djangoapps/third_party_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
from django.utils.translation import gettext_lazy as _
from organizations.models import Organization
from social_core.backends.base import BaseAuth
from social_core.backends.email import EmailAuth
from social_core.backends.oauth import OAuthAuth
from social_core.backends.saml import SAMLAuth
from social_core.exceptions import SocialAuthBaseException
from social_core.utils import module_member

from lms.djangoapps.bigcommerce_app.utils import access_token, BigCommerceAPI, internal_server_error

from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_current_request
from openedx.core.djangoapps.user_api.accounts import USERNAME_MAX_LENGTH
Expand All @@ -48,6 +51,7 @@ def _load_backend_classes(base_class=BaseAuth):
if issubclass(auth_class, base_class):
yield auth_class
_PSA_BACKENDS = {backend_class.name: backend_class for backend_class in _load_backend_classes()}
_PSA_EMAIL_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(EmailAuth)]
_PSA_OAUTH2_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(OAuthAuth)]
_PSA_SAML_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(SAMLAuth)]
_LTI_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(LTIAuthBackend)]
Expand Down Expand Up @@ -354,6 +358,93 @@ def enabled_for_current_site(self):
return self.enabled and self.site_id == Site.objects.get_current(get_current_request()).id


class EmailProviderConfig(ProviderConfig):
"""
Configuration Entry for an Email based provider.

.. no_pii:
"""
# We are keying the provider config by backend_name here as suggested in the python social
# auth documentation. In order to reuse a backend for a second provider, a subclass can be
# created with seperate name.
# example:
# class SecondEmailProvider(EmailAuth):
# name = "second-email-provider"
KEY_FIELDS = ('backend_name',)
prefix = 'email'
backend_name = models.CharField(
max_length=50, blank=False, db_index=True,
help_text=(
"Which python-social-auth Email provider backend to use. "
"The list of backend choices is determined by the THIRD_PARTY_AUTH_BACKENDS setting."
# To be precise, it's set by AUTHENTICATION_BACKENDS
# which production.py sets from THIRD_PARTY_AUTH_BACKENDS
)
)
key = models.TextField(blank=True, verbose_name="Client ID")
secret = models.TextField(
blank=True,
verbose_name="Client Secret",
help_text=(
'For increased security, you can avoid storing this in your database by leaving '
' this field blank and setting '
'SOCIAL_AUTH_OAUTH_SECRETS = {"(backend name)": "secret", ...} '
'in your instance\'s Django settings (or lms.auth.json)'
)
)
other_settings = models.TextField(blank=True, help_text="Optional JSON object with advanced settings, if any.")

class Meta(object):
app_label = "third_party_auth"
verbose_name = "Provider Configuration (Email)"
verbose_name_plural = verbose_name

def clean(self):
""" Standardize and validate fields """
super().clean()
self.other_settings = clean_json(self.other_settings, dict)

def get_setting(self, name):
""" Get the value of a setting, or raise KeyError """
if name == "KEY":
return self.key
if name == "SECRET":
if self.secret:
return self.secret
# To allow instances to avoid storing secrets in the DB, the secret can also be set via Django:
return getattr(settings, 'SOCIAL_AUTH_OAUTH_SECRETS', {}).get(self.backend_name, '')
if name == "ACCESS_TOKEN":
return access_token()
# if name == "BIGCOMMERCE_CUSTOMER_METADATA":
# import pdb;pdb.set_trace()
# return BigCommerceAPI.bigcommerce_customer_metadata()
if self.other_settings:
other_settings = json.loads(self.other_settings)
assert isinstance(other_settings, dict), "other_settings should be a JSON object (dictionary)"
return other_settings[name]
raise KeyError

def bigcommerce_retrieve_and_store_customer(self, payload):
""" Get the decoded payload user_data """
try:
return BigCommerceAPI.bigcommerce_customer_save(payload)
except Exception as excep: # pylint: disable=broad-except
log.error(
"Error decoding Customer payload token and storing in database."
)
return internal_server_error(excep)

def bigcommerce_save_store_customer_platform_user(self, payload):
""" Store the mapping of BigCommerce uid and platform user. """
try:
return BigCommerceAPI.bigcommerce_store_customer_platform_user_save(payload)
except Exception as excep: # pylint: disable=broad-except
log.error(
"Error decoding Customer payload token and storing in database."
)
return internal_server_error(excep)


class OAuth2ProviderConfig(ProviderConfig):
"""
Configuration Entry for an OAuth2 based provider.
Expand Down
13 changes: 13 additions & 0 deletions common/djangoapps/third_party_auth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

from common.djangoapps.third_party_auth.models import (
_LTI_BACKENDS,
_PSA_EMAIL_BACKENDS,
_PSA_OAUTH2_BACKENDS,
_PSA_SAML_BACKENDS,
EmailProviderConfig,
LTIProviderConfig,
OAuth2ProviderConfig,
SAMLConfiguration,
Expand All @@ -30,6 +32,11 @@ def _enabled_providers(cls):
Helper method that returns a generator used to iterate over all providers
of the current site.
"""
email_backend_names = EmailProviderConfig.key_values('backend_name', flat=True)
for email_backend_name in email_backend_names:
provider = EmailProviderConfig.current(email_backend_name)
if provider.enabled_for_current_site and provider.backend_name in _PSA_EMAIL_BACKENDS:
yield provider
oauth2_backend_names = OAuth2ProviderConfig.key_values('backend_name', flat=True)
for oauth2_backend_name in oauth2_backend_names:
provider = OAuth2ProviderConfig.current(oauth2_backend_name)
Expand Down Expand Up @@ -113,6 +120,12 @@ def get_enabled_by_backend_name(cls, backend_name):
Yields:
Instances of ProviderConfig.
"""
if backend_name in _PSA_EMAIL_BACKENDS:
email_backend_names = EmailProviderConfig.key_values('backend_name', flat=True)
for email_backend_name in email_backend_names:
provider = EmailProviderConfig.current(email_backend_name)
if provider.backend_name == backend_name and provider.enabled_for_current_site:
yield provider
if backend_name in _PSA_OAUTH2_BACKENDS:
oauth2_backend_names = OAuth2ProviderConfig.key_values('backend_name', flat=True)
for oauth2_backend_name in oauth2_backend_names:
Expand Down
Loading
Loading