use a left outer join for experience types
This commit is contained in:
@@ -19,8 +19,8 @@ from openedx.core.djangoapps.schedules import resolvers, tasks
|
||||
from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.core.djangolib.testing.utils import FilteredQueryCountMixin, CacheIsolationTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
|
||||
|
||||
SITE_QUERY = 2 # django_site, site_configuration_siteconfiguration
|
||||
@@ -56,7 +56,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
@ddt.ddt
|
||||
@freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True)
|
||||
class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
|
||||
class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase):
|
||||
|
||||
__test__ = False
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import datetime
|
||||
import ddt
|
||||
import pytz
|
||||
from edx_ace.utils.date import serialize
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
|
||||
from courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.schedules import tasks
|
||||
from openedx.core.djangoapps.schedules.models import ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory, ScheduleConfigFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory, SiteConfigurationFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms, FilteredQueryCountMixin, CacheIsolationTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
@freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True)
|
||||
class TestExperiences(FilteredQueryCountMixin, CacheIsolationTestCase):
|
||||
|
||||
ENABLED_CACHES = ['default']
|
||||
|
||||
ExperienceTest = namedtuple('ExperienceTest', 'experience offset email_sent')
|
||||
|
||||
def setUp(self):
|
||||
super(TestExperiences, self).setUp()
|
||||
|
||||
site = SiteFactory.create()
|
||||
self.site_config = SiteConfigurationFactory.create(site=site)
|
||||
ScheduleConfigFactory.create(site=self.site_config.site)
|
||||
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
|
||||
@ddt.data(
|
||||
ExperienceTest(experience=ScheduleExperience.DEFAULT, offset=-3, email_sent=True),
|
||||
ExperienceTest(experience=ScheduleExperience.DEFAULT, offset=-10, email_sent=True),
|
||||
ExperienceTest(experience=ScheduleExperience.COURSE_UPDATES, offset=-3, email_sent=True),
|
||||
ExperienceTest(experience=ScheduleExperience.COURSE_UPDATES, offset=-10, email_sent=False),
|
||||
)
|
||||
@patch.object(tasks, 'ace')
|
||||
def test_experience_type_exclusion(self, test_config, mock_ace):
|
||||
current_day = _get_datetime_beginning_of_day(datetime.datetime.now(pytz.UTC))
|
||||
target_day = current_day + datetime.timedelta(days=test_config.offset)
|
||||
|
||||
schedule = ScheduleFactory.create(
|
||||
start=target_day,
|
||||
enrollment__course__self_paced=True,
|
||||
experience__experience_type=test_config.experience,
|
||||
)
|
||||
|
||||
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
|
||||
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=test_config.offset,
|
||||
bin_num=(schedule.enrollment.user.id % tasks.ScheduleRecurringNudge.num_bins),
|
||||
))
|
||||
|
||||
self.assertEqual(mock_ace.send.called, test_config.email_sent)
|
||||
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||
name='ScheduleExperience',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('experience_type', models.IntegerField(default=0, choices=[(0, b'Recurring Nudge and Upgrade Reminder'), (1, b'Course Updates')])),
|
||||
('experience_type', models.PositiveSmallIntegerField(default=0, choices=[(0, b'Recurring Nudge and Upgrade Reminder'), (1, b'Course Updates')])),
|
||||
('schedule', models.OneToOneField(related_name='experience', to='schedules.Schedule')),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,13 +6,6 @@ from model_utils.models import TimeStampedModel
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
EXPERIENCE_TYPES = (
|
||||
(0, 'Recurring Nudge and Upgrade Reminder'),
|
||||
(1, 'Course Updates'),
|
||||
)
|
||||
DEFAULT_EXPERIENCE_TYPE = EXPERIENCE_TYPES[0][0]
|
||||
|
||||
|
||||
class Schedule(TimeStampedModel):
|
||||
enrollment = models.OneToOneField('student.CourseEnrollment', null=False)
|
||||
active = models.BooleanField(
|
||||
@@ -31,10 +24,10 @@ class Schedule(TimeStampedModel):
|
||||
)
|
||||
|
||||
def get_experience_type(self):
|
||||
if (hasattr(self, 'experience')):
|
||||
try:
|
||||
return self.experience.experience_type
|
||||
else:
|
||||
return DEFAULT_EXPERIENCE_TYPE
|
||||
except ScheduleExperience.DoesNotExist:
|
||||
return ScheduleExperience.DEFAULT
|
||||
|
||||
class Meta(object):
|
||||
verbose_name = _('Schedule')
|
||||
@@ -55,5 +48,12 @@ class ScheduleConfig(ConfigurationModel):
|
||||
|
||||
|
||||
class ScheduleExperience(models.Model):
|
||||
DEFAULT = 0
|
||||
COURSE_UPDATES = 1
|
||||
EXPERIENCES = (
|
||||
(DEFAULT, 'Recurring Nudge and Upgrade Reminder'),
|
||||
(COURSE_UPDATES, 'Course Updates')
|
||||
)
|
||||
|
||||
schedule = models.OneToOneField(Schedule, related_name='experience')
|
||||
experience_type = models.IntegerField(choices=EXPERIENCE_TYPES, default=DEFAULT_EXPERIENCE_TYPE)
|
||||
experience_type = models.PositiveSmallIntegerField(choices=EXPERIENCES, default=DEFAULT)
|
||||
|
||||
@@ -18,7 +18,7 @@ from courseware.date_summary import verified_upgrade_deadline_link, verified_upg
|
||||
from openedx.core.djangoapps.monitoring_utils import function_trace, set_custom_metric
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
|
||||
from openedx.core.djangoapps.schedules.models import DEFAULT_EXPERIENCE_TYPE, EXPERIENCE_TYPES, Schedule
|
||||
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
|
||||
from openedx.core.djangoapps.schedules.template_context import (
|
||||
absolute_url,
|
||||
@@ -64,7 +64,9 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
|
||||
relative to. For example, if this resolver finds schedules that started 7 days ago
|
||||
this variable should be set to "start".
|
||||
num_bins -- the int number of bins to split the users into
|
||||
experience_type -- the string name for the experience type that users will be filtered to
|
||||
experience_filter -- a queryset filter used to select only the users who should be getting this message as part
|
||||
of their experience. This defaults to users without a specified experience type and those
|
||||
in the "recurring nudges and upgrade reminder" experience.
|
||||
"""
|
||||
async_send_task = attr.ib()
|
||||
site = attr.ib()
|
||||
@@ -75,7 +77,7 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
|
||||
|
||||
schedule_date_field = None
|
||||
num_bins = DEFAULT_NUM_BINS
|
||||
experience_type = DEFAULT_EXPERIENCE_TYPE
|
||||
experience_filter = Q(experience__experience_type=ScheduleExperience.DEFAULT) | Q(experience__isnull=True)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
# TODO: in the next refactor of this task, pass in current_datetime instead of reproducing it here
|
||||
@@ -126,13 +128,10 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
|
||||
'enrollment__course',
|
||||
).prefetch_related(
|
||||
'enrollment__course__modes',
|
||||
'experience',
|
||||
).filter(
|
||||
Q(enrollment__course__end__isnull=True) | Q(
|
||||
enrollment__course__end__gte=self.current_datetime),
|
||||
Q(experience__isnull=True) | Q(experience__experience_type=self.experience_type)
|
||||
if self.experience_type == DEFAULT_EXPERIENCE_TYPE else
|
||||
Q(experience__isnull=False) & Q(experience__experience_type=self.experience_type),
|
||||
self.experience_filter,
|
||||
enrollment__user__in=users,
|
||||
enrollment__is_active=True,
|
||||
**schedule_day_equals_target_day_filter
|
||||
@@ -238,6 +237,14 @@ class RecurringNudgeResolver(BinnedSchedulesBaseResolver):
|
||||
schedule_date_field = 'start'
|
||||
num_bins = RECURRING_NUDGE_NUM_BINS
|
||||
|
||||
@property
|
||||
def experience_filter(self):
|
||||
if self.day_offset == -3:
|
||||
experiences = [ScheduleExperience.DEFAULT, ScheduleExperience.COURSE_UPDATES]
|
||||
return Q(experience__experience_type__in=experiences) | Q(experience__isnull=True)
|
||||
else:
|
||||
return Q(experience__experience_type=ScheduleExperience.DEFAULT) | Q(experience__isnull=True)
|
||||
|
||||
def get_template_context(self, user, user_schedules):
|
||||
first_schedule = user_schedules[0]
|
||||
context = {
|
||||
@@ -339,7 +346,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver):
|
||||
log_prefix = 'Course Update'
|
||||
schedule_date_field = 'start'
|
||||
num_bins = COURSE_UPDATE_NUM_BINS
|
||||
experience_type = EXPERIENCE_TYPES[1][0]
|
||||
experience_filter = Q(experience__experience_type=ScheduleExperience.COURSE_UPDATES)
|
||||
|
||||
def schedules_for_bin(self):
|
||||
week_num = abs(self.day_offset) / 7
|
||||
|
||||
@@ -6,6 +6,13 @@ from student.tests.factories import CourseEnrollmentFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
|
||||
|
||||
class ScheduleExperienceFactory(factory.DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = models.ScheduleExperience
|
||||
|
||||
experience_type = models.ScheduleExperience.DEFAULT
|
||||
|
||||
|
||||
class ScheduleFactory(factory.DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = models.Schedule
|
||||
@@ -13,6 +20,7 @@ class ScheduleFactory(factory.DjangoModelFactory):
|
||||
start = factory.Faker('future_datetime', tzinfo=pytz.UTC)
|
||||
upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC)
|
||||
enrollment = factory.SubFactory(CourseEnrollmentFactory)
|
||||
experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule')
|
||||
|
||||
|
||||
class ScheduleConfigFactory(factory.DjangoModelFactory):
|
||||
|
||||
Reference in New Issue
Block a user