Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RET triggered emails #15745

Merged
merged 1 commit into from
Sep 1, 2017
Merged

RET triggered emails #15745

merged 1 commit into from
Sep 1, 2017

Conversation

cpennington
Copy link
Contributor

No description provided.

@cpennington cpennington force-pushed the ret/triggered-email branch 6 times, most recently from 135c383 to bab199c Compare August 8, 2017 21:01
@mulby mulby force-pushed the ret/triggered-email branch from 8e62bda to f6220ca Compare August 11, 2017 18:16
@cpennington cpennington force-pushed the ret/triggered-email branch 2 times, most recently from 643d15c to 876df69 Compare August 16, 2017 20:56
).filter(
start__gte=target_hour,
start__lt=target_hour + datetime.timedelta(minutes=60),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part needs to be site aware as well, because individual sites manage specific courses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also filter out inactive enrollments

import openedx.core.djangoapps.xmodule_django.models


class Migration(migrations.Migration):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How has these migration changes been tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name='course_id',
field=openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True, verbose_name="Course", db_column='course_id'),
),
migrations.RenameField(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how this migration doesn't result in an actual SQL migration, causing issues with blue/green deployments?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cale and I talked through several solutions, I'll let him explain further.

model_name='coursemode',
name='course_id',
field=openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True, verbose_name="Course", db_column='course_id'),
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this actually altering the field? Is it not declared the same as before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comments explaining this.

@@ -81,7 +84,7 @@ def __init__(self, *args, **kwargs):
)

def clean_course_id(self):
course_id = self.cleaned_data['course_id']
course_id = self.cleaned_data['course']
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want the call to the modulestore below? Or, should it be checking CourseOverview instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not muck with that logic right now.

@@ -59,6 +59,9 @@ class Meta(object):
def __init__(self, *args, **kwargs):
super(CourseModeForm, self).__init__(*args, **kwargs)

