Skip to content

Commit 97b897c

Browse files
authored
Merge branch 'master' into pwnage101/ENT-11563
2 parents 30924b9 + ba66946 commit 97b897c

36 files changed

Lines changed: 562 additions & 33 deletions

File tree

common/djangoapps/student/models/course_enrollment.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1597,7 +1597,9 @@ class CourseEnrollmentAllowed(DeletableByUserValue, models.Model):
15971597
the object is marked with the student who enrolled, to prevent students from changing e-mails and
15981598
enrolling many accounts through the same e-mail.
15991599
1600-
.. no_pii:
1600+
.. pii: Contains email, redacted then deleted in AccountRetirementView
1601+
.. pii_types: email_address
1602+
.. pii_retirement: local_api
16011603
"""
16021604
email = models.CharField(max_length=255, db_index=True)
16031605
course_id = CourseKeyField(max_length=255, db_index=True)
@@ -1646,6 +1648,13 @@ def may_enroll_and_unenrolled(cls, course_id):
16461648
"""
16471649
return CourseEnrollmentAllowed.objects.filter(course_id=course_id, user__isnull=True)
16481650

1651+
@classmethod
1652+
def redact_before_delete_fields(cls):
1653+
"""
1654+
Redact email before deleting records for downstream soft-delete systems.
1655+
"""
1656+
return {'email': 'redacted-before-delete@safe.com'}
1657+
16491658

16501659
class CourseEnrollmentAttribute(models.Model):
16511660
"""

common/djangoapps/student/models/user.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,14 +904,21 @@ class PendingEmailChange(DeletableByUserValue, models.Model): # noqa: DJ008
904904
"""
905905
This model keeps track of pending requested changes to a user's email address.
906906
907-
.. pii: Contains new_email, retired in AccountRetirementView
907+
.. pii: Contains new_email, redacted then deleted in AccountRetirementView
908908
.. pii_types: email_address
909909
.. pii_retirement: local_api
910910
"""
911911
user = models.OneToOneField(User, unique=True, db_index=True, on_delete=models.CASCADE)
912912
new_email = models.CharField(blank=True, max_length=255, db_index=True)
913913
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
914914

915+
@classmethod
916+
def redact_before_delete_fields(cls):
917+
"""
918+
Redact PII fields before delete in downstream soft-delete systems.
919+
"""
920+
return {'new_email': 'redacted-before-delete@safe.com'}
921+
915922
def request_change(self, email):
916923
"""Request a change to a user's email.
917924

common/djangoapps/student/tests/test_models.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from django.conf import settings
1111
from django.contrib.auth.models import AnonymousUser, User # pylint: disable=imported-auth-user
1212
from django.core.cache import cache
13+
from django.db import connection
1314
from django.db.models.functions import Lower
1415
from django.test import TestCase, override_settings
16+
from django.test.utils import CaptureQueriesContext
1517
from edx_toggles.toggles.testutils import override_waffle_flag
1618
from freezegun import freeze_time
1719
from opaque_keys.edx.keys import CourseKey
@@ -42,7 +44,7 @@
4244
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
4345
from openedx.core.djangoapps.schedules.models import Schedule
4446
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
45-
from openedx.core.djangolib.testing.utils import skip_unless_lms
47+
from openedx.core.djangolib.testing.utils import assert_redact_before_delete, skip_unless_lms
4648
from xmodule.modulestore import ModuleStoreEnum # pylint: disable=wrong-import-order
4749
from xmodule.modulestore.tests.django_utils import ( # pylint: disable=wrong-import-order
4850
ModuleStoreTestCase,
@@ -600,6 +602,22 @@ def test_delete_by_user_no_effect_for_user_with_no_email_change(self):
600602
assert not record_was_deleted
601603
assert 1 == len(PendingEmailChange.objects.all())
602604

605+
def test_delete_by_user_value_redacts_pending_email_before_deletion(self):
606+
"""
607+
Verify that delete_by_user_value redacts new_email before deletion.
608+
"""
609+
table = PendingEmailChange._meta.db_table
610+
with CaptureQueriesContext(connection) as ctx:
611+
record_was_deleted = PendingEmailChange.delete_by_user_value(self.user, field='user')
612+
613+
assert_redact_before_delete(
614+
[q['sql'] for q in ctx],
615+
table=table,
616+
expected_redacted_value_list=['redacted-before-delete@safe.com'],
617+
)
618+
assert record_was_deleted
619+
assert not PendingEmailChange.objects.filter(user=self.user).exists()
620+
603621

604622
class TestCourseEnrollmentAllowed(ModuleStoreTestCase): # pylint: disable=missing-class-docstring
605623

@@ -615,11 +633,18 @@ def setUp(self):
615633
)
616634

617635
def test_retiring_user_deletes_record(self):
618-
is_successful = CourseEnrollmentAllowed.delete_by_user_value(
619-
value=self.email,
620-
field='email'
621-
)
636+
with CaptureQueriesContext(connection) as ctx:
637+
is_successful = CourseEnrollmentAllowed.delete_by_user_value(
638+
value=self.email,
639+
field='email'
640+
)
641+
622642
assert is_successful
643+
assert_redact_before_delete(
644+
[q['sql'] for q in ctx],
645+
table=CourseEnrollmentAllowed._meta.db_table,
646+
expected_redacted_value_list=['redacted-before-delete@safe.com'],
647+
)
623648
user_search_results = CourseEnrollmentAllowed.objects.filter(
624649
email=self.email
625650
)

common/djangoapps/student/views/management.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -963,14 +963,17 @@ def confirm_email_change(request, key):
963963

964964
user.email = pec.new_email
965965
user.save()
966-
pec.delete()
966+
967+
# Redact pending email before deletion.
968+
PendingEmailChange.delete_by_user_value(user, field="user")
969+
967970
# And send it to the new email...
968-
msg.recipient = Recipient(user.id, pec.new_email)
971+
msg.recipient = Recipient(user.id, user.email)
969972
try:
970973
ace.send(msg)
971974
except Exception: # pylint: disable=broad-except
972975
log.warning('Unable to send confirmation email to new address', exc_info=True)
973-
response = render_to_response("email_change_failed.html", {'email': pec.new_email})
976+
response = render_to_response("email_change_failed.html", {'email': user.email})
974977
transaction.set_rollback(True)
975978
return response
976979

common/djangoapps/util/tests/test_filters.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from django.test import override_settings
55
from openedx_filters import PipelineStep
6+
from openedx_filters.learning.filters import InstructorDashboardTabsRequested
67

78
from common.djangoapps.util import course
89
from openedx.core.djangolib.testing.utils import skip_unless_lms
@@ -76,3 +77,53 @@ def test_course_about_page_url_requested_without_filter_configuration(self):
7677
)
7778

7879
self.assertEqual(expected_course_about, course_about_url) # noqa: PT009
80+
81+
82+
class TestInstructorDashCustomTab(PipelineStep):
83+
"""
84+
Utility class used when getting steps for pipeline.
85+
"""
86+
87+
def run_filter(self, tabs, user, course_key): # pylint: disable=arguments-differ,unused-argument
88+
"""Pipeline step that appends a custom instructor dashboard tab."""
89+
result = {
90+
"tabs": tabs + [{
91+
"tab_id": "custom",
92+
"title": "Custom Tab",
93+
"url": f"/courses/{course_key}/instructor/custom",
94+
"sort_order": 999,
95+
}],
96+
}
97+
return result
98+
99+
100+
class TestPreventTabsGenerationWithTabs(PipelineStep):
101+
"""
102+
Pipeline step that raises PreventTabsGeneration with a custom tabs list.
103+
Used to test that the exception handler in get_tabs uses exc.tabs when present.
104+
"""
105+
106+
def run_filter(self, tabs, user, course_key): # pylint: disable=arguments-differ,unused-argument
107+
"""Pipeline step that raises PreventTabsGeneration with custom tabs."""
108+
raise InstructorDashboardTabsRequested.PreventTabsGeneration(
109+
"Preventing default tabs in favor of custom ones.",
110+
tabs=[{
111+
"tab_id": "plugin_tab",
112+
"title": "Plugin Tab",
113+
"url": f"/courses/{course_key}/instructor/plugin",
114+
"sort_order": 5,
115+
}],
116+
)
117+
118+
119+
class TestPreventTabsGenerationWithoutTabs(PipelineStep):
120+
"""
121+
Pipeline step that raises PreventTabsGeneration without a tabs list.
122+
Used to test that the exception handler in get_tabs falls back to an empty list.
123+
"""
124+
125+
def run_filter(self, tabs, user, course_key): # pylint: disable=arguments-differ,unused-argument
126+
"""Pipeline step that raises PreventTabsGeneration without providing tabs."""
127+
raise InstructorDashboardTabsRequested.PreventTabsGeneration(
128+
"Preventing all tabs from being generated."
129+
)
-131 KB
Binary file not shown.

docs/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ references/docstrings/common
55
references/docstrings/lms
66
references/docstrings/openedx
77
references/docstrings/xmodule
8+
apps/index.rst
9+
decisions/app_decisions.rst

docs/conf.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,10 @@ def on_init(app): # pylint: disable=redefined-outer-name, unused-argument
338338
avoid checking in the generated reStructuredText files.
339339
"""
340340
repo_docs_build_path = f'{root}/docs/references/docs'
341-
RepositoryDocs(root, repo_docs_build_path).build_rst_docs()
341+
repo_docs = RepositoryDocs(root, repo_docs_build_path)
342+
repo_docs.build_rst_docs()
343+
repo_docs.build_apps_index(root / 'docs' / 'apps' / 'index.rst')
344+
repo_docs.build_decisions_index(root / 'docs' / 'decisions' / 'app_decisions.rst')
342345

343346
docs_path = root / 'docs'
344347
apidoc_path = 'sphinx-apidoc'

docs/decisions/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
.. toctree::
44
0*
5+
app_decisions

docs/index.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ locations.
3535
concepts/index
3636
extensions/tinymce_plugins
3737
decisions/index
38+
apps/index
3839

3940
.. grid:: 1 2 2 2
4041
:gutter: 3
@@ -91,6 +92,18 @@ locations.
9192

9293
Hooks Extensions Framework
9394

95+
.. grid-item-card:: App Documentation
96+
:class-card: sd-shadow-md sd-p-2
97+
:class-footer: sd-border-0
98+
99+
* :doc:`apps/index`
100+
* :doc:`decisions/app_decisions`
101+
+++
102+
.. button-ref:: apps/index
103+
:color: primary
104+
:outline:
105+
:expand:
106+
94107

95108
Change History
96109
**************

0 commit comments

Comments
 (0)