Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
e0d authored Sep 24, 2024
2 parents 745fe74 + 649bd42 commit f3f77d4
Show file tree
Hide file tree
Showing 64 changed files with 686 additions and 169 deletions.
4 changes: 3 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lms/djangoapps/instructor_task/
lms/djangoapps/mobile_api/
openedx/core/djangoapps/credentials @openedx/2U-aperture
openedx/core/djangoapps/credit @openedx/2U-aperture
openedx/core/djangoapps/enrollments/ @openedx/2U-aperture
openedx/core/djangoapps/heartbeat/
openedx/core/djangoapps/oauth_dispatch
openedx/core/djangoapps/user_api/ @openedx/2U-aperture
Expand All @@ -37,8 +38,9 @@ lms/djangoapps/certificates/ @openedx/2U-
# Discovery
common/djangoapps/course_modes/
common/djangoapps/enrollment/
lms/djangoapps/branding/ @openedx/2U-aperture
lms/djangoapps/commerce/
lms/djangoapps/experiments/
lms/djangoapps/experiments/ @openedx/2U-aperture
lms/djangoapps/learner_dashboard/ @openedx/2U-aperture
lms/djangoapps/learner_home/ @openedx/2U-aperture
openedx/features/content_type_gating/
Expand Down
33 changes: 31 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,47 @@ sites)::
./manage.py lms collectstatic
./manage.py cms collectstatic

Set up CMS SSO (for Development)::

./manage.py lms manage_user studio_worker [email protected] --unusable-password
# DO NOT DO THIS IN PRODUCTION. It will make your auth insecure.
./manage.py lms create_dot_application studio-sso-id studio_worker \
--grant-type authorization-code \
--skip-authorization \
--redirect-uris 'http://localhost:18010/complete/edx-oauth2/' \
--scopes user_id \
--client-id 'studio-sso-id' \
--client-secret 'studio-sso-secret'

Set up CMS SSO (for Production):

* Create the CMS user and the OAuth application::

./manage.py lms manage_user studio_worker <[email protected]> --unusable-password
./manage.py lms create_dot_application studio-sso-id studio_worker \
--grant-type authorization-code \
--skip-authorization \
--redirect-uris 'http://localhost:18010/complete/edx-oauth2/' \
--scopes user_id

