From 0663795781f060fca356fa6f5d5ee547e3e8fbd2 Mon Sep 17 00:00:00 2001 From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:27:50 +0500 Subject: [PATCH] feat: reminder emails (#30159) send reminder emails to those users who saved course or program but not enroll within 15 days. VAN-887 --- lms/djangoapps/save_for_later/admin.py | 4 +- lms/djangoapps/save_for_later/api/v1/views.py | 37 +++++++- lms/djangoapps/save_for_later/helper.py | 27 +++--- .../save_for_later/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/send_course_reminder_emails.py | 92 +++++++++++++++++++ .../commands/send_program_reminder_emails.py | 85 +++++++++++++++++ .../management/commands/tests/__init__.py | 0 .../tests/test_send_course_reminder_emails.py | 39 ++++++++ .../test_send_program_reminder_emails.py | 38 ++++++++ .../migrations/0002_auto_20220322_1621.py | 66 +++++++++++++ lms/djangoapps/save_for_later/models.py | 23 +++++ .../save_for_later/tests/factories.py | 46 ++++++++++ lms/envs/common.py | 3 + 14 files changed, 442 insertions(+), 18 deletions(-) create mode 100644 lms/djangoapps/save_for_later/management/__init__.py create mode 100644 lms/djangoapps/save_for_later/management/commands/__init__.py create mode 100644 lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py create mode 100644 lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py create mode 100644 lms/djangoapps/save_for_later/management/commands/tests/__init__.py create mode 100644 lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py create mode 100644 lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py create mode 100644 lms/djangoapps/save_for_later/migrations/0002_auto_20220322_1621.py create mode 100644 lms/djangoapps/save_for_later/tests/factories.py diff --git a/lms/djangoapps/save_for_later/admin.py b/lms/djangoapps/save_for_later/admin.py index aa68212463..9c94e5e94b 100644 --- a/lms/djangoapps/save_for_later/admin.py +++ b/lms/djangoapps/save_for_later/admin.py @@ -10,7 +10,7 @@ class SavedCourseAdmin(admin.ModelAdmin): Admin for the Saved Course table. """ - list_display = ['email', 'course_id'] + list_display = ['email', 'course_id', 'email_sent_count', 'reminder_email_sent'] search_fields = ['email', 'course_id'] @@ -20,7 +20,7 @@ class SavedProgramAdmin(admin.ModelAdmin): Admin for the Saved Program table. """ - list_display = ['email', 'program_uuid'] + list_display = ['email', 'program_uuid', 'email_sent_count', 'reminder_email_sent'] search_fields = ['email', 'program_uuid'] diff --git a/lms/djangoapps/save_for_later/api/v1/views.py b/lms/djangoapps/save_for_later/api/v1/views.py index 82a7f0c3b2..5d7c827d2d 100644 --- a/lms/djangoapps/save_for_later/api/v1/views.py +++ b/lms/djangoapps/save_for_later/api/v1/views.py @@ -24,6 +24,7 @@ log = logging.getLogger(__name__) POST_EMAIL_KEY = 'openedx.core.djangoapps.util.ratelimit.request_data_email' REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip' +USER_SEND_SAVE_FOR_LATER_EMAIL = 'user.send.save.for.later.email' class CourseSaveForLaterApiView(APIView): @@ -61,6 +62,14 @@ class CourseSaveForLaterApiView(APIView): data = request.data course_id = data.get('course_id') email = data.get('email') + org_img_url = data.get('org_img_url') + marketing_url = data.get('marketing_url') + weeks_to_complete = data.get('weeks_to_complete', 0) + min_effort = data.get('min_effort', 0) + max_effort = data.get('max_effort', 0) + user_id = request.user.id + pref_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') + send_to_self = bool(not request.user.is_anonymous and request.user.email == email) if getattr(request, 'limited', False): return Response({'error_code': 'rate-limited'}, status=403) @@ -80,12 +89,28 @@ class CourseSaveForLaterApiView(APIView): user_id=user.id, email=email, course_id=course_id, + org_img_url=org_img_url, + marketing_url=marketing_url, + weeks_to_complete=weeks_to_complete, + min_effort=min_effort, + max_effort=max_effort, + reminder_email_sent=False, ) course_data = { 'course': course, + 'send_to_self': send_to_self, + 'user_id': user_id, + 'pref-lang': pref_lang, + 'org_img_url': org_img_url, + 'marketing_url': marketing_url, + 'weeks_to_complete': weeks_to_complete, + 'min_effort': min_effort, + 'max_effort': max_effort, 'type': 'course', + 'reminder': False, + 'braze_event': USER_SEND_SAVE_FOR_LATER_EMAIL, } - if send_email(request, email, course_data): + if send_email(email, course_data): return Response({'result': 'success'}, status=200) else: return Response({'error_code': 'email-not-send'}, status=400) @@ -120,6 +145,9 @@ class ProgramSaveForLaterApiView(APIView): data = request.data program_uuid = data.get('program_uuid') email = data.get('email') + user_id = request.user.id + pref_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') + send_to_self = bool(not request.user.is_anonymous and request.user.email == email) if getattr(request, 'limited', False): return Response({'error_code': 'rate-limited'}, status=403) @@ -139,9 +167,14 @@ class ProgramSaveForLaterApiView(APIView): if program: program_data = { 'program': program, + 'send_to_self': send_to_self, + 'user_id': user_id, + 'pref-lang': pref_lang, 'type': 'program', + 'reminder': False, + 'braze_event': USER_SEND_SAVE_FOR_LATER_EMAIL, } - if send_email(request, email, program_data): + if send_email(email, program_data): return Response({'result': 'success'}, status=200) else: return Response({'error_code': 'email-not-send'}, status=400) diff --git a/lms/djangoapps/save_for_later/helper.py b/lms/djangoapps/save_for_later/helper.py index 03f89b5811..5b58881b5f 100644 --- a/lms/djangoapps/save_for_later/helper.py +++ b/lms/djangoapps/save_for_later/helper.py @@ -12,7 +12,8 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_ log = logging.getLogger(__name__) -USER_SENT_EMAIL_SAVE_FOR_LATER = 'edx.bi.user.saveforlater.email.sent' +USER_SAVE_FOR_LATER_EMAIL_SENT = 'edx.bi.user.saveforlater.email.sent' +USER_SAVE_FOR_LATER_REMINDER_EMAIL_SENT = 'edx.bi.user.saveforlater.reminder.email.sent' def _get_program_pacing(course_runs): @@ -25,30 +26,28 @@ def _get_program_pacing(course_runs): return 'Self-paced' if pacing == 'self_paced' else 'Instructor-led' -def _get_event_properties(request, data): +def _get_event_properties(data): """ set event properties for course and program which are required in braze email template """ lms_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL) event_properties = { 'time': datetime.now().isoformat(), - 'name': 'user.send.save.for.later.email', + 'name': data.get('braze_event'), } if data.get('type') == 'course': course = data.get('course') - data = request.data - org_img_url = data.get('org_img_url') - marketing_url = data.get('marketing_url') event_properties.update({ 'properties': { 'course_image_url': '{base_url}{image_path}'.format( base_url=lms_url, image_path=course.course_image_url ), - 'partner_image_url': org_img_url, + 'partner_image_url': data.get('org_img_url'), 'enroll_course_url': '{base_url}/register?course_id={course_id}&enrollment_action=enroll&email_opt_in=' 'false&save_for_later=true'.format(base_url=lms_url, course_id=course.id), - 'view_course_url': marketing_url + '?save_for_later=true' if marketing_url else '#', + 'view_course_url': data.get('marketing_url') + '?save_for_later=true' if data.get( + 'marketing_url') else '#', 'display_name': course.display_name, 'short_description': course.short_description, 'weeks_to_complete': data.get('weeks_to_complete'), @@ -84,11 +83,11 @@ def _get_event_properties(request, data): return event_properties -def send_email(request, email, data): +def send_email(email, data): """ Send email through Braze """ - event_properties = _get_event_properties(request, data) + event_properties = _get_event_properties(data) braze_client = BrazeClient( api_key=settings.EDX_BRAZE_API_KEY, api_url=settings.EDX_BRAZE_API_SERVER, @@ -109,16 +108,16 @@ def send_email(request, email, data): event_properties.update({'user_alias': user_alias}) attributes = [{ 'user_alias': user_alias, - 'pref-lang': request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') + 'pref-lang': data.get('pref-lang', 'en') }] braze_client.track_user(events=[event_properties], attributes=attributes) event_data = { - 'user_id': request.user.id, + 'user_id': data.get('user_id'), 'category': 'save-for-later', 'type': event_properties.get('type'), - 'send_to_self': bool(not request.user.is_anonymous and request.user.email == email), + 'send_to_self': data.get('send_to_self'), } if data.get('type') == 'program': program = data.get('program') @@ -128,7 +127,7 @@ def send_email(request, email, data): event_data.update({'course_key': str(course.id)}) tracker.emit( - USER_SENT_EMAIL_SAVE_FOR_LATER, + USER_SAVE_FOR_LATER_REMINDER_EMAIL_SENT if data.get('reminder') else USER_SAVE_FOR_LATER_EMAIL_SENT, event_data ) except Exception: # pylint: disable=broad-except diff --git a/lms/djangoapps/save_for_later/management/__init__.py b/lms/djangoapps/save_for_later/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/save_for_later/management/commands/__init__.py b/lms/djangoapps/save_for_later/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py new file mode 100644 index 0000000000..5669925dd4 --- /dev/null +++ b/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py @@ -0,0 +1,92 @@ +""" +Management command to send reminder emails. +""" + +import logging + +from textwrap import dedent +from datetime import datetime, timedelta + +from django.conf import settings +from django.core.management import BaseCommand +from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user + +from lms.djangoapps.save_for_later.helper import send_email +from lms.djangoapps.save_for_later.models import SavedCourse +from common.djangoapps.student.models import CourseEnrollment +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from common.djangoapps.util.query import use_read_replica_if_available + +logger = logging.getLogger(__name__) + +USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL = 'user.send.save.for.later.reminder.email' + + +class Command(BaseCommand): + """ + Command to send reminder emails to those users who + saved course by email but not register within 15 days. + + + Examples: + + ./manage.py lms send_course_reminder_emails --batch-size=100 + """ + help = dedent(__doc__) + + def add_arguments(self, parser): + parser.add_argument( + '--batch-size', + type=int, + default=1000, + help='Maximum number of users to send reminder email in one chunk') + + def handle(self, *args, **options): + """ + Handle the send save for later reminder emails. + """ + + reminder_email_threshold_date = datetime.now() - timedelta( + days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) + saved_courses_ids = SavedCourse.objects.filter( + reminder_email_sent=False, modified__lt=reminder_email_threshold_date + ).values_list('id', flat=True) + total = saved_courses_ids.count() + batch_size = max(1, options.get('batch_size')) + num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 + + for batch_num in range(int(num_batches)): + reminder_email_sent_ids = [] + start = batch_num * batch_size + end = min(start + batch_size, total) - 1 + saved_courses_batch_ids = list(saved_courses_ids)[start:end + 1] + + query = SavedCourse.objects.filter(id__in=saved_courses_batch_ids).order_by('-modified') + saved_courses = use_read_replica_if_available(query) + for saved_course in saved_courses: + user = User.objects.filter(email=saved_course.email).first() + course_overview = CourseOverview.get_from_id(saved_course.course_id) + course_data = { + 'course': course_overview, + 'send_to_self': None, + 'user_id': saved_course.user_id, + 'org_img_url': saved_course.org_img_url, + 'marketing_url': saved_course.marketing_url, + 'weeks_to_complete': saved_course.weeks_to_complete, + 'min_effort': saved_course.min_effort, + 'max_effort': saved_course.max_effort, + 'type': 'course', + 'reminder': True, + 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, + } + if user: + enrollment = CourseEnrollment.get_enrollment(user, saved_course.course_id) + if enrollment: + continue + email_sent = send_email(saved_course.email, course_data) + if email_sent: + reminder_email_sent_ids.append(saved_course.id) + else: + logging.info("Unable to send reminder email to {user} for {course} course" + .format(user=str(saved_course.email), course=str(saved_course.course_id))) + SavedCourse.objects.filter(id__in=reminder_email_sent_ids).update(reminder_email_sent=True) diff --git a/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py new file mode 100644 index 0000000000..650fa42978 --- /dev/null +++ b/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py @@ -0,0 +1,85 @@ +""" +Management command to send program reminder emails. +""" + +import logging + +from textwrap import dedent +from datetime import datetime, timedelta + +from django.conf import settings +from django.core.management import BaseCommand +from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user + +from lms.djangoapps.save_for_later.helper import send_email +from lms.djangoapps.save_for_later.models import SavedProgram +from lms.djangoapps.program_enrollments.api import get_program_enrollment +from openedx.core.djangoapps.catalog.utils import get_programs +from common.djangoapps.util.query import use_read_replica_if_available + +LOGGER = logging.getLogger(__name__) + +USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL = 'user.send.save.for.later.reminder.email' + + +class Command(BaseCommand): + """ + Command to send reminder emails to those users who saved + program by email but not enroll program within 15 days. + + + Examples: + + ./manage.py lms send_program_reminder_emails --batch-size=100 + """ + help = dedent(__doc__) + + def add_arguments(self, parser): + parser.add_argument( + '--batch-size', + type=int, + default=1000, + help='Maximum number of users to send reminder email in one chunk') + + def handle(self, *args, **options): + """ + Handle the send save for later reminder emails. + """ + reminder_email_threshold_date = datetime.now() - timedelta( + days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) + saved_program_ids = SavedProgram.objects.filter( + reminder_email_sent=False, modified__lt=reminder_email_threshold_date + ).values_list('id', flat=True) + total = saved_program_ids.count() + batch_size = max(1, options.get('batch_size')) + num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 + + for batch_num in range(int(num_batches)): + reminder_email_sent_ids = [] + start = batch_num * batch_size + end = min(start + batch_size, total) - 1 + saved_program_batch_ids = list(saved_program_ids)[start:end + 1] + + query = SavedProgram.objects.filter(id__in=saved_program_batch_ids).order_by('-modified') + saved_programs = use_read_replica_if_available(query) + for saved_program in saved_programs: + user = User.objects.filter(email=saved_program.email).first() + program = get_programs(uuid=saved_program.program_uuid) + if program: + program_data = { + 'program': program, + 'send_to_self': None, + 'user_id': saved_program.user_id, + 'type': 'program', + 'reminder': True, + 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, + } + if user and get_program_enrollment(program_uuid=saved_program.uuid, user=user): + continue + email_sent = send_email(saved_program.email, program_data) + if email_sent: + reminder_email_sent_ids.append(saved_program.id) + else: + logging.info("Unable to send reminder email to {user} for {program} program" + .format(user=str(saved_program.email), program=str(saved_program.program_uuid))) + SavedProgram.objects.filter(id__in=reminder_email_sent_ids).update(reminder_email_sent=True) diff --git a/lms/djangoapps/save_for_later/management/commands/tests/__init__.py b/lms/djangoapps/save_for_later/management/commands/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py new file mode 100644 index 0000000000..6bdc8a6637 --- /dev/null +++ b/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py @@ -0,0 +1,39 @@ +""" Test the test_send_course_reminder_emails command line script.""" + +from unittest.mock import patch + +import ddt +from django.core.management import call_command +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase + +from openedx.core.djangolib.testing.utils import skip_unless_lms +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.save_for_later.tests.factories import SavedCourseFactory +from lms.djangoapps.save_for_later.models import SavedCourse +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory + + +@ddt.ddt +@skip_unless_lms +class SavedCourseReminderEmailsTest(SharedModuleStoreTestCase): + """ + Test the test_send_course_reminder_emails management command + """ + + def setUp(self): + super().setUp() + self.course_id = 'course-v1:edX+DemoX+Demo_Course' + self.user = UserFactory(email='email@test.com', username='jdoe') + self.saved_course = SavedCourseFactory.create(course_id=self.course_id, user_id=self.user.id) + self.saved_course_1 = SavedCourseFactory.create(course_id=self.course_id) + CourseOverviewFactory.create(id=self.saved_course.course_id) + CourseOverviewFactory.create(id=self.saved_course_1.course_id) + + def test_send_reminder_emails(self): + with patch('lms.djangoapps.save_for_later.helper.BrazeClient') as mock_task: + call_command('send_course_reminder_emails', '--batch-size=1') + mock_task.assert_called() + + saved_course = SavedCourse.objects.filter(course_id=self.course_id).first() + assert saved_course.reminder_email_sent is True + assert saved_course.email_sent_count > 0 diff --git a/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py new file mode 100644 index 0000000000..2501ccdb9f --- /dev/null +++ b/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py @@ -0,0 +1,38 @@ +""" Test the test_send_program_reminder_emails command line script.""" + + +from unittest.mock import patch + +import ddt +from django.core.management import call_command +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase + +from openedx.core.djangolib.testing.utils import skip_unless_lms +from lms.djangoapps.save_for_later.tests.factories import SavedPogramFactory +from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory +from lms.djangoapps.save_for_later.models import SavedProgram + + +@ddt.ddt +@skip_unless_lms +class SavedProgramReminderEmailsTest(SharedModuleStoreTestCase): + """ + Test the send_program_reminder_emails management command + """ + + def setUp(self): + super().setUp() + self.uuid = '587f6abe-bfa4-4125-9fbe-4789bf3f97f1' + self.program = ProgramFactory(uuid=self.uuid) + self.saved_program = SavedPogramFactory.create(program_uuid=self.uuid) + + @patch('lms.djangoapps.save_for_later.management.commands.send_program_reminder_emails.get_programs') + def test_send_reminder_emails(self, mock_get_programs): + mock_get_programs.return_value = self.program + with patch('lms.djangoapps.save_for_later.helper.BrazeClient') as mock_task: + call_command('send_program_reminder_emails', '--batch-size=1') + mock_task.assert_called() + + saved_program = SavedProgram.objects.filter(program_uuid=self.uuid).first() + assert saved_program.reminder_email_sent is True + assert saved_program.email_sent_count > 0 diff --git a/lms/djangoapps/save_for_later/migrations/0002_auto_20220322_1621.py b/lms/djangoapps/save_for_later/migrations/0002_auto_20220322_1621.py new file mode 100644 index 0000000000..318fe56c84 --- /dev/null +++ b/lms/djangoapps/save_for_later/migrations/0002_auto_20220322_1621.py @@ -0,0 +1,66 @@ +# Generated by Django 3.2.12 on 2022-03-22 16:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('save_for_later', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='savedcourse', + name='marketing_url', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='savedcourse', + name='max_effort', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='savedcourse', + name='min_effort', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='savedcourse', + name='org_img_url', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='savedcourse', + name='weeks_to_complete', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='savedcourse', + name='email_sent_count', + field=models.IntegerField(default=0, null=True), + ), + migrations.AlterUniqueTogether( + name='savedcourse', + unique_together={('email', 'course_id')}, + ), + migrations.AddField( + model_name='savedprogram', + name='email_sent_count', + field=models.IntegerField(default=0, null=True), + ), + migrations.AlterUniqueTogether( + name='savedprogram', + unique_together={('email', 'program_uuid')}, + ), + migrations.AddField( + model_name='savedcourse', + name='reminder_email_sent', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='savedprogram', + name='reminder_email_sent', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/lms/djangoapps/save_for_later/models.py b/lms/djangoapps/save_for_later/models.py index 3a6b5ed243..23eef180b0 100644 --- a/lms/djangoapps/save_for_later/models.py +++ b/lms/djangoapps/save_for_later/models.py @@ -21,6 +21,20 @@ class SavedCourse(DeletableByUserValue, TimeStampedModel): user_id = models.IntegerField(null=True, blank=True) email = models.EmailField(db_index=True) course_id = CourseKeyField(max_length=255, db_index=True) + marketing_url = models.CharField(max_length=255, null=True, blank=True) + org_img_url = models.CharField(max_length=255, null=True, blank=True) + weeks_to_complete = models.IntegerField(null=True) + min_effort = models.IntegerField(null=True) + max_effort = models.IntegerField(null=True) + email_sent_count = models.IntegerField(null=True, default=0) + reminder_email_sent = models.BooleanField(default=False, null=True) + + class Meta: + unique_together = ('email', 'course_id',) + + def save(self, *args, **kwargs): + self.email_sent_count = self.email_sent_count + 1 + super().save(*args, **kwargs) class SavedProgram(DeletableByUserValue, TimeStampedModel): @@ -34,3 +48,12 @@ class SavedProgram(DeletableByUserValue, TimeStampedModel): user_id = models.IntegerField(null=True, blank=True) email = models.EmailField(db_index=True) program_uuid = models.UUIDField() + email_sent_count = models.IntegerField(null=True, default=0) + reminder_email_sent = models.BooleanField(default=False, null=True) + + class Meta: + unique_together = ('email', 'program_uuid',) + + def save(self, *args, **kwargs): + self.email_sent_count = self.email_sent_count + 1 + super().save(*args, **kwargs) diff --git a/lms/djangoapps/save_for_later/tests/factories.py b/lms/djangoapps/save_for_later/tests/factories.py new file mode 100644 index 0000000000..5c10295ec5 --- /dev/null +++ b/lms/djangoapps/save_for_later/tests/factories.py @@ -0,0 +1,46 @@ +""" + Provides factories for save_for_later models. +""" + + +from datetime import datetime +from pytz import UTC + +import factory +from factory.django import DjangoModelFactory + +from lms.djangoapps.save_for_later.models import SavedCourse, SavedProgram + + +class SavedCourseFactory(DjangoModelFactory): + """ + Factory for SavedCourses objects + """ + class Meta: + model = SavedCourse + django_get_or_create = ('course_id', 'user_id') + + email = 'abc@test.com' + course_id = factory.Sequence('{}'.format) + user_id = factory.Sequence('{}'.format) + reminder_email_sent = False + email_sent_count = 0 + created = datetime(2020, 1, 1, tzinfo=UTC) + modified = datetime(2020, 2, 1, tzinfo=UTC) + + +class SavedPogramFactory(DjangoModelFactory): + """ + Factory for SavedProgram objects + """ + class Meta: + model = SavedProgram + django_get_or_create = ('program_uuid', ) + + email = 'abc@test.com' + user_id = factory.Sequence('{}'.format) + program_uuid = factory.Sequence('{}'.format) + reminder_email_sent = False + email_sent_count = 0 + created = datetime(2020, 1, 1, tzinfo=UTC) + modified = datetime(2020, 2, 1, tzinfo=UTC) diff --git a/lms/envs/common.py b/lms/envs/common.py index 5693c31ddc..ad1b911ebb 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1054,6 +1054,9 @@ ENABLE_COPPA_COMPLIANCE = False # VAN-741 - save for later api put behind a flag to make it only available for edX ENABLE_SAVE_FOR_LATER = False +# VAN-887 - save for later reminder emails threshold days +SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD = 15 + ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms REPO_ROOT = PROJECT_ROOT.dirname()