if self.data.get('course'):
self.data['course'] = CourseKey.from_string(self.data['course'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So am I reading this right - for the django admin view, we never bother converting the course string value to a CourseOverview object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The admin form widget for a ForeignKey just uses the key to the other table, not the actual object.

@@ -114,3 +119,56 @@ def test_optin_course(self):
sent_addresses = [message.to[0] for message in mail.outbox]
self.assertIn(self.student.email, sent_addresses)
self.assertIn(self.instructor.email, sent_addresses)

def test_policy_optedout(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: For readability and self-documenting code, consider putting these test methods in a separate Test class for ACE-specific policy tests.

"""
url = reverse('change_email_settings')
response = self.client.post(url, {'course_id': self.course.id.to_deprecated_string(), 'receive_emails': 'on'})
self.assertEquals(json.loads(response.content), {'success': True})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: consider DRYing the redundant setup of these test methods.

@@ -91,11 +91,11 @@ def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused-
user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
grade_factory = CourseGradeFactory()
for enrollment in user_enrollments:
if grade_factory.read(user=user, course=enrollment.course).passed:
if fire_ungenerated_certificate_task(user, enrollment.course.id):
if grade_factory.read(user=user, course=enrollment.course_overview).passed:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. Shouldn't enrollment.course work now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.course doesn't create the CourseOverview if it doesn't already exist.

ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME = ENV_TOKENS.get('ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME', ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME)
ACE_CHANNEL_SAILTHRU_API_KEY = AUTH_TOKENS.get('ACE_CHANNEL_SAILTHRU_API_KEY', ACE_CHANNEL_SAILTHRU_API_KEY)
ACE_CHANNEL_SAILTHRU_API_SECRET = AUTH_TOKENS.get('ACE_CHANNEL_SAILTHRU_API_SECRET', ACE_CHANNEL_SAILTHRU_API_SECRET)
ACE_ROUTING_KEY = ENV_TOKENS.get('ACE_ROUTING_KEY', ACE_ROUTING_KEY)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it preferable to put all ACE settings in their own dict structure? Or is having separate variables like this better operationally? FYI @jibsheet

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest problems with using a dict is that Ansible doesn't merge dictionaries from lms/configuration/internal/secure together, it just takes the top level dictionary in the innermost override, so you have to have wacky looking overrides where you pull out the API_KEY and API_SECRET into secure and then interpolate them in edx-internal (see Cale's examples here https://openedx.atlassian.net/wiki/spaces/EdxOps/pages/122454502/Splitting+of+secure+repos) . I like the idea of most of these being grouped in a dictionary, but in my experience they cause more pain. If we've documented the dict approach somewhere, let me know and I can revisit those docs.

@@ -35,7 +35,7 @@
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls, check_mongo_calls_range

from .models import CourseOverview, CourseOverviewImageSet, CourseOverviewImageConfig
from ..models import CourseOverview, CourseOverviewImageSet, CourseOverviewImageConfig
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were tests moved to an init.py file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted tests to be a module, because I added factories.py to it. But I could move the tests into a separate submodule as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

enrollment=enrollment,
active=True,
start=timezone.now(),
upgrade_deadline=timezone.now() + datetime.timedelta(days=21)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this upgrade_deadline offset be a django admin setting (as part of ScheduleConfig)?

I've added a discussion item for morning planning on whether this offset also needs to have a course-specific override.

class RecurringNudge(MessageType):
def __init__(self, week, *args, **kwargs):
super(RecurringNudge, self).__init__(*args, **kwargs)
self.name = "recurringnudge_week{}".format(week)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add an underscore between recurring and nudge for readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to keep this consistent with the standard template naming (which just lowercases the class name).

return

if not (
ScheduleConfig.current().is_enabled or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this call to ScheduleConfig need to pass a value for site?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

self.site = site
self.current_date = current_date.replace(hour=0, minute=0, second=0)

def send(self, week):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docstring here (or at the class level) would be useful to explain that the week here refers to the week number since schedule start.


@task(ignore_result=True, routing_key=settings.ACE_ROUTING_KEY)
def _schedule_hour(site_id, week, target_hour):
msg_type = RecurringNudge(week)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. interesting. The RecipientResolver here has also become the Composer of the email. As a Later refactor, what do you think of decoupling the 2 so that multiple Composers can use the same ScheduleStartResolver?

class Command(BaseCommand):

def add_arguments(self, parser):
parser.add_argument('--date', default=datetime.datetime.utcnow().date().isoformat())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add help strings here for the expected format of the arguments.


def handle(self, *args, **options):
current_date = datetime.datetime(
*[int(x) for x in options['date'].split('-')],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use datetime.strptime instead?

Copy link
Contributor Author

@cpennington cpennington Aug 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can force strptime to be a timezone aware UTC date, unfortunately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've done it like this previously
https://github.com/edx/edx-platform/pull/14608/files (and some other management commands)


template_context = {
'user_full_name': user.profile.name,
'user_personal_address': user.profile.name if user.profile.name else user.username,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: is there a better name for this? Doesn't Personal address imply a residential address? How about user_personal_name?

pass


class VerifiedDeadlineResolver(RecipientResolver):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still work-in-progress? If so, we can get back to this later. At a first glance, I'm wondering where we filter out users who have already verified? And filter out courses whose verification deadlines have already passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's still in work-in-progress.

@@ -0,0 +1,3 @@
Dear {{student_name}},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a subfolder named "schedules" within a folder already named "schedules"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that theming overrides can set the template schedules/edx_ace/recurringnudge_week1/email/body.html to override this template. Django doesn't give app templates separate namespaces by default, afaik.


def add_arguments(self, parser):
parser.add_argument('--date', default=datetime.datetime.utcnow().date().isoformat())
parser.add_argument('site_domain_name')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: does this need a -- prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's required.

@cpennington cpennington force-pushed the ret/triggered-email branch 2 times, most recently from 6fce3f6 to 613240e Compare August 23, 2017 15:02
@edx-pipeline-bot
Copy link
Contributor

EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production on Thursday, August 24, 2017.

@mulby
Copy link
Contributor

mulby commented Aug 26, 2017

Still TODO on this:

  • Fix the broken tests. They pass on my docker devstack when I run "paver test_system -s lms -t openedx.core.djangoapps.schedules.management.commands.tests.test_send_recurring_nudge", but they consistently fail on Jenkins.
  • Clean up the template so that it works on all email clients - Litmus is still showing some funky ones.
  • Update the template to use {% static %} for all of the images and commit the images to the repo.
  • Verify that the static images will be uploaded to the CDN and the correct links will be generated in the template in production.
  • Update the template to mark strings for translation.
  • Re-evaluate the wording in the template.
  • Update the week 2 email to use the new base template.
  • Remove references to edx.org from the templates, replace with platform name. Not sure what to do about logos?
  • Make sure all links in the template are wired up with correct values - currently most are just "#". Sailthru supports stuff like "forward to a friend" but I'm not sure we can make use of it since we do the template rendering in ace and don't have access to the "forward_url".
  • Update the template so that the entire CTA is clickable (currently only the text is a link).
  • Make sure the template is accessible.

@nasthagiri nasthagiri force-pushed the ret/triggered-email branch from e71f7c3 to 90aef0f Compare August 29, 2017 13:04
DynamicUpgradeDeadlineConfiguration.is_enabled()
or CourseDynamicUpgradeDeadlineConfiguration.is_enabled(self.course_id)
)
if schedule_driven_deadlines_enabled and self.schedule and self.schedule.upgrade_deadline is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment for why we're checking for a non-Null value for upgrade_deadline.

class ScheduleConfig(ConfigurationModel):
KEY_FIELDS = ('site',)

site = models.ForeignKey(Site)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we need to add a field for create_schedules in this model. Otherwise, we can't do a course-specific staged rollout after enabling this global flag, per our rollout plan. See step #3 and #6.

@mulby mulby force-pushed the ret/triggered-email branch from 65784b0 to cb41ee5 Compare August 31, 2017 19:56
@nasthagiri nasthagiri force-pushed the ret/triggered-email branch 2 times, most recently from 8b1b126 to bc5d8dc Compare August 31, 2017 20:43
@nasthagiri nasthagiri changed the title WIP: RET triggered emails RET triggered emails Aug 31, 2017
@nasthagiri nasthagiri force-pushed the ret/triggered-email branch from bc5d8dc to 756ac01 Compare August 31, 2017 20:48
@nasthagiri nasthagiri force-pushed the ret/triggered-email branch from 756ac01 to 6a36eb0 Compare August 31, 2017 20:58
return

try:
site_config = SiteConfiguration.objects.get(site_id=self.site.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is not completely straightforward to understand and seems like it doesn't belong within this function. It may be better suited to create a helper function for this within SiteConfiguration.

@nasthagiri nasthagiri merged commit c08060a into master Sep 1, 2017
@nasthagiri nasthagiri deleted the ret/triggered-email branch September 1, 2017 00:44
@edx-pipeline-bot
Copy link
Contributor

EdX Release Notice: This PR has been deployed to the production environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants