feat: save for later (#29089)
send favorite course through email to user VAN-741
This commit is contained in:
2
.github/workflows/pylint-checks.yml
vendored
2
.github/workflows/pylint-checks.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- module-name: lms-1
|
||||
path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/"
|
||||
path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/ lms/djangoapps/save_for_later/"
|
||||
- module-name: lms-2
|
||||
path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py"
|
||||
- module-name: openedx-1
|
||||
|
||||
2
.github/workflows/unit-tests.yml
vendored
2
.github/workflows/unit-tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
"lms/djangoapps/courseware/",
|
||||
"lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/",
|
||||
"lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/",
|
||||
"lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy/ lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py",
|
||||
"lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy/ lms/djangoapps/save_for_later/ lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py",
|
||||
"openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/coursegraph/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/demographics/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/",
|
||||
"openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/self_paced/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/tests/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/",
|
||||
"cms/djangoapps/api/ cms/djangoapps/cms_user_tasks/ cms/djangoapps/course_creators/ cms/djangoapps/export_course_metadata/ cms/djangoapps/maintenance/ cms/djangoapps/models/ cms/djangoapps/pipeline_js/ cms/djangoapps/xblock_config/ cms/envs/ cms/lib/",
|
||||
|
||||
0
lms/djangoapps/save_for_later/__init__.py
Normal file
0
lms/djangoapps/save_for_later/__init__.py
Normal file
0
lms/djangoapps/save_for_later/api/__init__.py
Normal file
0
lms/djangoapps/save_for_later/api/__init__.py
Normal file
12
lms/djangoapps/save_for_later/api/urls.py
Normal file
12
lms/djangoapps/save_for_later/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
URL definitions for the save_for_later API.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
app_name = 'lms.djangoapps.save_for_later'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v1/', include(('lms.djangoapps.save_for_later.api.v1.urls', 'v1'), namespace='v1')),
|
||||
]
|
||||
0
lms/djangoapps/save_for_later/api/v1/__init__.py
Normal file
0
lms/djangoapps/save_for_later/api/v1/__init__.py
Normal file
94
lms/djangoapps/save_for_later/api/v1/tests/test_views.py
Normal file
94
lms/djangoapps/save_for_later/api/v1/tests/test_views.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Tests for /save/course/ API.
|
||||
"""
|
||||
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from django.test.utils import override_settings
|
||||
from rest_framework.test import APITestCase
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from common.djangoapps.third_party_auth.tests.testutil import ThirdPartyAuthTestMixin
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@ddt.ddt
|
||||
class SaveForLaterApiViewTest(ThirdPartyAuthTestMixin, APITestCase):
|
||||
"""
|
||||
Save for later tests
|
||||
"""
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Test Setup
|
||||
"""
|
||||
super().setUp()
|
||||
|
||||
self.url = reverse('api:v1:save_course')
|
||||
self.email = 'test@edx.org'
|
||||
self.invalid_email = 'test@edx'
|
||||
self.course_id = 'course-v1:TestX+ProEnroll+P'
|
||||
self.org_img_url = '/path/logo.png'
|
||||
self.course_key = CourseKey.from_string(self.course_id)
|
||||
CourseOverviewFactory.create(id=self.course_key)
|
||||
|
||||
def test_save_course_using_email(self):
|
||||
"""
|
||||
Test successfully email sent
|
||||
"""
|
||||
request_payload = {
|
||||
'email': self.email,
|
||||
'course_id': self.course_id,
|
||||
'marketing_url': 'http://google.com',
|
||||
'org_img_url': self.org_img_url,
|
||||
}
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code == 200
|
||||
|
||||
@override_settings(
|
||||
CACHES={
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'registration_proxy',
|
||||
}
|
||||
}
|
||||
)
|
||||
def test_save_for_later_api_rate_limiting(self):
|
||||
"""
|
||||
Test api rate limit
|
||||
"""
|
||||
request_payload = {
|
||||
'email': self.email,
|
||||
'course_id': self.course_id,
|
||||
'marketing_url': 'http://google.com',
|
||||
'org_img_url': self.org_img_url,
|
||||
}
|
||||
for _ in range(int(settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT.split('/')[0])):
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code != 403
|
||||
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code == 403
|
||||
cache.clear()
|
||||
|
||||
for _ in range(int(settings.SAVE_FOR_LATER_IP_RATE_LIMIT.split('/')[0])):
|
||||
request_payload['email'] = 'test${_}@edx.org'.format(_=_)
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code != 403
|
||||
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code == 403
|
||||
cache.clear()
|
||||
|
||||
def test_invalid_email_address(self):
|
||||
"""
|
||||
Test email validation
|
||||
"""
|
||||
request_payload = {'email': self.invalid_email, 'course_id': self.course_id}
|
||||
response = self.client.post(self.url, data=request_payload)
|
||||
assert response.status_code == 400
|
||||
12
lms/djangoapps/save_for_later/api/v1/urls.py
Normal file
12
lms/djangoapps/save_for_later/api/v1/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
URLs for save_for_later v1
|
||||
"""
|
||||
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from lms.djangoapps.save_for_later.api.v1.views import SaveForLaterApiView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^save/course/', SaveForLaterApiView.as_view(), name='save_course'),
|
||||
]
|
||||
119
lms/djangoapps/save_for_later/api/v1/views.py
Normal file
119
lms/djangoapps/save_for_later/api/v1/views.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Save for later views
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.sites.models import Site
|
||||
from ratelimit.decorators import ratelimit
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.db import transaction
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from edx_ace import ace
|
||||
from edx_ace.recipient import Recipient
|
||||
|
||||
from common.djangoapps.track import segment
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.accounts.api import get_email_validation_error
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
|
||||
from lms.djangoapps.save_for_later.message_types import SaveForLater
|
||||
from lms.djangoapps.save_for_later.models import SavedCourse
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POST_EMAIL_KEY = 'post:email'
|
||||
REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip'
|
||||
|
||||
|
||||
class SaveForLaterApiView(APIView):
|
||||
"""
|
||||
API VIEW
|
||||
"""
|
||||
@transaction.atomic
|
||||
@method_decorator(ratelimit(key=POST_EMAIL_KEY, rate=settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT, method='POST'))
|
||||
@method_decorator(ratelimit(key=REAL_IP_KEY, rate=settings.SAVE_FOR_LATER_IP_RATE_LIMIT, method='POST'))
|
||||
def post(self, request):
|
||||
"""
|
||||
**Use Case**
|
||||
|
||||
* Send favorite course through email to user for later learning.
|
||||
|
||||
**Example Request**
|
||||
|
||||
POST /api/v1/save/course/
|
||||
|
||||
**Example POST Request**
|
||||
|
||||
{
|
||||
"email": "test@edx.org",
|
||||
"course_id": "course-v1:edX+DemoX+2021",
|
||||
"marketing_url": "https://test.com",
|
||||
"org_img_url": "https://test.com/logo.png"
|
||||
|
||||
}
|
||||
"""
|
||||
user = request.user
|
||||
course_id = request.POST.get('course_id')
|
||||
email = request.POST.get('email')
|
||||
marketing_url = request.POST.get('marketing_url')
|
||||
org_img_url = request.POST.get('org_img_url')
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
if getattr(request, 'limited', False):
|
||||
return Response({'result': 'failure'}, status=403)
|
||||
|
||||
if get_email_validation_error(email):
|
||||
return Response({'result': 'failure'}, status=400)
|
||||
|
||||
try:
|
||||
course_overview = CourseOverview.get_from_id(course_key)
|
||||
SavedCourse.objects.update_or_create(
|
||||
user_id=user.id,
|
||||
email=email,
|
||||
course_id=course_id,
|
||||
)
|
||||
except CourseOverview.DoesNotExist:
|
||||
return Response({'result': 'failure'}, status=404)
|
||||
|
||||
site = Site.objects.get_current()
|
||||
message_context = get_base_template_context(site)
|
||||
message_context.update({
|
||||
'course_image_url': course_overview.course_image_url,
|
||||
'partner_image_url': org_img_url,
|
||||
'enroll_course_url': '/register?course_id={course_key}&enrollment_action=enroll&email_opt_in=false&'
|
||||
'save_for_later=true'.format(course_key=course_key),
|
||||
'view_course_url': marketing_url + '?save_for_later=true' if marketing_url else '#',
|
||||
'course_key': course_key,
|
||||
'display_name': course_overview.display_name,
|
||||
'short_description': course_overview.short_description,
|
||||
'lms_url': configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL),
|
||||
'from_address': configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL),
|
||||
})
|
||||
|
||||
msg = SaveForLater().personalize(
|
||||
recipient=Recipient(lms_user_id=0, email_address=email),
|
||||
language=settings.LANGUAGE_CODE,
|
||||
user_context=message_context,
|
||||
)
|
||||
try:
|
||||
ace.send(msg)
|
||||
segment.track(
|
||||
user.id,
|
||||
'edx.bi.user.sent.email.save.for.later',
|
||||
{
|
||||
'category': 'save-for-later',
|
||||
'type': 'course' if course_id else 'program'
|
||||
}
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.warning('Unable to send save for later email ', exc_info=True)
|
||||
return Response({'result': 'failure'}, status=400)
|
||||
else:
|
||||
return Response({'result': 'success'}, status=200)
|
||||
14
lms/djangoapps/save_for_later/apps.py
Normal file
14
lms/djangoapps/save_for_later/apps.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
save_for_later Application Configuration
|
||||
"""
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SaveForLaterConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for save_for_later.
|
||||
"""
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'lms.djangoapps.save_for_later'
|
||||
13
lms/djangoapps/save_for_later/message_types.py
Normal file
13
lms/djangoapps/save_for_later/message_types.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
ACE message types for the save_for_later module.
|
||||
"""
|
||||
|
||||
|
||||
from openedx.core.djangoapps.ace_common.message import BaseMessageType
|
||||
|
||||
|
||||
class SaveForLater(BaseMessageType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.options['transactional'] = True
|
||||
48
lms/djangoapps/save_for_later/migrations/0001_initial.py
Normal file
48
lms/djangoapps/save_for_later/migrations/0001_initial.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.8 on 2021-11-15 09:01
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import opaque_keys.edx.django.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SavedProgram',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('email', models.EmailField(db_index=True, max_length=254)),
|
||||
('program_uuid', models.UUIDField()),
|
||||
('user_id', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SavedCourse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('email', models.EmailField(db_index=True, max_length=254)),
|
||||
('course_id', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
|
||||
('user_id', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
20
lms/djangoapps/save_for_later/models.py
Normal file
20
lms/djangoapps/save_for_later/models.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Django ORM models for save_for_later APP
|
||||
"""
|
||||
|
||||
|
||||
from model_utils.models import TimeStampedModel
|
||||
from django.db import models
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
|
||||
class SavedCourse(TimeStampedModel):
|
||||
user_id = models.IntegerField(null=True, blank=True)
|
||||
email = models.EmailField(db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
|
||||
|
||||
class SavedProgram(TimeStampedModel):
|
||||
user_id = models.IntegerField(null=True, blank=True)
|
||||
email = models.EmailField(db_index=True)
|
||||
program_uuid = models.UUIDField()
|
||||
7
lms/djangoapps/save_for_later/urls.py
Normal file
7
lms/djangoapps/save_for_later/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
""" URLs for save_for_later """
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^api/', include(('lms.djangoapps.save_for_later.api.urls', 'api'), namespace='api')),
|
||||
]
|
||||
@@ -1001,6 +1001,9 @@ MARKETING_EMAILS_OPT_IN = False
|
||||
# .. toggle_tickets: 'https://openedx.atlassian.net/browse/VAN-622'
|
||||
ENABLE_COPPA_COMPLIANCE = False
|
||||
|
||||
# VAN-741 - save for later api put behind a flag to make it only available for edX
|
||||
ENABLE_SAVE_FOR_LATER = False
|
||||
|
||||
############################# SET PATH INFORMATION #############################
|
||||
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
@@ -3194,6 +3197,9 @@ INSTALLED_APPS = [
|
||||
|
||||
# For edx ace template tags
|
||||
'edx_ace',
|
||||
|
||||
# For save for later
|
||||
'lms.djangoapps.save_for_later'
|
||||
]
|
||||
|
||||
######################### CSRF #########################################
|
||||
@@ -4561,6 +4567,10 @@ RESET_PASSWORD_API_RATELIMIT = '30/7d'
|
||||
PASSWORD_RESET_IP_RATE = '1/m'
|
||||
PASSWORD_RESET_EMAIL_RATE = '2/h'
|
||||
|
||||
#### SAVE FOR LATER EMAIL RATE LIMIT SETTINGS ####
|
||||
SAVE_FOR_LATER_IP_RATE_LIMIT = '100/d'
|
||||
SAVE_FOR_LATER_EMAIL_RATE_LIMIT = '5/m'
|
||||
|
||||
############### Settings for Retirement #####################
|
||||
# .. setting_name: RETIRED_USERNAME_PREFIX
|
||||
# .. setting_default: retired__user_
|
||||
|
||||
@@ -613,3 +613,10 @@ RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = '2/m'
|
||||
RESET_PASSWORD_API_RATELIMIT = '2/m'
|
||||
|
||||
CORS_ORIGIN_WHITELIST = ['https://sandbox.edx.org']
|
||||
|
||||
# enable /api/v1/save/course/ api for testing
|
||||
ENABLE_SAVE_FOR_LATER = True
|
||||
|
||||
# rate limit for /api/v1/save/course/ api
|
||||
SAVE_FOR_LATER_IP_RATE_LIMIT = '5/d'
|
||||
SAVE_FOR_LATER_EMAIL_RATE_LIMIT = '5/m'
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_body.html' %}
|
||||
|
||||
{% load django_markup %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<div style="display: flex;">
|
||||
<div style="background-color: #1C8DBE;flex: 50%;">
|
||||
<div style="text-align: center;">
|
||||
<h1 style="color: #fff;
|
||||
line-height: 40px;
|
||||
font-size: 36px;
|
||||
padding-top: 110px;
|
||||
padding-bottom: 24px;
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
min-width: 256px;
|
||||
margin: 0;
|
||||
text-align: left;"
|
||||
>
|
||||
{% filter force_escape %}
|
||||
{% blocktrans %}Check out this course on edx{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
</h1>
|
||||
<div style="text-align: left;
|
||||
padding-left: 32px;"
|
||||
>
|
||||
<a style="background: #00688D;
|
||||
width: 94px;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 2.25rem;
|
||||
height: 36px;
|
||||
padding: 8px 12px;"
|
||||
href="{{ lms_url|add:enroll_course_url }}"
|
||||
>
|
||||
{% filter force_escape %}
|
||||
{% blocktrans %}Enroll now{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
</a>
|
||||
<a style="background: #03c7e8;
|
||||
border: 1px solid #fff;
|
||||
margin-left: 8px;
|
||||
width: 107px;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 2.25rem;
|
||||
height: 36px;
|
||||
padding: 8px 12px;"
|
||||
href="{{ view_course_url }}"
|
||||
>
|
||||
{% filter force_escape %}
|
||||
{% blocktrans %}View Course{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 50%;">
|
||||
<div style="height: 140px;min-width: 320px;background: url({{lms_url|add:course_image_url}})">
|
||||
{% if partner_image_url %}
|
||||
<div style="padding-top: 82px;padding-left: 24px;">
|
||||
<img style="width: 116px;
|
||||
height: 66px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15), 0px 1px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
"src={{partner_image_url}} />
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="background: #FBFAF9;">
|
||||
<h2 style="color: #002121;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
padding-top: 24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-bottom: 20px;
|
||||
margin: 0;"
|
||||
>
|
||||
{{ display_name }}
|
||||
</h2>
|
||||
<p style="color: #707070;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-bottom: 20px;
|
||||
margin: 0;"
|
||||
>
|
||||
{{ short_description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background-color: #F2F0EF;display: flex;color: #00262B;">
|
||||
<div style="width: 33.3%;">
|
||||
<div style="display: inline-block;">
|
||||
<svg width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
style="margin-bottom: 5px;margin-left: 20px;"
|
||||
>
|
||||
<path d="M16.24 7.76A5.974 5.974 0 0012 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0a5.99 5.99 0 00-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="display: inline-block;margin-left: 5px !important">
|
||||
<p style="padding-top: 8px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 24px;"
|
||||
>
|
||||
{% trans "Estimated 6 weeks" as estimated_weeks_heading %}{{ estimated_weeks_heading | force_escape }}
|
||||
</p>
|
||||
<p style=" margin: 0;
|
||||
padding-bottom: 9px;
|
||||
font-size: 12px;
|
||||
color: #707070;"
|
||||
>
|
||||
{% trans "4-6 hours per week" as estimated_weeks_msg %}{{ estimated_weeks_msg | force_escape }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 33.3%;">
|
||||
<div style="display: inline-block;">
|
||||
<svg width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
style="margin-bottom: 5px;"
|
||||
>
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="display: inline-block;margin-left: 5px;">
|
||||
<p style="padding-top: 8px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 24px;"
|
||||
>
|
||||
{% trans "Self-paced" as self_paced_heading %}{{ self_paced_heading | force_escape }}
|
||||
</p>
|
||||
<p style=" margin: 0;
|
||||
padding-bottom: 9px;
|
||||
font-size: 12px;
|
||||
color: #707070;"
|
||||
>
|
||||
{% trans "Progress at your own speed" as self_paced_msg %}{{ self_paced_msg | force_escape }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 33.3%">
|
||||
<div style="display: inline-block;">
|
||||
<svg width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
style="margin-bottom: 5px;"
|
||||
>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.73-2.77-.01-2.2-1.9-2.96-3.66-3.42z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="display: inline-block;margin-left: 5px;">
|
||||
<p style="padding-top: 8px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 24px;"
|
||||
>
|
||||
{% trans "Free" as upgrade_heading %}{{ upgrade_heading | force_escape }}
|
||||
</p>
|
||||
<p style=" margin: 0;
|
||||
padding-bottom: 9px;
|
||||
font-size: 12px;
|
||||
color: #707070;"
|
||||
>
|
||||
{% trans "Optional upgrade available" as upgrade_msg %}{{ upgrade_msg | force_escape }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}Check out this course on {{ platform_name }}.{% endblocktrans %}
|
||||
|
||||
{% blocktrans %}This email message was automatically sent by {{ lms_url }} {% endblocktrans %}
|
||||
|
||||
{{ confirm_link }}
|
||||
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ platform_name }}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_head.html' %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans trimmed %} {{ display_name }} {% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -1020,3 +1020,9 @@ if getattr(settings, 'PROVIDER_STATES_URL', None):
|
||||
name='courseware_xblock_handler_provider_state',
|
||||
)
|
||||
]
|
||||
|
||||
# save_for_later API urls
|
||||
if settings.ENABLE_SAVE_FOR_LATER:
|
||||
urlpatterns += [
|
||||
path('', include('lms.djangoapps.save_for_later.urls')),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user