* Log into Django admin (eg. http://localhost:18000/admin/oauth2_provider/application/),
click into the application you created above (``studio-sso-id``), and copy its "Client secret".
* In your private LMS_CFG yaml file or your private Django settings module:

* Set ``SOCIAL_AUTH_EDX_OAUTH2_KEY`` to the client ID (``studio-sso-id``).
* Set ``SOCIAL_AUTH_EDX_OAUTH2_SECRET`` to the client secret (which you copied).
Run the Platform
----------------

First, ensure MySQL, Mongo, and Memcached are running.

Start the LMS::

./manage.py lms runserver
./manage.py lms runserver 18000

Start the CMS::

./manage.py cms runserver
./manage.py cms runserver 18010

This will give you a mostly-headless Open edX platform. Most frontends have
been migrated to "Micro-Frontends (MFEs)" which need to be installed and run
Expand Down
3 changes: 2 additions & 1 deletion cms/envs/devstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing
################ Using LMS SSO for login to Studio ################
SOCIAL_AUTH_EDX_OAUTH2_KEY = 'studio-sso-key'
SOCIAL_AUTH_EDX_OAUTH2_SECRET = 'studio-sso-secret' # in stage, prod would be high-entropy secret
SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = 'http://edx.devstack.lms:18000' # routed internally server-to-server
# routed internally server-to-server
SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = ENV_TOKENS.get('SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT', 'http://edx.devstack.lms:18000')
SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = 'http://localhost:18000' # used in browser redirect

# Don't form the return redirect URL with HTTPS on devstack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def test_verification_signal(self):
"""
Verification signal is sent upon approval.
"""
with mock.patch('openedx.core.djangoapps.signals.signals.LEARNER_NOW_VERIFIED.send_robust') as mock_signal:
with mock.patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') as mock_signal:
# Begin the pipeline.
pipeline.set_id_verification_status(
auth_entry=pipeline.AUTH_ENTRY_LOGIN,
Expand Down
30 changes: 13 additions & 17 deletions lms/djangoapps/bulk_email/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Signal handlers for the bulk_email app
"""
from django.contrib.auth import get_user_model
from django.dispatch import receiver
from eventtracking import tracker

Expand Down Expand Up @@ -32,29 +31,26 @@ def ace_email_sent_handler(sender, **kwargs):
"""
When an email is sent using ACE, this method will create an event to detect ace email success status
"""
# Fetch the message object from kwargs, defaulting to None if not present
message = kwargs.get('message', None)

user_model = get_user_model()
try:
user_id = user_model.objects.get(email=message.recipient.email_address).id
except user_model.DoesNotExist:
user_id = None
course_email = message.context.get('course_email', None)
course_id = message.context.get('course_id')
# Fetch the message dictionary from kwargs, defaulting to {} if not present
message = kwargs.get('message', {})
recipient = message.get('recipient', {})
message_name = message.get('name', None)
context = message.get('context', {})
email_address = recipient.get('email', None)
user_id = recipient.get('user_id', None)
channel = message.get('channel', None)
course_id = context.get('course_id', None)
if not course_id:
course_email = context.get('course_email', None)
course_id = course_email.course_id if course_email else None
try:
channel = sender.__class__.__name__
except AttributeError:
channel = 'Other'

tracker.emit(
'edx.ace.message_sent',
{
'message_type': message.name,
'message_type': message_name,
'channel': channel,
'course_id': course_id,
'user_id': user_id,
'user_email': message.recipient.email_address,
'user_email': email_address,
}
)
1 change: 1 addition & 0 deletions lms/djangoapps/bulk_email/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ def _send_course_email(entry_id, email_id, to_list, global_email_context, subtas
'course_id': str(course_email.course_id),
'to_list': [user_obj.get('email', '') for user_obj in to_list],
'total_recipients': total_recipients,
'ace_enabled_for_bulk_email': is_bulk_email_edx_ace_enabled(),
}
)
# Exclude optouts (if not a retry):
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/ccx/api/v0/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,8 @@ def make_ccx(self, max_students_allowed=200):
course_id=ccx_course_key,
student_email=self.coach.email,
auto_enroll=True,
email_students=False,
email_params=email_params,
message_students=False,
message_params=email_params,
)
return ccx

Expand Down
8 changes: 4 additions & 4 deletions lms/djangoapps/ccx/api/v0/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,8 @@ def post(self, request):
course_id=ccx_course_key,
student_email=coach.email,
auto_enroll=True,
email_students=True,
email_params=email_params,
message_students=True,
message_params=email_params,
)
# assign staff role for the coach to the newly created ccx
assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
Expand Down Expand Up @@ -768,8 +768,8 @@ def patch(self, request, ccx_course_id=None):
course_id=ccx_course_key,
student_email=coach.email,
auto_enroll=True,
email_students=True,
email_params=email_params,
message_students=True,
message_params=email_params,
)
# make the new coach staff on the CCX
assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
Expand Down
26 changes: 16 additions & 10 deletions lms/djangoapps/ccx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
log.info("%s", error)
errors.append(error)
break
enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params)
enroll_email(
course_key,
email,
auto_enroll=True,
message_students=email_students,
message_params=email_params
)
elif action == 'Unenroll' or action == 'revoke': # lint-amnesty, pylint: disable=consider-using-in
for identifier in identifiers:
try:
Expand All @@ -278,7 +284,7 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
log.info("%s", exp)
errors.append(f"{exp}")
continue
unenroll_email(course_key, email, email_students=email_students, email_params=email_params)
unenroll_email(course_key, email, message_students=email_students, message_params=email_params)
return errors


Expand Down Expand Up @@ -348,8 +354,8 @@ def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_em
course_id=ccx_key,
student_email=staff.email,
auto_enroll=True,
email_students=send_email,
email_params=email_params,
message_students=send_email,
message_params=email_params,
)

# allow 'staff' access on ccx to staff of master course
Expand All @@ -373,8 +379,8 @@ def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_em
course_id=ccx_key,
student_email=instructor.email,
auto_enroll=True,
email_students=send_email,
email_params=email_params,
message_students=send_email,
message_params=email_params,
)

# allow 'instructor' access on ccx to instructor of master course
Expand Down Expand Up @@ -417,8 +423,8 @@ def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, se
unenroll_email(
course_id=ccx_key,
student_email=staff.email,
email_students=send_email,
email_params=email_params,
message_students=send_email,
message_params=email_params,
)

for instructor in list_instructor:
Expand All @@ -430,6 +436,6 @@ def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, se
unenroll_email(
course_id=ccx_key,
student_email=instructor.email,
email_students=send_email,
email_params=email_params,
message_students=send_email,
message_params=email_params,
)
4 changes: 2 additions & 2 deletions lms/djangoapps/ccx/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ def create_ccx(request, course, ccx=None):
course_id=ccx_id,
student_email=request.user.email,
auto_enroll=True,
email_students=True,
email_params=email_params,
message_students=True,
message_params=email_params,
)

assign_staff_role_to_ccx(ccx_id, request.user, course.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ workspace {
}

grades_app -> signal_handlers "Emits COURSE_GRADE_NOW_PASSED signal"
verify_student_app -> signal_handlers "Emits LEARNER_NOW_VERIFIED signal"
verify_student_app -> signal_handlers "Emits IDV_ATTEMPT_APPROVED signal"
student_app -> signal_handlers "Emits ENROLLMENT_TRACK_UPDATED signal"
allowlist -> signal_handlers "Emits APPEND_CERTIFICATE_ALLOWLIST signal"
signal_handlers -> generation_handler "Invokes generate_allowlist_certificate()"
Expand Down
10 changes: 6 additions & 4 deletions lms/djangoapps/certificates/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@
from openedx.core.djangoapps.signals.signals import (
COURSE_GRADE_NOW_FAILED,
COURSE_GRADE_NOW_PASSED,
LEARNER_NOW_VERIFIED
)
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, IDV_ATTEMPT_APPROVED

User = get_user_model()

Expand Down Expand Up @@ -118,14 +117,17 @@ def _listen_for_failing_grade(sender, user, course_id, grade, **kwargs): # pyli
log.info(f'Certificate marked not passing for {user.id} : {course_id} via failing grade')


@receiver(LEARNER_NOW_VERIFIED, dispatch_uid="learner_track_changed")
def _listen_for_id_verification_status_changed(sender, user, **kwargs): # pylint: disable=unused-argument
@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="learner_track_changed")
def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pylint: disable=unused-argument
"""
Listen for a signal indicating that the user's id verification status has changed.
"""
if not auto_certificate_generation_enabled():
return

event_data = kwargs.get('idv_attempt')
user = User.objects.get(id=event_data.user.id)

user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
expected_verification_status = IDVerificationService.user_status(user)
expected_verification_status = expected_verification_status['status']
Expand Down
19 changes: 12 additions & 7 deletions lms/djangoapps/certificates/tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@
from openedx_events.data import EventsMetadata
from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx_events.tests.utils import OpenEdxEventsTestMixin

from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.certificates.api import has_self_generated_certificates_enabled
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
GeneratedCertificate
)
from lms.djangoapps.certificates.models import CertificateGenerationConfiguration, GeneratedCertificate
from lms.djangoapps.certificates.signals import handle_exam_attempt_rejected_event
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory


class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
Expand Down Expand Up @@ -302,10 +300,17 @@ def test_failing_grade_allowlist(self):
assert cert.status == CertificateStatuses.downloadable


class LearnerIdVerificationTest(ModuleStoreTestCase):
class LearnerIdVerificationTest(ModuleStoreTestCase, OpenEdxEventsTestMixin):
"""
Tests for certificate generation task firing on learner id verification
"""
ENABLED_OPENEDX_EVENTS = ['org.openedx.learning.idv_attempt.approved.v1']

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.start_events_isolation()

def setUp(self):
super().setUp()
self.course_one = CourseFactory.create(self_paced=True)
Expand Down
14 changes: 14 additions & 0 deletions lms/djangoapps/discussion/rest_api/discussions_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,18 @@ def clean_thread_html_body(html_body):
for match in html_body.find_all(tag):
match.unwrap()

# Replace tags that are not allowed in email
tags_to_update = [
{"source": "button", "target": "span"},
{"source": "h1", "target": "h4"},
{"source": "h2", "target": "h4"},
{"source": "h3", "target": "h4"},
]
for tag_dict in tags_to_update:
for source_tag in html_body.find_all(tag_dict['source']):
target_tag = html_body.new_tag(tag_dict['target'], **source_tag.attrs)
if source_tag.string:
target_tag.string = source_tag.string
source_tag.replace_with(target_tag)

return str(html_body)
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,29 @@ def test_only_script_tag(self):

result = clean_thread_html_body(html_body)
self.assertEqual(result.strip(), expected_output)

def test_button_tag_replace(self):
"""
Tests that the clean_thread_html_body function replaces the button tag with span tag
"""
# Tests for button replacement tag with text
html_body = '<button class="abc">Button</button>'
expected_output = '<span class="abc">Button</span>'
result = clean_thread_html_body(html_body)
self.assertEqual(result, expected_output)

# Tests button tag replacement without text
html_body = '<button class="abc"></button>'
expected_output = '<span class="abc"></span>'
result = clean_thread_html_body(html_body)
self.assertEqual(result, expected_output)

def test_heading_tag_replace(self):
"""
Tests that the clean_thread_html_body function replaces the h1, h2 and h3 tags with h4 tag
"""
for tag in ['h1', 'h2', 'h3']:
html_body = f'<{tag}>Heading</{tag}>'
expected_output = '<h4>Heading</h4>'
result = clean_thread_html_body(html_body)
self.assertEqual(result, expected_output)
Loading

0 comments on commit f3f77d4

Please sign in to comment.