Skip to content

Commit

Permalink
Merge branch 'master' into mudassir/logo_url_from_backend_for_batch_e…
Browse files Browse the repository at this point in the history
…nrollment
  • Loading branch information
e0d authored Jul 24, 2024
2 parents e4c62bc + baf5de2 commit ce5bf2a
Show file tree
Hide file tree
Showing 24 changed files with 335 additions and 50 deletions.
1 change: 1 addition & 0 deletions .github/workflows/add-remove-label-on-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ on:
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

2 changes: 1 addition & 1 deletion cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2688,7 +2688,7 @@
############## NOTIFICATIONS EXPIRY ##############
NOTIFICATIONS_EXPIRY = 60
EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000
NOTIFICATION_CREATION_BATCH_SIZE = 83
NOTIFICATION_CREATION_BATCH_SIZE = 76

############################ AI_TRANSLATIONS ##################################
AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1'
Expand Down
4 changes: 3 additions & 1 deletion common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
)
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.discounts.applicability import FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG
from openedx.features.enterprise_support.utils import is_enterprise_learner
from common.djangoapps.student.email_helpers import generate_activation_email_context
from common.djangoapps.student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info
Expand Down Expand Up @@ -206,12 +207,13 @@ def compose_activation_email(
message_context = generate_activation_email_context(user, user_registration)
message_context.update({
'confirm_activation_link': _get_activation_confirmation_link(message_context['key'], redirect_url),
'is_enterprise_learner': is_enterprise_learner(user),
'is_first_purchase_discount_overridden': FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled(),
'route_enabled': route_enabled,
'routed_user': user.username,
'routed_user_email': user.email,
'routed_profile_name': profile_name,
'registration_flow': registration_flow,
'is_enterprise_learner': is_enterprise_learner(user),
'show_auto_generated_username': show_auto_generated_username(user.username),
})

Expand Down
33 changes: 26 additions & 7 deletions lms/djangoapps/course_home_api/outline/tests/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json # lint-amnesty, pylint: disable=wrong-import-order
from completion.models import BlockCompletion
from django.conf import settings # lint-amnesty, pylint: disable=wrong-import-order
from django.test import override_settings
from django.urls import reverse # lint-amnesty, pylint: disable=wrong-import-order
from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order

Expand All @@ -33,7 +34,10 @@
DISPLAY_COURSE_SOCK_FLAG,
ENABLE_COURSE_GOALS
)
from openedx.features.discounts.applicability import DISCOUNT_APPLICABILITY_FLAG
from openedx.features.discounts.applicability import (
DISCOUNT_APPLICABILITY_FLAG,
FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG
)
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order

Expand Down Expand Up @@ -179,17 +183,32 @@ def test_welcome_message(self, welcome_message_is_dismissed):
welcome_message_html = self.client.get(self.url).data['welcome_message_html']
assert welcome_message_html == (None if welcome_message_is_dismissed else '<p>Welcome</p>')

def test_offer(self):
@ddt.data(
(False, 'EDXWELCOME', 15),
(True, 'NOTEDXWELCOME', 30),
)
@ddt.unpack
def test_offer(self, is_fpd_override_waffle_flag_on, fpd_code, fpd_percentage):
"""
Test that the offer data contains the correct code for the first purchase discount,
which can be overriden via a waffle flag from the default EDXWELCOME.
"""
CourseEnrollment.enroll(self.user, self.course.id)

response = self.client.get(self.url)
assert response.data['offer'] is None

with override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True):
response = self.client.get(self.url)

# Just a quick spot check that the dictionary looks like what we expect
assert response.data['offer']['code'] == 'EDXWELCOME'
with override_settings(FIRST_PURCHASE_DISCOUNT_OVERRIDE_CODE='NOTEDXWELCOME'):
with override_settings(FIRST_PURCHASE_DISCOUNT_OVERRIDE_PERCENTAGE=fpd_percentage):
with override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True):
with override_waffle_flag(
FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG, active=is_fpd_override_waffle_flag_on
):
response = self.client.get(self.url)

# Just a quick spot check that the dictionary looks like what we expect
assert response.data['offer']['code'] == fpd_code
assert response.data['offer']['percentage'] == fpd_percentage

