feat: VAN-966 - Added a management command to back populate 'marketing_emails_opt_in' UserAttribute
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
""" Management command to back-populate marketing emails opt-in for the user accounts. """
|
||||
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import IntegrityError
|
||||
|
||||
from common.djangoapps.student.models import UserAttribute
|
||||
from common.djangoapps.util.query import use_read_replica_if_available
|
||||
|
||||
OLD_USER_ATTRIBUTE_NAME = 'marketing_emails_opt_in'
|
||||
NEW_USER_ATTRIBUTE_NAME = 'is_marketable'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Example usage:
|
||||
$ ./manage.py lms populate_is_marketable_user_attribute
|
||||
"""
|
||||
help = """
|
||||
Creates a row in the UserAttribute table for all users in the platform.
|
||||
This command back-populates the 'is_marketable' attribute in the
|
||||
UserAttribute table for the user accounts.
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--batch-delay',
|
||||
type=float,
|
||||
dest='batch_delay',
|
||||
default=0.5,
|
||||
help='Time delay in each iteration'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--batch-size',
|
||||
type=int,
|
||||
dest='batch_size',
|
||||
default=10000,
|
||||
help='Batch size'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--backfill-only',
|
||||
type=str,
|
||||
dest='backfill_only',
|
||||
default=None,
|
||||
help='Only backfill user attribute, renaming attribute is not required'
|
||||
)
|
||||
|
||||
def _get_user_attribute_queryset(self, user_attribute_id, batch_size):
|
||||
""" Fetches the user attributes in batches. """
|
||||
self.stdout.write(
|
||||
f'Fetching user attributes in batch starting from ID {user_attribute_id} with batch size {batch_size}.'
|
||||
)
|
||||
query_set = UserAttribute.objects.filter(
|
||||
id__gt=user_attribute_id,
|
||||
name=OLD_USER_ATTRIBUTE_NAME
|
||||
).order_by('id')[:batch_size]
|
||||
return use_read_replica_if_available(query_set)
|
||||
|
||||
def _get_user_queryset(self, user_id, batch_size):
|
||||
"""
|
||||
Fetches users, in ascending order of id, that exist before a specified user id.
|
||||
Returns queryset of tuples with 'id' and 'is_active' field values.
|
||||
"""
|
||||
self.stdout.write(f'Fetching users in batch starting from ID {user_id} with batch size {batch_size}.')
|
||||
query_set = get_user_model().objects.filter(
|
||||
id__gt=user_id,
|
||||
id__lt=settings.MARKETING_EMAILS_OPT_IN_FIRST_USER_ID
|
||||
).values_list('id', 'is_active').order_by('id')[:batch_size]
|
||||
return use_read_replica_if_available(query_set)
|
||||
|
||||
def _backfill_is_marketable_user_attribute(self, batch_size, batch_delay):
|
||||
"""
|
||||
Backfills the is_marketable user attribute. Fetches user accounts, in ascending order of id, that are created
|
||||
before a specified user id.
|
||||
"""
|
||||
last_user_id = 0
|
||||
users = self._get_user_queryset(last_user_id, batch_size)
|
||||
while users:
|
||||
user_attributes = []
|
||||
for user in users:
|
||||
user_attributes.append(UserAttribute(
|
||||
user_id=user[0],
|
||||
name=NEW_USER_ATTRIBUTE_NAME,
|
||||
value=str(user[1]).lower()
|
||||
))
|
||||
last_user_id = user[0]
|
||||
|
||||
try:
|
||||
UserAttribute.objects.bulk_create(user_attributes)
|
||||
except IntegrityError:
|
||||
# A UserAttribute object was already created. This could only happen if we try to create 'is_marketable'
|
||||
# user attribute that is already created. Ignore it if it does.
|
||||
pass
|
||||
|
||||
time.sleep(batch_delay)
|
||||
users = self._get_user_queryset(last_user_id, batch_size)
|
||||
|
||||
def _rename_user_attribute_name(self, batch_size, batch_delay):
|
||||
""" Renames the old user attribute 'marketing_emails_opt_in' to 'is_marketable'. """
|
||||
last_user_attribute_id = 0
|
||||
user_attributes = self._get_user_attribute_queryset(last_user_attribute_id, batch_size)
|
||||
|
||||
while user_attributes:
|
||||
updates = []
|
||||
for user_attribute in user_attributes:
|
||||
user_attribute.name = NEW_USER_ATTRIBUTE_NAME
|
||||
last_user_attribute_id = user_attribute.id
|
||||
updates.append(user_attribute)
|
||||
|
||||
UserAttribute.objects.bulk_update(updates, ['name'])
|
||||
time.sleep(batch_delay)
|
||||
user_attributes = self._get_user_attribute_queryset(last_user_attribute_id, batch_size)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
This command back-populates the 'is_marketable' attribute for all existing users who do not already
|
||||
have the attribute set.
|
||||
"""
|
||||
batch_delay = options['batch_delay']
|
||||
batch_size = options['batch_size']
|
||||
self.stdout.write(f'Command execution started with options: {options}.')
|
||||
|
||||
if not options['backfill_only']:
|
||||
self._rename_user_attribute_name(batch_size, batch_delay)
|
||||
self._backfill_is_marketable_user_attribute(batch_size, batch_delay)
|
||||
|
||||
self.stdout.write('Command executed successfully.')
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Unittests for populate_marketing_opt_in_user_attribute management command.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.management import call_command
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from common.djangoapps.student.models import UserAttribute
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
|
||||
MARKETING_EMAILS_OPT_IN = 'is_marketable'
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
class TestPopulateMarketingOptInUserAttribute(TransactionTestCase):
|
||||
"""
|
||||
Test populate_is_marketable_user_attribute management command.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.existing_user = UserFactory()
|
||||
|
||||
def test_command_with_existing_users(self):
|
||||
"""
|
||||
Test population of is_marketable attribute with an existing user.
|
||||
"""
|
||||
assert UserAttribute.objects.count() == 0
|
||||
call_command('populate_is_marketable_user_attribute')
|
||||
assert UserAttribute.objects.filter(name=MARKETING_EMAILS_OPT_IN).count() == User.objects.count()
|
||||
|
||||
def test_command_with_new_user(self):
|
||||
"""
|
||||
Test population of is_marketable attribute with a new user.
|
||||
"""
|
||||
user = UserFactory()
|
||||
call_command('populate_is_marketable_user_attribute')
|
||||
assert UserAttribute.objects.filter(name=MARKETING_EMAILS_OPT_IN).count() == User.objects.count()
|
||||
|
||||
def test_command_rename_to_new_attribute(self):
|
||||
"""
|
||||
Test renaming of marketing_emails_opt_in to is_marketable attribute.
|
||||
"""
|
||||
user = UserFactory()
|
||||
UserAttribute.objects.create(user=user, name='marketing_emails_opt_in', value='true')
|
||||
call_command('populate_is_marketable_user_attribute')
|
||||
assert UserAttribute.objects.filter(name='marketing_emails_opt_in').count() == 0
|
||||
assert UserAttribute.get_user_attribute(user, MARKETING_EMAILS_OPT_IN) == 'true'
|
||||
|
||||
def test_command_with_invalid_argument(self):
|
||||
"""
|
||||
Test management command raises TypeError on wrong data type value for '--batch-size' argument.
|
||||
"""
|
||||
with pytest.raises(TypeError):
|
||||
call_command(
|
||||
"populate_is_marketable_user_attribute",
|
||||
batch_size='1000'
|
||||
)
|
||||
@@ -1060,6 +1060,10 @@ RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS = 5
|
||||
|
||||
MARKETING_EMAILS_OPT_IN = False
|
||||
|
||||
# TODO: Remove this temporary flag after successfully running the management command.
|
||||
# Ticket: https://openedx.atlassian.net/browse/VAN-966
|
||||
MARKETING_EMAILS_OPT_IN_FIRST_USER_ID = 10000
|
||||
|
||||
# .. toggle_name: ENABLE_COPPA_COMPLIANCE
|
||||
# .. toggle_implementation: DjangoSetting
|
||||
# .. toggle_default: False
|
||||
|
||||
@@ -101,7 +101,7 @@ REGISTRATION_UTM_PARAMETERS = {
|
||||
'utm_content': 'registration_utm_content',
|
||||
}
|
||||
REGISTRATION_UTM_CREATED_AT = 'registration_utm_created_at'
|
||||
MARKETING_EMAILS_OPT_IN = 'marketing_emails_opt_in'
|
||||
MARKETING_EMAILS_OPT_IN = 'is_marketable'
|
||||
# used to announce a registration
|
||||
# providing_args=["user", "registration"]
|
||||
REGISTER_USER = Signal()
|
||||
|
||||
Reference in New Issue
Block a user