-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
RET triggered emails #15745
Conversation
135c383
to
bab199c
Compare
8e62bda
to
f6220ca
Compare
643d15c
to
876df69
Compare
).filter( | ||
start__gte=target_hour, | ||
start__lt=target_hour + datetime.timedelta(minutes=60), | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can see the generated sql in https://github.com/edx/edx-platform/pull/15878
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'), | ||
), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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']) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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}) |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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('-')], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use datetime.strptime
instead?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}}, |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's required.
6fce3f6
to
613240e
Compare
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production on Thursday, August 24, 2017. |
613240e
to
d9020ec
Compare
Still TODO on this:
|
e71f7c3
to
90aef0f
Compare
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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
65784b0
to
cb41ee5
Compare
8b1b126
to
bc5d8dc
Compare
bc5d8dc
to
756ac01
Compare
756ac01
to
6a36eb0
Compare
return | ||
|
||
try: | ||
site_config = SiteConfiguration.objects.get(site_id=self.site.id) |
There was a problem hiding this comment.
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
.
EdX Release Notice: This PR has been deployed to the production environment. |
No description provided.