def test_access_expiration(self):
enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
Expand Down
14 changes: 10 additions & 4 deletions lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,19 +418,25 @@ class CourseEnrollmentDetailsView(APIView):
This api works with all versions {api_version}, you can use: v0.5, v1, v2 or v3
GET /api/mobile/{api_version}/course_info/{course_id}}/enrollment_details
GET /api/mobile/{api_version}/course_info/{course_id}/enrollment_details
"""
@mobile_course_access()
def get(self, request, course, *args, **kwargs):
def get(self, request, *args, **kwargs):
"""
Handle the GET request
Returns user enrollment and course details.
"""
course_key_string = kwargs.get('course_id')
try:
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError:
error = {'error': f"'{str(course_key_string)}' is not a valid course key."}
return Response(data=error, status=status.HTTP_400_BAD_REQUEST)

data = {
'api_version': self.kwargs.get('api_version'),
'course_id': course.id,
'course_id': course_key,
'user': request.user,
'request': request,
}
Expand Down
79 changes: 79 additions & 0 deletions lms/djangoapps/mobile_api/tests/test_course_info_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for course_info
"""
from datetime import datetime, timedelta
from unittest.mock import patch

import ddt
Expand All @@ -11,6 +12,7 @@
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from milestones.tests.utils import MilestonesTestCaseMixin
from pytz import utc
from rest_framework import status

from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import
Expand All @@ -26,6 +28,7 @@
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import \
SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.xml_importer import import_course_from_xml # lint-amnesty, pylint: disable=wrong-import-order

User = get_user_model()
Expand Down Expand Up @@ -521,3 +524,79 @@ def verify_certificate(self, response, mock_certificate_downloadable_status):
mock_certificate_downloadable_status.assert_called_once()
certificate_url = 'https://test_certificate_url'
assert response.data['certificate'] == {'url': certificate_url}

@patch('lms.djangoapps.mobile_api.course_info.utils.certificate_downloadable_status')
def test_course_not_started(self, mock_certificate_downloadable_status):
""" Test course data which has not started yet """

certificate_url = 'https://test_certificate_url'
mock_certificate_downloadable_status.return_value = {
'is_downloadable': True,
'download_url': certificate_url,
}
now = datetime.now(utc)
course_not_started = CourseFactory.create(
mobile_available=True,
static_asset_path="needed_for_split",
start=now + timedelta(days=5),
)

url = reverse('course-enrollment-details', kwargs={
'api_version': 'v1',
'course_id': course_not_started.id
})

response = self.client.get(path=url)
assert response.status_code == 200
assert response.data['id'] == str(course_not_started.id)

self.verify_course_access_details(response)

@patch('lms.djangoapps.mobile_api.course_info.utils.certificate_downloadable_status')
def test_course_closed(self, mock_certificate_downloadable_status):
""" Test course data whose end date is in past """

certificate_url = 'https://test_certificate_url'
mock_certificate_downloadable_status.return_value = {
'is_downloadable': True,
'download_url': certificate_url,
}
now = datetime.now(utc)
course_closed = CourseFactory.create(
mobile_available=True,
static_asset_path="needed_for_split",
start=now - timedelta(days=250),
end=now - timedelta(days=50),
)

url = reverse('course-enrollment-details', kwargs={
'api_version': 'v1',
'course_id': course_closed.id
})

response = self.client.get(path=url)
assert response.status_code == 200
assert response.data['id'] == str(course_closed.id)

self.verify_course_access_details(response)

@patch('lms.djangoapps.mobile_api.course_info.utils.certificate_downloadable_status')
def test_invalid_course_id(self, mock_certificate_downloadable_status):
""" Test view with invalid course id """

certificate_url = 'https://test_certificate_url'
mock_certificate_downloadable_status.return_value = {
'is_downloadable': True,
'download_url': certificate_url,
}

invalid_id = "invalid" + str(self.course.id)
url = reverse('course-enrollment-details', kwargs={
'api_version': 'v1',
'course_id': invalid_id
})

response = self.client.get(path=url)
assert response.status_code == 400
expected_error = "'{}' is not a valid course key.".format(invalid_id)
assert response.data['error'] == expected_error
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-06-27 20:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('support', '0005_unique_course_id'),
]

operations = [
migrations.AlterField(
model_name='historicalusersocialauth',
name='extra_data',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='historicalusersocialauth',
name='id',
field=models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID'),
),
]
6 changes: 5 additions & 1 deletion lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4295,6 +4295,10 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
}
}

# Enable First Purchase Discount offer override
FIRST_PURCHASE_DISCOUNT_OVERRIDE_CODE = ''
FIRST_PURCHASE_DISCOUNT_OVERRIDE_PERCENTAGE = 15

# E-Commerce API Configuration
ECOMMERCE_PUBLIC_URL_ROOT = 'http://localhost:8002'
ECOMMERCE_API_URL = 'http://localhost:8002/api/v2'
Expand Down Expand Up @@ -5382,7 +5386,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
############## NOTIFICATIONS ##############
NOTIFICATIONS_EXPIRY = 60
EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000
NOTIFICATION_CREATION_BATCH_SIZE = 83
NOTIFICATION_CREATION_BATCH_SIZE = 76
NOTIFICATIONS_DEFAULT_FROM_EMAIL = "no-reply@example.com"
NOTIFICATION_TYPE_ICONS = {}
DEFAULT_NOTIFICATION_ICON_URL = ""
Expand Down
4 changes: 4 additions & 0 deletions openedx/core/djangoapps/discussions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from django.contrib.admin.utils import quote
from simple_history.admin import SimpleHistoryAdmin

from openedx.core.djangoapps.config_model_utils.admin import StackedConfigModelAdmin
Expand All @@ -26,6 +27,9 @@ class DiscussionsConfigurationAdmin(SimpleHistoryAdmin):
'provider_type',
)

def change_view(self, request, object_id=None, form_url="", extra_context=None):
return super().change_view(request, quote(object_id), form_url, extra_context)


class AllowListFilter(SimpleListFilter):
"""
Expand Down
32 changes: 32 additions & 0 deletions openedx/core/djangoapps/discussions/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Tests for DiscussionsConfiguration admin view
"""
from django.test import TestCase
from django.urls import reverse

from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider


class DiscussionsConfigurationAdminTest(TestCase):
"""
Tests for discussion config admin
"""
def setUp(self):
super().setUp()
self.superuser = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.superuser.username, password="Password1234")

def test_change_view(self):
"""
Test that the DiscussionAdmin's change_view processes the context_key correctly and returns a successful
response.
"""
discussion_config = DiscussionsConfiguration.objects.create(
context_key='course-v1:test+test+06_25_2024',
provider_type=Provider.OPEN_EDX,
)
url = reverse('admin:discussions_discussionsconfiguration_change', args=[discussion_config.context_key])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'course-v1:test+test+06_25_2024')
40 changes: 40 additions & 0 deletions openedx/core/djangoapps/notifications/email/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Events for email notifications
"""
import datetime

from eventtracking import tracker

from common.djangoapps.track import segment
from openedx.core.djangoapps.notifications.base_notification import COURSE_NOTIFICATION_APPS


EMAIL_DIGEST_SENT = "edx.notifications.email_digest"


def send_user_email_digest_sent_event(user, cadence_type, notifications):
"""
Sends tracker and segment email for user email digest
"""
notification_breakdown = {key: 0 for key in COURSE_NOTIFICATION_APPS.keys()}
for notification in notifications:
notification_breakdown[notification.app_name] += 1
event_data = {
"username": user.username,
"email": user.email,
"cadence_type": cadence_type,
"total_notifications_count": len(notifications),
"count_breakdown": notification_breakdown,
"notification_ids": [notification.id for notification in notifications],
"send_at": str(datetime.datetime.now())
}
with tracker.get_tracker().context(EMAIL_DIGEST_SENT, event_data):
tracker.emit(
EMAIL_DIGEST_SENT,
event_data,
)
segment.track(
'None',
EMAIL_DIGEST_SENT,
event_data,
)
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/notifications/email/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Notification,
get_course_notification_preference_config_version
)
from .events import send_user_email_digest_sent_event
from .message_type import EmailNotificationMessageType
from .utils import (
add_headers_to_email_message,
Expand Down Expand Up @@ -101,6 +102,7 @@ def send_digest_email_to_user(user, cadence_type, course_language='en', courses_
).personalize(recipient, course_language, message_context)
message = add_headers_to_email_message(message, message_context)
ace.send(message)
send_user_email_digest_sent_event(user, cadence_type, notifications)
logger.info(f'<Email Cadence> Email sent to {user.username} ==Temp Log==')


Expand Down
Loading

0 comments on commit ce5bf2a

Please sign in to comment.