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.
This part needs to be site aware as well, because individual sites manage specific courses.
There was a problem hiding this comment.
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.
How has these migration changes been tested?
There was a problem hiding this comment.
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.
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.
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.
How is this actually altering the field? Is it not declared the same as before?
There was a problem hiding this comment.
I've added comments explaining this.
| course_id = self.cleaned_data['course'] | ||
| try: | ||
| course_key = CourseKey.from_string(course_id) | ||
| except InvalidKeyError: |
There was a problem hiding this comment.
Do we still want the call to the modulestore below? Or, should it be checking CourseOverview instead?
There was a problem hiding this comment.
I'd rather not muck with that logic right now.
| 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.
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.
The admin form widget for a ForeignKey just uses the key to the other table, not the actual object.
| 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.
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.
Nit: consider DRYing the redundant setup of these test methods.
| 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.
Wait. Shouldn't enrollment.course work now?
There was a problem hiding this comment.
.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) No newline at end of file |
There was a problem hiding this comment.
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.
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.
| 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.
Why were tests moved to an init.py file?
There was a problem hiding this comment.
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.
| enrollment=enrollment, | ||
| active=True, | ||
| start=timezone.now(), | ||
| upgrade_deadline=timezone.now() + datetime.timedelta(days=21) |
There was a problem hiding this comment.
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.
Nit: add an underscore between recurring and nudge for readability.
There was a problem hiding this comment.
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.
Does this call to ScheduleConfig need to pass a value for site?
| 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.
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.
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.
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.
Use datetime.strptime instead?
There was a problem hiding this comment.
I don't think you can force strptime to be a timezone aware UTC date, unfortunately.
There was a problem hiding this comment.
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.
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.
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.
Yes, it's still in work-in-progress.
| @@ -0,0 +1,3 @@ | |||
| Dear {{student_name}}, | |||
There was a problem hiding this comment.
Why do we need a subfolder named "schedules" within a folder already named "schedules"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Q: does this need a -- prefix?
There was a problem hiding this comment.
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.
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.
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.
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.