diff --git a/lms/djangoapps/survey/tests/factories.py b/lms/djangoapps/survey/tests/factories.py new file mode 100644 index 0000000000..40844c716c --- /dev/null +++ b/lms/djangoapps/survey/tests/factories.py @@ -0,0 +1,21 @@ +# pylint:disable=missing-docstring +import factory + +from student.tests.factories import UserFactory +from survey.models import SurveyAnswer, SurveyForm + + +class SurveyFormFactory(factory.DjangoModelFactory): + class Meta(object): + model = SurveyForm + + name = 'Test Survey Form' + form = '
' + + +class SurveyAnswerFactory(factory.DjangoModelFactory): + class Meta(object): + model = SurveyAnswer + + user = factory.SubFactory(UserFactory) + form = factory.SubFactory(SurveyFormFactory) diff --git a/openedx/core/djangoapps/credit/tests/factories.py b/openedx/core/djangoapps/credit/tests/factories.py index ae6aa8398d..a802ddea40 100644 --- a/openedx/core/djangoapps/credit/tests/factories.py +++ b/openedx/core/djangoapps/credit/tests/factories.py @@ -8,7 +8,14 @@ import factory from factory.fuzzy import FuzzyText import pytz -from openedx.core.djangoapps.credit.models import CreditProvider, CreditEligibility, CreditCourse, CreditRequest +from openedx.core.djangoapps.credit.models import ( + CreditProvider, + CreditEligibility, + CreditCourse, + CreditRequest, + CreditRequirement, + CreditRequirementStatus, +) from util.date_utils import to_timestamp @@ -20,6 +27,21 @@ class CreditCourseFactory(factory.DjangoModelFactory): enabled = True +class CreditRequirementFactory(factory.DjangoModelFactory): + class Meta(object): + model = CreditRequirement + + course = factory.SubFactory(CreditCourseFactory) + + +class CreditRequirementStatusFactory(factory.DjangoModelFactory): + class Meta(object): + model = CreditRequirementStatus + + requirement = factory.SubFactory(CreditRequirementFactory) + status = CreditRequirementStatus.REQUIREMENT_STATUS_CHOICES[0][0] + + class CreditProviderFactory(factory.DjangoModelFactory): class Meta(object): model = CreditProvider diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index a49946d2c5..aed515382a 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -28,6 +28,7 @@ from integrated_channels.sap_success_factors.models import ( ) import mock from nose.plugins.attrib import attr +from opaque_keys.edx.keys import CourseKey import pytest import pytz from rest_framework import status @@ -37,6 +38,8 @@ from social_django.models import UserSocialAuth from entitlements.models import CourseEntitlementSupportDetail from entitlements.tests.factories import CourseEntitlementFactory +from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory +from openedx.core.djangoapps.course_groups.models import CourseUserGroup, UnregisteredLearnerCohortAssignments from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KEY from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_MAILINGS @@ -45,6 +48,7 @@ from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms from openedx.core.lib.token_utils import JwtBuilder from student.models import ( + CourseEnrollmentAllowed, PendingEmailChange, SocialLink, UserProfile, @@ -54,12 +58,16 @@ from student.models import ( from student.tests.factories import ( TEST_PASSWORD, ContentTypeFactory, + CourseEnrollmentAllowedFactory, + PendingEmailChangeFactory, PermissionFactory, SuperuserFactory, UserFactory ) + from .. import ALL_USERS_VISIBILITY, PRIVATE_VISIBILITY from ..views import AccountRetirementView, USER_PROFILE_PII +from ...tests.factories import UserOrgTagFactory TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=pytz.UTC) @@ -1677,6 +1685,26 @@ class TestAccountRetirementPost(RetirementTestCase): comments='A comment containing potential PII.' ) + # Misc. setup + self.photo_verification = SoftwareSecurePhotoVerificationFactory.create(user=self.test_user) + PendingEmailChangeFactory.create(user=self.test_user) + UserOrgTagFactory.create(user=self.test_user, key='foo', value='bar') + UserOrgTagFactory.create(user=self.test_user, key='cat', value='dog') + + CourseEnrollmentAllowedFactory.create(email=self.original_email) + + self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course') + self.cohort = CourseUserGroup.objects.create( + name="TestCohort", + course_id=self.course_key, + group_type=CourseUserGroup.COHORT + ) + self.cohort_assignment = UnregisteredLearnerCohortAssignments.objects.create( + course_user_group=self.cohort, + course_id=self.course_key, + email=self.original_email + ) + # setup for doing POST from test client self.headers = self.build_jwt_headers(self.test_superuser) self.headers['content_type'] = "application/json" @@ -1771,6 +1799,13 @@ class TestAccountRetirementPost(RetirementTestCase): self._pending_enterprise_customer_user_assertions() self._entitlement_support_detail_assertions() + self._photo_verification_assertions() + self.assertFalse(PendingEmailChange.objects.filter(user=self.test_user).exists()) + self.assertFalse(UserOrgTag.objects.filter(user=self.test_user).exists()) + + self.assertFalse(CourseEnrollmentAllowed.objects.filter(email=self.original_email).exists()) + self.assertFalse(UnregisteredLearnerCohortAssignments.objects.filter(email=self.original_email).exists()) + def test_deletes_pii_from_user_profile(self): for model_field, value_to_assign in USER_PROFILE_PII.iteritems(): if value_to_assign == '': @@ -1866,3 +1901,12 @@ class TestAccountRetirementPost(RetirementTestCase): """ self.entitlement_support_detail.refresh_from_db() self.assertEqual('', self.entitlement_support_detail.comments) + + def _photo_verification_assertions(self): + """ + Helper method for asserting that ``SoftwareSecurePhotoVerification`` objects are retired. + """ + self.photo_verification.refresh_from_db() + self.assertEqual(self.test_user, self.photo_verification.user) + for field in ('name', 'face_image_url', 'photo_id_image_url', 'photo_id_key'): + self.assertEqual('', getattr(self.photo_verification, field)) diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index a8d786d602..1b8ca530e8 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -27,6 +27,8 @@ from six import text_type from social_django.models import UserSocialAuth from entitlements.models import CourseEntitlement +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification +from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments from openedx.core.djangoapps.profile_images.images import remove_profile_images from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in @@ -36,6 +38,8 @@ from openedx.core.lib.api.authentication import ( ) from openedx.core.lib.api.parsers import MergePatchParser from student.models import ( + CourseEnrollmentAllowed, + PendingEmailChange, User, UserProfile, get_potentially_retired_user_by_username, @@ -594,16 +598,31 @@ class AccountRetirementView(ViewSet): user = retirement_status.user retired_username = retirement_status.retired_username or get_retired_username_by_username(username) retired_email = retirement_status.retired_email or get_retired_email_by_email(user.email) + original_email = retirement_status.original_email + # Retire core user/profile information self.clear_pii_from_userprofile(user) self.delete_users_profile_images(user) self.delete_users_country_cache(user) + + # Retire data from Enterprise models self.retire_users_data_sharing_consent(username, retired_username) self.retire_sapsf_data_transmission(user) self.retire_user_from_pending_enterprise_customer_user(user, retired_email) self.retire_entitlement_support_detail(user) + + # Retire misc. models that may contain PII of this user + SoftwareSecurePhotoVerification.retire_user(user.id) + PendingEmailChange.delete_by_user_value(user, field='user') + UserOrgTag.delete_by_user_value(user, field='user') + + # Retire any objects linked to the user via their original email + CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email') + UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field='email') + # TODO: Password Reset links - https://openedx.atlassian.net/browse/PLAT-2104 # TODO: Delete OAuth2 records - https://openedx.atlassian.net/browse/EDUCATOR-2703 + user.first_name = '' user.last_name = '' user.is_active = False