Commit cd4ebaf2 by Calen Pennington Committed by Clinton Blackburn

Only send one email even if there are multiple enrollments by the same user on the same day

parent 8c37dff1
...@@ -9,12 +9,12 @@ from bulk_email.models import Optout ...@@ -9,12 +9,12 @@ from bulk_email.models import Optout
class CourseEmailOptout(Policy): class CourseEmailOptout(Policy):
def check(self, message): def check(self, message):
course_id = message.context.get('course_id') course_ids = message.context.get('course_ids')
if not course_id: if not course_ids:
return PolicyResult(deny=frozenset()) return PolicyResult(deny=frozenset())
course_key = CourseKey.from_string(course_id) course_keys = [CourseKey.from_string(course_id) for course_id in course_ids]
if Optout.objects.filter(user__username=message.recipient.username, course_id=course_key).exists(): if Optout.objects.filter(user__username=message.recipient.username, course_id__in=course_keys).count() == len(course_keys):
return PolicyResult(deny={ChannelType.EMAIL}) return PolicyResult(deny={ChannelType.EMAIL})
return PolicyResult(deny=frozenset()) return PolicyResult(deny=frozenset())
...@@ -166,7 +166,7 @@ class TestACEOptoutCourseEmails(ModuleStoreTestCase): ...@@ -166,7 +166,7 @@ class TestACEOptoutCourseEmails(ModuleStoreTestCase):
email_address=self.student.email, email_address=self.student.email,
), ),
context={ context={
'course_id': str(self.course.id) 'course_ids': [str(self.course.id)]
}, },
) )
......
...@@ -28,3 +28,7 @@ class CourseOverviewFactory(DjangoModelFactory): ...@@ -28,3 +28,7 @@ class CourseOverviewFactory(DjangoModelFactory):
@factory.lazy_attribute @factory.lazy_attribute
def id(self): def id(self):
return CourseLocator(self.org, 'toy', '2012_Fall') return CourseLocator(self.org, 'toy', '2012_Fall')
@factory.lazy_attribute
def display_name(self):
return "{} Course".format(self.id)
import datetime import datetime
from mock import patch, Mock import itertools
from unittest import skipUnless from unittest import skipUnless
import pytz
import attr
import ddt import ddt
import pytz
from django.conf import settings from django.conf import settings
from django.test import override_settings
from edx_ace.channel import ChannelType
from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies
from edx_ace.utils.date import serialize from edx_ace.utils.date import serialize
from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from openedx.core.djangoapps.schedules import tasks from openedx.core.djangoapps.schedules import tasks
from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory, ScheduleConfigFactory from student.tests.factories import UserFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
@ddt.ddt @ddt.ddt
@skip_unless_lms @skip_unless_lms
@skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS, "Can't test schedules if the app isn't installed") @skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
"Can't test schedules if the app isn't installed")
class TestSendRecurringNudge(CacheIsolationTestCase): class TestSendRecurringNudge(CacheIsolationTestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
def setUp(self): def setUp(self):
...@@ -71,7 +76,7 @@ class TestSendRecurringNudge(CacheIsolationTestCase): ...@@ -71,7 +76,7 @@ class TestSendRecurringNudge(CacheIsolationTestCase):
test_time_str = serialize(datetime.datetime(2017, 8, 1, 18, tzinfo=pytz.UTC)) test_time_str = serialize(datetime.datetime(2017, 8, 1, 18, tzinfo=pytz.UTC))
with self.assertNumQueries(1): with self.assertNumQueries(1):
tasks.recurring_nudge_schedule_hour( tasks.recurring_nudge_schedule_hour(
self.site_config.site, 3, test_time_str, [schedules[0].enrollment.course.org], self.site_config.site.id, 3, test_time_str, [schedules[0].enrollment.course.org],
) )
self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count) self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count)
self.assertFalse(mock_ace.send.called) self.assertFalse(mock_ace.send.called)
...@@ -88,7 +93,7 @@ class TestSendRecurringNudge(CacheIsolationTestCase): ...@@ -88,7 +93,7 @@ class TestSendRecurringNudge(CacheIsolationTestCase):
test_time_str = serialize(datetime.datetime(2017, 8, 1, 20, tzinfo=pytz.UTC)) test_time_str = serialize(datetime.datetime(2017, 8, 1, 20, tzinfo=pytz.UTC))
with self.assertNumQueries(1): with self.assertNumQueries(1):
tasks.recurring_nudge_schedule_hour( tasks.recurring_nudge_schedule_hour(
self.site_config.site, 3, test_time_str, [schedule.enrollment.course.org], self.site_config.site.id, 3, test_time_str, [schedule.enrollment.course.org],
) )
# There is no database constraint that enforces that enrollment.course_id points # There is no database constraint that enforces that enrollment.course_id points
...@@ -136,27 +141,15 @@ class TestSendRecurringNudge(CacheIsolationTestCase): ...@@ -136,27 +141,15 @@ class TestSendRecurringNudge(CacheIsolationTestCase):
for config in (limited_config, unlimited_config): for config in (limited_config, unlimited_config):
ScheduleConfigFactory.create(site=config.site) ScheduleConfigFactory.create(site=config.site)
filtered_sched = ScheduleFactory.create( ScheduleFactory.create(
start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=filtered_org, enrollment__course__org=filtered_org,
) )
unfiltered_scheds = [ for _ in range(2):
ScheduleFactory.create( ScheduleFactory.create(
start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org, enrollment__course__org=unfiltered_org,
) )
for _ in range(2)
]
print(filtered_sched.enrollment)
print(filtered_sched.enrollment.course)
print(filtered_sched.enrollment.course.org)
print(unfiltered_scheds[0].enrollment)
print(unfiltered_scheds[0].enrollment.course)
print(unfiltered_scheds[0].enrollment.course.org)
print(unfiltered_scheds[1].enrollment)
print(unfiltered_scheds[1].enrollment.course)
print(unfiltered_scheds[1].enrollment.course.org)
test_time_str = serialize(datetime.datetime(2017, 8, 2, 17, tzinfo=pytz.UTC)) test_time_str = serialize(datetime.datetime(2017, 8, 2, 17, tzinfo=pytz.UTC))
with self.assertNumQueries(1): with self.assertNumQueries(1):
...@@ -164,6 +157,77 @@ class TestSendRecurringNudge(CacheIsolationTestCase): ...@@ -164,6 +157,77 @@ class TestSendRecurringNudge(CacheIsolationTestCase):
limited_config.site.id, 3, test_time_str, org_list=org_list, exclude_orgs=exclude_orgs, limited_config.site.id, 3, test_time_str, org_list=org_list, exclude_orgs=exclude_orgs,
) )
print(mock_schedule_send.mock_calls)
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count) self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count)
self.assertFalse(mock_ace.send.called) self.assertFalse(mock_ace.send.called)
@ddt.data(
(19, 1),
(20, 0),
(21, 0)
)
@ddt.unpack
@patch.object(tasks, 'ace')
@patch.object(tasks, '_recurring_nudge_schedule_send')
def test_multiple_enrollments(self, test_hour, messages_sent, mock_schedule_send, mock_ace):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 1, hour, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Hour{}'.format(hour))
)
for hour in (19, 20, 21)
]
test_time_str = serialize(datetime.datetime(2017, 8, 1, test_hour, tzinfo=pytz.UTC))
with self.assertNumQueries(1):
tasks.recurring_nudge_schedule_hour(
self.site_config.site.id, 3, test_time_str, [schedules[0].enrollment.course.org],
)
self.assertEqual(mock_schedule_send.apply_async.call_count, messages_sent)
self.assertFalse(mock_ace.send.called)
@ddt.data(*itertools.product((1, 10, 100), (3, 10)))
@ddt.unpack
@override_settings()
def test_templates(self, message_count, day):
settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 1, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Hour{}'.format(idx))
)
for idx in range(message_count)
]
test_time_str = serialize(datetime.datetime(2017, 8, 1, 19, tzinfo=pytz.UTC))
patch_policies(self, [StubPolicy([ChannelType.PUSH])])
mock_channel = Mock(
name='test_channel',
channel_type=ChannelType.EMAIL
)
patch_channels(self, [mock_channel])
sent_messages = []
with patch.object(tasks, '_recurring_nudge_schedule_send') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
with self.assertNumQueries(1):
tasks.recurring_nudge_schedule_hour(
self.site_config.site.id, day, test_time_str, [schedules[0].enrollment.course.org],
)
self.assertEqual(len(sent_messages), 1)
for args in sent_messages:
tasks._recurring_nudge_schedule_send(*args)
self.assertEqual(mock_channel.deliver.call_count, 1)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
for template in attr.astuple(email):
self.assertNotIn("TEMPLATE WARNING", template)
import datetime import datetime
from subprocess import check_output, CalledProcessError from itertools import groupby
from logging import getLogger
from urlparse import urlparse from urlparse import urlparse
from celery.task import task from celery.task import task
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Min
from django.db.utils import DatabaseError from django.db.utils import DatabaseError
from django.utils.http import urlquote from django.utils.http import urlquote
from logging import getLogger
from edx_ace import ace from edx_ace import ace
from edx_ace.message import MessageType, Message from edx_ace.message import Message, MessageType
from edx_ace.recipient import Recipient from edx_ace.recipient import Recipient
from edx_ace.utils.date import deserialize from edx_ace.utils.date import deserialize
from edxmako.shortcuts import marketing_link
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
from edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
log = getLogger(__name__) log = getLogger(__name__)
...@@ -84,14 +84,33 @@ def _recurring_nudge_schedule_send(site_id, msg_str): ...@@ -84,14 +84,33 @@ def _recurring_nudge_schedule_send(site_id, msg_str):
def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=False): def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=False):
beginning_of_day = target_hour.replace(hour=0, minute=0, second=0)
users = User.objects.filter(
courseenrollment__schedule__start__gte=beginning_of_day,
courseenrollment__schedule__start__lt=beginning_of_day + datetime.timedelta(days=1),
courseenrollment__is_active=True,
).annotate(
first_schedule=Min('courseenrollment__schedule__start')
).filter(
first_schedule__gte=target_hour,
first_schedule__lt=target_hour + datetime.timedelta(minutes=60)
)
if org_list is not None:
if exclude_orgs:
users = users.exclude(courseenrollment__course__org__in=org_list)
else:
users = users.filter(courseenrollment__course__org__in=org_list)
schedules = Schedule.objects.select_related( schedules = Schedule.objects.select_related(
'enrollment__user__profile', 'enrollment__user__profile',
'enrollment__course', 'enrollment__course',
).filter( ).filter(
start__gte=target_hour, enrollment__user__in=users,
start__lt=target_hour + datetime.timedelta(minutes=60), start__gte=beginning_of_day,
start__lt=beginning_of_day + datetime.timedelta(days=1),
enrollment__is_active=True, enrollment__is_active=True,
) ).order_by('enrollment__user__id')
if org_list is not None: if org_list is not None:
if exclude_orgs: if exclude_orgs:
...@@ -102,20 +121,24 @@ def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=Fals ...@@ -102,20 +121,24 @@ def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=Fals
if "read_replica" in settings.DATABASES: if "read_replica" in settings.DATABASES:
schedules = schedules.using("read_replica") schedules = schedules.using("read_replica")
for schedule in schedules: dashboard_relative_url = reverse('dashboard')
enrollment = schedule.enrollment
user = enrollment.user
course_id_str = str(enrollment.course_id) for (user, user_schedules) in groupby(schedules, lambda s: s.enrollment.user):
course = enrollment.course user_schedules = list(user_schedules)
course_id_strs = [str(schedule.enrollment.course_id) for schedule in user_schedules]
course_root_relative_url = reverse('course_root', args=[course_id_str]) def absolute_url(relative_path):
dashboard_relative_url = reverse('dashboard') return u'{}{}'.format(settings.LMS_ROOT_URL, urlquote(relative_path))
first_schedule = user_schedules[0]
template_context = { template_context = {
'student_name': user.profile.name, 'student_name': user.profile.name,
'course_name': course.display_name,
'course_url': absolute_url(course_root_relative_url), 'course_name': first_schedule.enrollment.course.display_name,
'course_url': absolute_url(reverse('course_root', args=[str(first_schedule.enrollment.course_id)])),
# This is used by the bulk email optout policy
'course_ids': course_id_strs,
# Platform information # Platform information
'homepage_url': encode_url(marketing_link('ROOT')), 'homepage_url': encode_url(marketing_link('ROOT')),
...@@ -125,12 +148,8 @@ def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=Fals ...@@ -125,12 +148,8 @@ def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=Fals
'contact_mailing_address': settings.CONTACT_MAILING_ADDRESS, 'contact_mailing_address': settings.CONTACT_MAILING_ADDRESS,
'social_media_urls': encode_urls_in_dict(getattr(settings, 'SOCIAL_MEDIA_FOOTER_URLS', {})), 'social_media_urls': encode_urls_in_dict(getattr(settings, 'SOCIAL_MEDIA_FOOTER_URLS', {})),
'mobile_store_urls': encode_urls_in_dict(getattr(settings, 'MOBILE_STORE_URLS', {})), 'mobile_store_urls': encode_urls_in_dict(getattr(settings, 'MOBILE_STORE_URLS', {})),
# This is used by the bulk email optout policy
'course_id': course_id_str,
} }
yield (user, first_schedule.enrollment.course.language, template_context)
yield (user, course.language, template_context)
def encode_url(url): def encode_url(url):
......
...@@ -2,10 +2,17 @@ ...@@ -2,10 +2,17 @@
{% load i18n %} {% load i18n %}
{% block preview_text %} {% block preview_text %}
{% blocktrans trimmed %} {% if courses|length > 1 %}
Keep up the momentum! Many edX learners in {{course_name}} are completing more problems every week, and {% blocktrans trimmed %}
participating in the discussion forums. What do you want to do to keep learning? Many {{ platform_name }} learners are completing more problems every week, and
{% endblocktrans %} participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
Many {{ platform_name }} learners in {{course_name}} are completing more problems every week, and
participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
...@@ -15,14 +22,27 @@ ...@@ -15,14 +22,27 @@
<h1>{% trans "Keep up the momentum!" %}</h1> <h1>{% trans "Keep up the momentum!" %}</h1>
<p> <p>
{% blocktrans trimmed %} {% if courses|length > 1 %}
Many edX learners in <strong>{{course_name}}</strong> are completing more problems every week, and {% blocktrans trimmed %}
participating in the discussion forums. What do you want to do to keep learning? Many edX learners are completing more problems every week, and
{% endblocktrans %} participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
Many edX learners in <strong>{{course_name}}</strong> are completing more problems every week, and
participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% endif %}
</p> </p>
<p> <p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles --> <!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<a href="{{ course_url }}" style=" <a
{% if courses|length > 1 %}
href="{{ dashboard_url }}"
{% else %}
href="{{ course_url }}"
{% endif %}
style="
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 4px;
...@@ -42,4 +62,4 @@ ...@@ -42,4 +62,4 @@
</td> </td>
</tr> </tr>
</table> </table>
{% endblock %} {% endblock %}
\ No newline at end of file
{% load i18n %} {% load i18n %}
{% blocktrans trimmed %} {% if courses|length > 1 %}
Keep up the momentum! Many edX learners in {{course_name}} are completing more problems every week, and {% blocktrans trimmed %}
participating in the discussion forums. What do you want to do to keep learning? Many edX learners are completing more problems every week, and
{% endblocktrans %} participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% trans "Keep learning" %} <{{course_url}}> {% trans "Keep learning" %} <{{dashboard_url}}>
\ No newline at end of file {% else %}
{% blocktrans trimmed %}
Many edX learners in {{course_name}} are completing more problems every week, and
participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% trans "Keep learning" %} <{{course_url}}>
{% endif %}
{{ course_name }} {% if courses|length > 1 %}
\ No newline at end of file {{ platform_name }}
{% else %}
{{ course_name }}
{% endif %}
...@@ -2,10 +2,17 @@ ...@@ -2,10 +2,17 @@
{% load i18n %} {% load i18n %}
{% block preview_text %} {% block preview_text %}
{% blocktrans trimmed %} {% if courses|length > 1 %}
Keep learning today. Remember when you enrolled in {{course_name}} on edX.org? We do, and we’re glad {% blocktrans trimmed %}
to have you! Come see what everyone is learning. Remember when you enrolled in {{ course_name }}, and other courses on edX.org? We do, and we’re glad
{% endblocktrans %} to have you! Come see what everyone is learning.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
Remember when you enrolled in {{ course_name }} on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
...@@ -15,15 +22,28 @@ ...@@ -15,15 +22,28 @@
<h1>{% trans "Keep learning today" %}.</h1> <h1>{% trans "Keep learning today" %}.</h1>
<p> <p>
{% blocktrans trimmed %} {% if courses|length > 1 %}
Remember when you enrolled in <strong>{{ course_name }}</strong> on edX.org? We do, and we’re glad {% blocktrans trimmed %}
to have you! Come see what everyone is learning. Remember when you enrolled in <strong>{{ course_name }}</strong>, and other courses on edX.org? We do, and we’re glad
{% endblocktrans %} to have you! Come see what everyone is learning.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
Remember when you enrolled in <strong>{{ course_name }}</strong> on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% endif %}
</p> </p>
<p> <p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles --> <!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<a href="{{ course_url }}" style=" <a
{% if courses|length > 1 %}
href="{{ dashboard_url }}"
{% else %}
href="{{ course_url }}"
{% endif %}
style="
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 4px;
...@@ -37,10 +57,10 @@ ...@@ -37,10 +57,10 @@
display: inline-block; display: inline-block;
"> ">
<!-- old email clients require the use of the font tag :( --> <!-- old email clients require the use of the font tag :( -->
<font color="#ffffff"><b>{% trans "Keep learning" %}</b></font> <font color="#ffffff"><b>{% trans "Start learning now" %}</b></font>
</a> </a>
</p> </p>
</td> </td>
</tr> </tr>
</table> </table>
{% endblock %} {% endblock %}
\ No newline at end of file
{% load i18n %} {% load i18n %}
{% if courses|length > 1 %}
{% blocktrans trimmed %}
Remember when you enrolled in {{ course_name }}, and other courses on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% trans "Start learning now" %} <{{ dashboard_url }}>
{% else %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
Keep learning today. Remember when you enrolled in {{course_name}} on edX.org? We do, and we’re glad Remember when you enrolled in {{ course_name }} on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning. to have you! Come see what everyone is learning.
{% endblocktrans %} {% endblocktrans %}
{% trans "Keep learning" %} <{{course_url}}> {% trans "Start learning now" %} <{{ course_url }}>
\ No newline at end of file {% endif %}
{{ course_name }} {% if courses|length > 1 %}
\ No newline at end of file {{ platform_name }}
{% else %}
{{ course_name }}
{% endif %}
{% load i18n %} {% load i18n %}
{% blocktrans %}Keep learning in {{course_name}}!{% endblocktrans %} {% if courses|length > 1 %}
\ No newline at end of file {% blocktrans %}Keep learning on {{ platform_name }}!{% endblocktrans %}
{% else %}
{% blocktrans %}Keep learning in {{course_name}} !{% endblocktrans %}
{% endif %}
...@@ -38,6 +38,7 @@ django==1.8.18 ...@@ -38,6 +38,7 @@ django==1.8.18
django-waffle==0.12.0 django-waffle==0.12.0
djangorestframework-jwt==1.11.0 djangorestframework-jwt==1.11.0
enum34==1.1.6 enum34==1.1.6
edx-ace==0.1.2
edx-ccx-keys==0.2.1 edx-ccx-keys==0.2.1
edx-celeryutils==0.2.6 edx-celeryutils==0.2.6
edx-drf-extensions==1.2.3 edx-drf-extensions==1.2.3
......
...@@ -97,7 +97,6 @@ git+https://github.com/edx/xblock-utils.git@v1.0.5#egg=xblock-utils==1.0.5 ...@@ -97,7 +97,6 @@ git+https://github.com/edx/xblock-utils.git@v1.0.5#egg=xblock-utils==1.0.5
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1 git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.5#egg=lti_consumer-xblock==1.1.5 git+https://github.com/edx/xblock-lti-consumer.git@v1.1.5#egg=lti_consumer-xblock==1.1.5
git+https://github.com/edx/edx-proctoring.git@1.2.0#egg=edx-proctoring==1.2.0 git+https://github.com/edx/edx-proctoring.git@1.2.0#egg=edx-proctoring==1.2.0
git+https://github.com/edx/edx-ace.git@v0.1.1#egg=edx-ace==0.1.1
# Third Party XBlocks # Third Party XBlocks
git+https://github.com/open-craft/xblock-poll@7ba819b968fe8faddb78bb22e1fe7637005eb414#egg=xblock-poll==1.2.7 git+https://github.com/open-craft/xblock-poll@7ba819b968fe8faddb78bb22e1fe7637005eb414#egg=xblock-poll==1.2.7
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment