refactor: ran pyupgrade on lms/djangoapps/commerce (#26734)
This commit is contained in:
@@ -4,12 +4,11 @@
|
||||
import itertools
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
import pytz
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
@@ -17,11 +16,11 @@ from django.urls import reverse, reverse_lazy
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.tests import EnrollmentEventTestMixin
|
||||
from openedx.core.djangoapps.embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollment
|
||||
from openedx.core.lib.django_test_client_utils import get_absolute_url
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.tests import EnrollmentEventTestMixin
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -53,7 +52,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
|
||||
:return: Response
|
||||
"""
|
||||
payload = {
|
||||
"course_id": six.text_type(course_id or self.course.id)
|
||||
"course_id": str(course_id or self.course.id)
|
||||
}
|
||||
if marketing_email_opt_in:
|
||||
payload["email_opt_in"] = True
|
||||
@@ -69,7 +68,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
|
||||
assert actual == expected_msg
|
||||
|
||||
def setUp(self):
|
||||
super(BasketsViewTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.url = reverse('commerce_api:v0:baskets:create')
|
||||
self._login()
|
||||
|
||||
@@ -78,13 +77,13 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
|
||||
# TODO Verify this is the best method to create CourseMode objects.
|
||||
# TODO Find/create constants for the modes.
|
||||
for mode in [CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.AUDIT]:
|
||||
sku_string = six.text_type(uuid4().hex)
|
||||
sku_string = str(uuid4().hex)
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode,
|
||||
mode_display_name=mode,
|
||||
sku=sku_string,
|
||||
bulk_sku='BULK-{}'.format(sku_string)
|
||||
bulk_sku=f'BULK-{sku_string}'
|
||||
)
|
||||
|
||||
# Ignore events fired from UserFactory creation
|
||||
@@ -200,9 +199,9 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
|
||||
|
||||
CourseMode.objects.filter(course_id=self.course.id).delete()
|
||||
mode = CourseMode.NO_ID_PROFESSIONAL_MODE
|
||||
sku_string = six.text_type(uuid4().hex)
|
||||
sku_string = str(uuid4().hex)
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
|
||||
sku=sku_string, bulk_sku='BULK-{}'.format(sku_string))
|
||||
sku=sku_string, bulk_sku=f'BULK-{sku_string}')
|
||||
response = self._post_to_view()
|
||||
|
||||
# The view should return an error status code
|
||||
@@ -252,7 +251,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
CourseEnrollment.unenroll(self.user, self.course.id, True)
|
||||
assert not CourseEnrollment.is_enrolled(self.user, self.course.id)
|
||||
assert get_enrollment(self.user.username, six.text_type(self.course.id)) is not None
|
||||
assert get_enrollment(self.user.username, str(self.course.id)) is not None
|
||||
|
||||
@mock.patch('lms.djangoapps.commerce.api.v0.views.update_email_opt_in')
|
||||
@ddt.data(*itertools.product((False, True), (False, True), (False, True)))
|
||||
@@ -290,7 +289,7 @@ class BasketOrderViewTests(UserMixin, TestCase):
|
||||
path = reverse_lazy(view_name, kwargs={'basket_id': 1})
|
||||
|
||||
def setUp(self):
|
||||
super(BasketOrderViewTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self._login()
|
||||
|
||||
def test_order_found(self):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
import six
|
||||
from django.urls import reverse
|
||||
from edx_rest_api_client import exceptions
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
@@ -13,20 +12,19 @@ from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
|
||||
from rest_framework.views import APIView
|
||||
from six import text_type
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware import courses
|
||||
from common.djangoapps.entitlements.models import CourseEntitlement
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.signals import SAILTHRU_AUDIT_PURCHASE
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
from lms.djangoapps.courseware import courses
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.djangoapps.embargo import api as embargo_api
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment
|
||||
from openedx.core.djangoapps.enrollments.views import EnrollmentCrossDomainSessionAuth
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.signals import SAILTHRU_AUDIT_PURCHASE
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
|
||||
from ...constants import Messages
|
||||
from ...http import DetailResponse
|
||||
@@ -57,20 +55,20 @@ class BasketsView(APIView):
|
||||
course_id = request.data.get('course_id')
|
||||
|
||||
if not course_id:
|
||||
return False, None, u'Field course_id is missing.'
|
||||
return False, None, 'Field course_id is missing.'
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
courses.get_course(course_key)
|
||||
except (InvalidKeyError, ValueError) as ex:
|
||||
log.exception(u'Unable to locate course matching %s.', course_id)
|
||||
return False, None, text_type(ex)
|
||||
log.exception('Unable to locate course matching %s.', course_id)
|
||||
return False, None, str(ex)
|
||||
|
||||
return True, course_key, None
|
||||
|
||||
def _enroll(self, course_key, user, mode=CourseMode.DEFAULT_MODE_SLUG):
|
||||
""" Enroll the user in the course. """
|
||||
add_enrollment(user.username, six.text_type(course_key), mode)
|
||||
add_enrollment(user.username, str(course_key), mode)
|
||||
|
||||
def _handle_marketing_opt_in(self, request, course_key, user):
|
||||
"""
|
||||
@@ -85,7 +83,7 @@ class BasketsView(APIView):
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# log the error, return silently
|
||||
log.exception(
|
||||
u'Failed to handle marketing opt-in flag: user="%s", course="%s"', user.username, course_key
|
||||
'Failed to handle marketing opt-in flag: user="%s", course="%s"', user.username, course_key
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument
|
||||
@@ -103,7 +101,7 @@ class BasketsView(APIView):
|
||||
return embargo_response
|
||||
|
||||
# Don't do anything if an enrollment already exists
|
||||
course_id = six.text_type(course_key)
|
||||
course_id = str(course_key)
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
if enrollment and enrollment.is_active:
|
||||
msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
|
||||
@@ -113,7 +111,7 @@ class BasketsView(APIView):
|
||||
course = courses.get_course(course_key)
|
||||
if CourseEnrollment.is_enrollment_closed(user, course):
|
||||
msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id)
|
||||
log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id)
|
||||
log.info('Unable to enroll user %s in closed course %s.', user.id, course_id)
|
||||
return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
|
||||
|
||||
# If there is no audit or honor course mode, this most likely
|
||||
@@ -126,7 +124,7 @@ class BasketsView(APIView):
|
||||
if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_key):
|
||||
return JsonResponse(
|
||||
{
|
||||
'redirect_destination': reverse('courseware', args=[six.text_type(course_id)]),
|
||||
'redirect_destination': reverse('courseware', args=[str(course_id)]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import logging
|
||||
from itertools import groupby
|
||||
|
||||
import six
|
||||
from django.db import transaction
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -18,14 +17,14 @@ log = logging.getLogger(__name__)
|
||||
UNDEFINED = object()
|
||||
|
||||
|
||||
class Course(object):
|
||||
class Course:
|
||||
""" Pseudo-course model used to group CourseMode objects. """
|
||||
id = None # pylint: disable=invalid-name
|
||||
modes = None
|
||||
_deleted_modes = None
|
||||
|
||||
def __init__(self, id, modes, **kwargs): # pylint: disable=redefined-builtin
|
||||
self.id = CourseKey.from_string(six.text_type(id)) # pylint: disable=invalid-name
|
||||
self.id = CourseKey.from_string(str(id)) # pylint: disable=invalid-name
|
||||
self.modes = list(modes)
|
||||
self.verification_deadline = UNDEFINED
|
||||
if 'verification_deadline' in kwargs:
|
||||
@@ -35,14 +34,14 @@ class Course(object):
|
||||
@property
|
||||
def name(self):
|
||||
""" Return course name. """
|
||||
course_id = CourseKey.from_string(six.text_type(self.id))
|
||||
course_id = CourseKey.from_string(str(self.id))
|
||||
|
||||
try:
|
||||
return CourseOverview.get_from_id(course_id).display_name
|
||||
except CourseOverview.DoesNotExist:
|
||||
# NOTE (CCB): Ideally, the course modes table should only contain data for courses that exist in
|
||||
# modulestore. If that is not the case, say for local development/testing, carry on without failure.
|
||||
log.warning(u'Failed to retrieve CourseOverview for [%s]. Using empty course name.', course_id)
|
||||
log.warning('Failed to retrieve CourseOverview for [%s]. Using empty course name.', course_id)
|
||||
return None
|
||||
|
||||
def get_mode_display_name(self, mode):
|
||||
@@ -127,9 +126,9 @@ class Course(object):
|
||||
def get(cls, course_id):
|
||||
""" Retrieve a single course. """
|
||||
try:
|
||||
course_id = CourseKey.from_string(six.text_type(course_id))
|
||||
course_id = CourseKey.from_string(str(course_id))
|
||||
except InvalidKeyError:
|
||||
log.debug(u'[%s] is not a valid course key.', course_id)
|
||||
log.debug('[%s] is not a valid course key.', course_id)
|
||||
raise ValueError # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
course_modes = CourseMode.objects.filter(course_id=course_id)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -33,7 +32,7 @@ class CourseModeSerializer(serializers.ModelSerializer):
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = CourseMode
|
||||
fields = ('name', 'currency', 'price', 'sku', 'bulk_sku', 'expires')
|
||||
# For disambiguating within the drf-yasg swagger schema
|
||||
@@ -45,17 +44,17 @@ def validate_course_id(course_id):
|
||||
Check that course id is valid and exists in modulestore.
|
||||
"""
|
||||
try:
|
||||
course_key = CourseKey.from_string(six.text_type(course_id))
|
||||
course_key = CourseKey.from_string(str(course_id))
|
||||
except InvalidKeyError:
|
||||
raise serializers.ValidationError( # lint-amnesty, pylint: disable=raise-missing-from
|
||||
_(u"{course_id} is not a valid course key.").format(
|
||||
_("{course_id} is not a valid course key.").format(
|
||||
course_id=course_id
|
||||
)
|
||||
)
|
||||
|
||||
if not modulestore().has_course(course_key):
|
||||
raise serializers.ValidationError(
|
||||
_(u'Course {course_id} does not exist.').format(
|
||||
_('Course {course_id} does not exist.').format(
|
||||
course_id=course_id
|
||||
)
|
||||
)
|
||||
@@ -69,7 +68,7 @@ class PossiblyUndefinedDateTimeField(serializers.DateTimeField):
|
||||
def to_representation(self, value):
|
||||
if value is UNDEFINED:
|
||||
return None
|
||||
return super(PossiblyUndefinedDateTimeField, self).to_representation(value) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
return super().to_representation(value)
|
||||
|
||||
|
||||
class CourseSerializer(serializers.Serializer):
|
||||
@@ -79,7 +78,7 @@ class CourseSerializer(serializers.Serializer):
|
||||
verification_deadline = PossiblyUndefinedDateTimeField(format=None, allow_null=True, required=False)
|
||||
modes = CourseModeSerializer(many=True)
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
# For disambiguating within the drf-yasg swagger schema
|
||||
ref_name = 'commerce.Course'
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class CourseTests(TestCase):
|
||||
""" Tests for Course model. """
|
||||
|
||||
def setUp(self):
|
||||
super(CourseTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course = Course('a/b/c', [])
|
||||
|
||||
@ddt.unpack
|
||||
|
||||
@@ -13,7 +13,7 @@ class CourseValidatorTests(TestCase):
|
||||
""" Verify a validator checking non-existent courses."""
|
||||
course_key = 'non/existing/keyone'
|
||||
|
||||
error_msg = u"Course {} does not exist.".format(course_key)
|
||||
error_msg = f"Course {course_key} does not exist."
|
||||
with self.assertRaisesRegex(serializers.ValidationError, error_msg):
|
||||
validate_course_id(course_key)
|
||||
|
||||
@@ -21,6 +21,6 @@ class CourseValidatorTests(TestCase):
|
||||
""" Verify a validator checking invalid course keys."""
|
||||
course_key = 'invalidkey'
|
||||
|
||||
error_msg = u"{} is not a valid course key.".format(course_key)
|
||||
error_msg = f"{course_key} is not a valid course key."
|
||||
with self.assertRaisesRegex(serializers.ValidationError, error_msg):
|
||||
validate_course_id(course_key)
|
||||
|
||||
@@ -7,7 +7,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import TestCase
|
||||
@@ -16,8 +15,8 @@ from django.urls import reverse, reverse_lazy
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -28,22 +27,22 @@ PASSWORD = 'test'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
|
||||
|
||||
class CourseApiViewTestMixin(object):
|
||||
class CourseApiViewTestMixin:
|
||||
""" Mixin for CourseApi views.
|
||||
|
||||
Automatically creates a course and CourseMode.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CourseApiViewTestMixin, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
self.course_mode = CourseMode.objects.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=u'verified',
|
||||
mode_slug='verified',
|
||||
min_price=100,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123'
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -60,12 +59,12 @@ class CourseApiViewTestMixin(object):
|
||||
def _serialize_course_mode(cls, course_mode):
|
||||
""" Serialize a CourseMode to a dict. """
|
||||
return {
|
||||
u'name': course_mode.mode_slug,
|
||||
u'currency': course_mode.currency.lower(),
|
||||
u'price': course_mode.min_price,
|
||||
u'sku': course_mode.sku,
|
||||
u'bulk_sku': course_mode.bulk_sku,
|
||||
u'expires': cls._serialize_datetime(course_mode.expiration_datetime),
|
||||
'name': course_mode.mode_slug,
|
||||
'currency': course_mode.currency.lower(),
|
||||
'price': course_mode.min_price,
|
||||
'sku': course_mode.sku,
|
||||
'bulk_sku': course_mode.bulk_sku,
|
||||
'expires': cls._serialize_datetime(course_mode.expiration_datetime),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -75,10 +74,10 @@ class CourseApiViewTestMixin(object):
|
||||
verification_deadline = verification_deadline or VerificationDeadline.deadline_for_course(course.id)
|
||||
|
||||
return {
|
||||
u'id': six.text_type(course.id),
|
||||
u'name': six.text_type(course.display_name),
|
||||
u'verification_deadline': cls._serialize_datetime(verification_deadline),
|
||||
u'modes': [cls._serialize_course_mode(mode) for mode in modes]
|
||||
'id': str(course.id),
|
||||
'name': str(course.display_name),
|
||||
'verification_deadline': cls._serialize_datetime(verification_deadline),
|
||||
'modes': [cls._serialize_course_mode(mode) for mode in modes]
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +113,8 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(CourseRetrieveUpdateViewTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
self.path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(self.course.id)])
|
||||
super().setUp()
|
||||
self.path = reverse('commerce_api:v1:courses:retrieve_update', args=[str(self.course.id)])
|
||||
self.user = UserFactory.create()
|
||||
self.client.login(username=self.user.username, password=PASSWORD)
|
||||
|
||||
@@ -155,11 +154,11 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
def _get_update_response_and_expected_data(self, mode_expiration, verification_deadline):
|
||||
""" Returns expected data and response for course update. """
|
||||
expected_course_mode = CourseMode(
|
||||
mode_slug=u'verified',
|
||||
mode_slug='verified',
|
||||
min_price=200,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123',
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123',
|
||||
expiration_datetime=mode_expiration
|
||||
)
|
||||
expected = self._serialize_course(self.course, [expected_course_mode], verification_deadline)
|
||||
@@ -226,11 +225,11 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
assert VerificationDeadline.deadline_for_course(self.course.id) == verification_deadline
|
||||
|
||||
verified_mode = CourseMode(
|
||||
mode_slug=u'verified',
|
||||
mode_slug='verified',
|
||||
min_price=200,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123',
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123',
|
||||
expiration_datetime=None
|
||||
)
|
||||
updated_data = self._serialize_course(self.course, [verified_mode], None)
|
||||
@@ -251,11 +250,11 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
assert VerificationDeadline.deadline_for_course(self.course.id) == verification_deadline
|
||||
|
||||
verified_mode = CourseMode(
|
||||
mode_slug=u'verified',
|
||||
mode_slug='verified',
|
||||
min_price=200,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123',
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123',
|
||||
expiration_datetime=None
|
||||
)
|
||||
updated_data = self._serialize_course(self.course, [verified_mode], None)
|
||||
@@ -296,22 +295,22 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
existing_mode = self.course_mode
|
||||
existing_masters_mode = CourseMode.objects.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=u'masters',
|
||||
mode_slug='masters',
|
||||
min_price=10000,
|
||||
currency=u'USD',
|
||||
sku=u'DEF456',
|
||||
bulk_sku=u'BULK-DEF456'
|
||||
currency='USD',
|
||||
sku='DEF456',
|
||||
bulk_sku='BULK-DEF456'
|
||||
)
|
||||
new_mode = CourseMode(
|
||||
course_id=self.course.id,
|
||||
mode_slug=u'credit',
|
||||
mode_slug='credit',
|
||||
min_price=500,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123'
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123'
|
||||
)
|
||||
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(self.course.id)])
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[str(self.course.id)])
|
||||
data = json.dumps(self._serialize_course(self.course, [new_mode]))
|
||||
response = self.client.put(path, data, content_type=JSON_CONTENT_TYPE)
|
||||
assert response.status_code == 200
|
||||
@@ -341,14 +340,14 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
CourseMode(
|
||||
mode_slug=mode_slug,
|
||||
min_price=500,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123',
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123',
|
||||
expiration_datetime=expiration_datetime
|
||||
)
|
||||
)
|
||||
course_id = six.text_type(self.course.id)
|
||||
payload = {u'id': course_id, u'modes': [mode]}
|
||||
course_id = str(self.course.id)
|
||||
payload = {'id': course_id, 'modes': [mode]}
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[course_id])
|
||||
|
||||
expected_status = 400 if CourseMode.is_professional_slug(mode_slug) and expiration_datetime is not None else 200
|
||||
@@ -360,22 +359,22 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
course = CourseFactory.create()
|
||||
expected_modes = [
|
||||
CourseMode(
|
||||
mode_slug=u'verified',
|
||||
mode_slug='verified',
|
||||
min_price=150,
|
||||
currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123'
|
||||
currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123'
|
||||
),
|
||||
CourseMode(
|
||||
mode_slug=u'honor',
|
||||
mode_slug='honor',
|
||||
min_price=0,
|
||||
currency=u'USD',
|
||||
sku=u'DEADBEEF',
|
||||
bulk_sku=u'BULK-DEADBEEF'
|
||||
currency='USD',
|
||||
sku='DEADBEEF',
|
||||
bulk_sku='BULK-DEADBEEF'
|
||||
)
|
||||
]
|
||||
expected = self._serialize_course(course, expected_modes)
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(course.id)])
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[str(course.id)])
|
||||
|
||||
response = self.client.put(path, json.dumps(expected), content_type=JSON_CONTENT_TYPE, **request_kwargs)
|
||||
assert response.status_code == 201
|
||||
@@ -412,29 +411,29 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
|
||||
expected_modes = [
|
||||
CourseMode(
|
||||
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
|
||||
min_price=150, currency=u'USD',
|
||||
sku=u'ABC123',
|
||||
bulk_sku=u'BULK-ABC123'
|
||||
min_price=150, currency='USD',
|
||||
sku='ABC123',
|
||||
bulk_sku='BULK-ABC123'
|
||||
)
|
||||
]
|
||||
|
||||
course_key = 'non/existing/key'
|
||||
|
||||
course_dict = {
|
||||
u'id': six.text_type(course_key),
|
||||
u'name': six.text_type('Non Existing Course'),
|
||||
u'verification_deadline': None,
|
||||
u'modes': [self._serialize_course_mode(mode) for mode in expected_modes]
|
||||
'id': str(course_key),
|
||||
'name': 'Non Existing Course',
|
||||
'verification_deadline': None,
|
||||
'modes': [self._serialize_course_mode(mode) for mode in expected_modes]
|
||||
}
|
||||
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(course_key)])
|
||||
path = reverse('commerce_api:v1:courses:retrieve_update', args=[str(course_key)])
|
||||
|
||||
response = self.client.put(path, json.dumps(course_dict), content_type=JSON_CONTENT_TYPE)
|
||||
assert response.status_code == 400
|
||||
|
||||
expected_dict = {
|
||||
'id': [
|
||||
u'Course {} does not exist.'.format(
|
||||
'Course {} does not exist.'.format(
|
||||
course_key
|
||||
)
|
||||
]
|
||||
@@ -450,7 +449,7 @@ class OrderViewTests(UserMixin, TestCase):
|
||||
path = reverse_lazy(view_name, kwargs={'number': ORDER_NUMBER})
|
||||
|
||||
def setUp(self):
|
||||
super(OrderViewTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self._login()
|
||||
|
||||
def test_order_found(self):
|
||||
|
||||
@@ -10,7 +10,7 @@ from . import views
|
||||
|
||||
COURSE_URLS = ([
|
||||
url(r'^$', views.CourseListView.as_view(), name='list'),
|
||||
url(r'^{}/$'.format(settings.COURSE_ID_PATTERN), views.CourseRetrieveUpdateView.as_view(), name='retrieve_update'),
|
||||
url(fr'^{settings.COURSE_ID_PATTERN}/$', views.CourseRetrieveUpdateView.as_view(), name='retrieve_update'),
|
||||
], 'courses')
|
||||
|
||||
ORDER_URLS = ([
|
||||
|
||||
@@ -13,12 +13,12 @@ from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.generics import ListAPIView, RetrieveUpdateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from openedx.core.lib.api.authentication import BearerAuthentication
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.lib.api.mixins import PutAsCreateMixin
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.lib.api.authentication import BearerAuthentication
|
||||
from openedx.core.lib.api.mixins import PutAsCreateMixin
|
||||
|
||||
from ...utils import is_account_activation_requirement_disabled
|
||||
from .models import Course
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
""" Constants for this app as well as the external API. """
|
||||
|
||||
|
||||
class OrderStatus(object):
|
||||
class OrderStatus:
|
||||
"""Constants representing all known order statuses. """
|
||||
OPEN = 'Open'
|
||||
FULFILLMENT_ERROR = 'Fulfillment Error'
|
||||
COMPLETE = 'Complete'
|
||||
|
||||
|
||||
class Messages(object):
|
||||
class Messages:
|
||||
""" Strings used to populate response messages. """
|
||||
NO_ECOM_API = u'E-Commerce API not setup. Enrolled {username} in {course_id} directly.'
|
||||
NO_SKU_ENROLLED = u'The {enrollment_mode} mode for {course_id}, {course_name}, does not have a SKU. Enrolling ' \
|
||||
u'{username} directly. Course announcement is {announcement}.'
|
||||
ENROLL_DIRECTLY = u'Enroll {username} in {course_id} directly because no need for E-Commerce baskets and orders.'
|
||||
ORDER_COMPLETED = u'Order {order_number} was completed.'
|
||||
ORDER_INCOMPLETE_ENROLLED = u'Order {order_number} was created, but is not yet complete. User was enrolled.'
|
||||
NO_HONOR_MODE = u'Course {course_id} does not have an honor mode.'
|
||||
NO_DEFAULT_ENROLLMENT_MODE = u'Course {course_id} does not have an honor or audit mode.'
|
||||
ENROLLMENT_EXISTS = u'User {username} is already enrolled in {course_id}.'
|
||||
ENROLLMENT_CLOSED = u'Enrollment is closed for {course_id}.'
|
||||
NO_ECOM_API = 'E-Commerce API not setup. Enrolled {username} in {course_id} directly.'
|
||||
NO_SKU_ENROLLED = 'The {enrollment_mode} mode for {course_id}, {course_name}, does not have a SKU. Enrolling ' \
|
||||
'{username} directly. Course announcement is {announcement}.'
|
||||
ENROLL_DIRECTLY = 'Enroll {username} in {course_id} directly because no need for E-Commerce baskets and orders.'
|
||||
ORDER_COMPLETED = 'Order {order_number} was completed.'
|
||||
ORDER_INCOMPLETE_ENROLLED = 'Order {order_number} was created, but is not yet complete. User was enrolled.'
|
||||
NO_HONOR_MODE = 'Course {course_id} does not have an honor mode.'
|
||||
NO_DEFAULT_ENROLLMENT_MODE = 'Course {course_id} does not have an honor or audit mode.'
|
||||
ENROLLMENT_EXISTS = 'User {username} is already enrolled in {course_id}.'
|
||||
ENROLLMENT_CLOSED = 'Enrollment is closed for {course_id}.'
|
||||
|
||||
@@ -11,7 +11,7 @@ class DetailResponse(JsonResponse):
|
||||
|
||||
def __init__(self, message, status=HTTP_200_OK):
|
||||
data = {'detail': message}
|
||||
super(DetailResponse, self).__init__(resp_obj=data, status=status) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(resp_obj=data, status=status)
|
||||
|
||||
|
||||
class InternalRequestErrorResponse(DetailResponse):
|
||||
@@ -19,7 +19,7 @@ class InternalRequestErrorResponse(DetailResponse):
|
||||
|
||||
def __init__(self, internal_message):
|
||||
message = (
|
||||
u'Call to E-Commerce API failed. Internal Service Message: [{internal_message}]'
|
||||
'Call to E-Commerce API failed. Internal Service Message: [{internal_message}]'
|
||||
.format(internal_message=internal_message)
|
||||
)
|
||||
super(InternalRequestErrorResponse, self).__init__(message=message, status=HTTP_500_INTERNAL_SERVER_ERROR) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(message=message, status=HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -11,17 +11,15 @@ from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from enterprise.models import EnterpriseCourseEnrollment
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from requests import Timeout
|
||||
from slumber.exceptions import HttpServerError, SlumberBaseException
|
||||
|
||||
from enterprise.models import EnterpriseCourseEnrollment
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
|
||||
from common.djangoapps.util.query import use_read_replica_if_available
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -48,7 +46,7 @@ class Command(BaseCommand):
|
||||
EnterpriseCourseEnrollments Queryset
|
||||
|
||||
"""
|
||||
self.stdout.write(u'Getting enrollments from {start} to {end} index (as per command params)'
|
||||
self.stdout.write('Getting enrollments from {start} to {end} index (as per command params)'
|
||||
.format(start=start_index or 'start', end=end_index or 'end'))
|
||||
enrollments_qs = EnterpriseCourseEnrollment.objects.filter(
|
||||
source__isnull=True
|
||||
@@ -118,7 +116,7 @@ class Command(BaseCommand):
|
||||
"""
|
||||
|
||||
self.stdout.write(
|
||||
u'\tFetching Enrollments from {start} to {end}'.format(start=offset, end=offset + batch_size)
|
||||
'\tFetching Enrollments from {start} to {end}'.format(start=offset, end=offset + batch_size)
|
||||
)
|
||||
enrollments = enrollments_queryset.select_related(
|
||||
'enterprise_customer_user', 'enterprise_customer_user__enterprise_customer'
|
||||
@@ -135,7 +133,7 @@ class Command(BaseCommand):
|
||||
invalid = 0
|
||||
|
||||
self.stdout.write(
|
||||
u'\t\tProcessing Total : {},'.format(len(enrollments_batch))
|
||||
'\t\tProcessing Total : {},'.format(len(enrollments_batch))
|
||||
)
|
||||
|
||||
for enrollment in enrollments_batch:
|
||||
@@ -160,23 +158,23 @@ class Command(BaseCommand):
|
||||
"enterprise_customer_uuid": str(enterprise_customer.uuid),
|
||||
}
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
self.stderr.write(u'\t\tskipping enrollment {}, as CourseEnrollment not found'.format(enrollment.id))
|
||||
self.stderr.write(f'\t\tskipping enrollment {enrollment.id}, as CourseEnrollment not found')
|
||||
invalid += 1
|
||||
continue
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
self.stderr.write(u'\t\tskipping enrollment {} due to invalid data. {}'.format(enrollment.id, ex))
|
||||
self.stderr.write(f'\t\tskipping enrollment {enrollment.id} due to invalid data. {ex}')
|
||||
invalid += 1
|
||||
continue
|
||||
enrollments_payload.append(enrollment_payload)
|
||||
|
||||
self.stdout.write(u'\t\tFound {count} Paid enrollments to sync'.format(count=len(enrollments_payload)))
|
||||
self.stdout.write('\t\tFound {count} Paid enrollments to sync'.format(count=len(enrollments_payload)))
|
||||
if not enrollments_payload:
|
||||
return 0, 0, 0, invalid, non_paid, []
|
||||
|
||||
self.stdout.write(u'\t\tSyncing started...')
|
||||
self.stdout.write('\t\tSyncing started...')
|
||||
success, new, failed, order_numbers = self._create_manual_enrollment_orders(enrollments_payload)
|
||||
self.stdout.write(
|
||||
u'\t\tSuccess: {} , New: {}, Failed: {}, Invalid:{} , Non-Paid: {}'.format(
|
||||
'\t\tSuccess: {} , New: {}, Failed: {}, Invalid:{} , Non-Paid: {}'.format(
|
||||
success, new, failed, invalid, non_paid,
|
||||
)
|
||||
)
|
||||
@@ -186,7 +184,7 @@ class Command(BaseCommand):
|
||||
"""
|
||||
Syncs a single site
|
||||
"""
|
||||
self.stdout.write(u'Syncing process started.')
|
||||
self.stdout.write('Syncing process started.')
|
||||
|
||||
offset = 0
|
||||
enrollments_queue = []
|
||||
@@ -201,7 +199,7 @@ class Command(BaseCommand):
|
||||
while offset < enrollments_count:
|
||||
is_last_iteration = (offset + enrollments_query_batch_size) >= enrollments_count
|
||||
self.stdout.write(
|
||||
u'\tSyncing enrollments batch from {start} to {end}.'.format(
|
||||
'\tSyncing enrollments batch from {start} to {end}.'.format(
|
||||
start=offset, end=offset + enrollments_query_batch_size
|
||||
)
|
||||
)
|
||||
@@ -221,23 +219,23 @@ class Command(BaseCommand):
|
||||
invalid_enrollments += invalid
|
||||
non_paid_enrollments += non_paid
|
||||
new_created_order_numbers += order_numbers
|
||||
self.stdout.write(u'\t\tsleeping for {} second/seconds'.format(sleep_time))
|
||||
self.stdout.write(f'\t\tsleeping for {sleep_time} second/seconds')
|
||||
time.sleep(sleep_time)
|
||||
|
||||
self.stdout.write(
|
||||
u'\tSuccessfully synced enrollments batch from {start} to {end}'.format(
|
||||
'\tSuccessfully synced enrollments batch from {start} to {end}'.format(
|
||||
start=offset, end=offset + enrollments_query_batch_size,
|
||||
)
|
||||
)
|
||||
offset += enrollments_query_batch_size
|
||||
|
||||
self.stdout.write(
|
||||
u'[Final Summary] Enrollments Success: {}, New: {}, Failed: {}, Invalid: {} , Non-Paid: {}'.format(
|
||||
'[Final Summary] Enrollments Success: {}, New: {}, Failed: {}, Invalid: {} , Non-Paid: {}'.format(
|
||||
successfully_synced_enrollments, new_created_orders, failed_to_synced_enrollments, invalid_enrollments,
|
||||
non_paid_enrollments
|
||||
)
|
||||
)
|
||||
self.stdout.write('New created order numbers {}'.format(new_created_order_numbers))
|
||||
self.stdout.write(f'New created order numbers {new_created_order_numbers}')
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""
|
||||
@@ -281,12 +279,12 @@ class Command(BaseCommand):
|
||||
sleep_time = options['sleep_time']
|
||||
|
||||
try:
|
||||
self.stdout.write(u'Command execution started with options = {}.'.format(options))
|
||||
self.stdout.write(f'Command execution started with options = {options}.')
|
||||
enrollments_queryset = self._get_enrollments_queryset(start_index, end_index)
|
||||
enrollments_count = enrollments_queryset.count()
|
||||
self.stdout.write(u'Total Enrollments count to process: {count}'.format(count=enrollments_count))
|
||||
self.stdout.write(f'Total Enrollments count to process: {enrollments_count}')
|
||||
self._sync(enrollments_queryset, enrollments_count, batch_size, sleep_time)
|
||||
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise CommandError(u'Command failed with traceback %s' % str(ex)) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
raise CommandError('Command failed with traceback %s' % str(ex)) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
@@ -3,19 +3,19 @@ Test the create_orders_for_old_enterprise_course_enrollment management command
|
||||
"""
|
||||
|
||||
import re
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.six import StringIO
|
||||
from mock import patch
|
||||
from six.moves import range
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from openedx.core.djangoapps.credit.tests.test_api import TEST_ECOMMERCE_WORKER
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.features.enterprise_support.tests.factories import (
|
||||
EnterpriseCourseEnrollmentFactory, EnterpriseCustomerUserFactory
|
||||
EnterpriseCourseEnrollmentFactory,
|
||||
EnterpriseCustomerUserFactory
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestEnterpriseCourseEnrollmentCreateOldOrder(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(TestEnterpriseCourseEnrollmentCreateOldOrder, cls).setUpTestData()
|
||||
super().setUpTestData()
|
||||
UserFactory(username=TEST_ECOMMERCE_WORKER)
|
||||
cls._create_enterprise_course_enrollments(30)
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@@ -21,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('checkout_on_ecommerce_service', models.BooleanField(default=False, help_text='Use the checkout page hosted by the E-Commerce service.')),
|
||||
('single_course_checkout_page', models.CharField(default=u'/basket/single-item/', help_text='Path to single course checkout page hosted by the E-Commerce service.', max_length=255)),
|
||||
('single_course_checkout_page', models.CharField(default='/basket/single-item/', help_text='Path to single course checkout page hosted by the E-Commerce service.', max_length=255)),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -19,6 +16,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='commerceconfiguration',
|
||||
name='receipt_page',
|
||||
field=models.CharField(default=u'/commerce/checkout/receipt/?orderNum=', help_text='Path to order receipt page.', max_length=255),
|
||||
field=models.CharField(default='/commerce/checkout/receipt/?orderNum=', help_text='Path to order receipt page.', max_length=255),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -14,6 +11,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='commerceconfiguration',
|
||||
name='receipt_page',
|
||||
field=models.CharField(default=u'/checkout/receipt/?order_number=', help_text='Path to order receipt page.', max_length=255),
|
||||
field=models.CharField(default='/checkout/receipt/?order_number=', help_text='Path to order receipt page.', max_length=255),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -18,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='commerceconfiguration',
|
||||
name='basket_checkout_page',
|
||||
field=models.CharField(default=u'/basket/add/', help_text='Path to course(s) checkout page hosted by the E-Commerce service.', max_length=255),
|
||||
field=models.CharField(default='/basket/add/', help_text='Path to course(s) checkout page hosted by the E-Commerce service.', max_length=255),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.25 on 2019-10-24 20:48
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class CommerceConfiguration(ConfigurationModel):
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "commerce"
|
||||
|
||||
API_NAME = 'commerce'
|
||||
@@ -32,7 +32,7 @@ class CommerceConfiguration(ConfigurationModel):
|
||||
|
||||
basket_checkout_page = models.CharField(
|
||||
max_length=255,
|
||||
default=u'/basket/add/',
|
||||
default='/basket/add/',
|
||||
help_text=_('Path to course(s) checkout page hosted by the E-Commerce service.')
|
||||
)
|
||||
cache_ttl = models.PositiveIntegerField(
|
||||
|
||||
@@ -9,8 +9,8 @@ from crum import get_current_request
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.dispatch import receiver
|
||||
|
||||
from openedx.core.djangoapps.commerce.utils import is_commerce_service_configured
|
||||
from common.djangoapps.student.signals import REFUND_ORDER
|
||||
from openedx.core.djangoapps.commerce.utils import is_commerce_service_configured
|
||||
|
||||
from .utils import refund_seat
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Commerce app tests package. """
|
||||
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import httpretty
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from freezegun import freeze_time
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
JSON = 'application/json'
|
||||
TEST_PUBLIC_URL_ROOT = 'http://www.example.com'
|
||||
@@ -34,7 +34,7 @@ class EdxRestApiClientTest(TestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(EdxRestApiClientTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory()
|
||||
|
||||
@httpretty.activate
|
||||
@@ -67,7 +67,7 @@ class EdxRestApiClientTest(TestCase):
|
||||
}
|
||||
}
|
||||
expected_jwt = create_jwt_for_user(self.user, additional_claims=claims, scopes=self.SCOPES)
|
||||
expected_header = u'JWT {}'.format(expected_jwt)
|
||||
expected_header = f'JWT {expected_jwt}'
|
||||
assert actual_header == expected_header
|
||||
|
||||
@httpretty.activate
|
||||
@@ -86,4 +86,4 @@ class EdxRestApiClientTest(TestCase):
|
||||
adding_headers={'Content-Type': JSON},
|
||||
)
|
||||
actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
|
||||
assert actual_object == {u'result': u'Préparatoire'}
|
||||
assert actual_object == {'result': 'Préparatoire'}
|
||||
|
||||
@@ -7,7 +7,7 @@ from factory.fuzzy import FuzzyText
|
||||
|
||||
class OrderFactory(factory.Factory):
|
||||
""" Factory for stubbing orders resources from Ecommerce (v2). """
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
number = factory.Sequence(lambda n: 'edx-%d' % n)
|
||||
@@ -20,7 +20,7 @@ class OrderFactory(factory.Factory):
|
||||
|
||||
class OrderLineFactory(factory.Factory):
|
||||
""" Factory for stubbing order lines resources from Ecommerce (v2). """
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
title = FuzzyText(prefix='Seat in ')
|
||||
@@ -34,7 +34,7 @@ class OrderLineFactory(factory.Factory):
|
||||
|
||||
class ProductFactory(factory.Factory):
|
||||
""" Factory for stubbing Product resources from Ecommerce (v2). """
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name
|
||||
@@ -49,7 +49,7 @@ class ProductAttributeFactory(factory.Factory):
|
||||
""" Factory for stubbing product attribute resources from
|
||||
Ecommerce (v2).
|
||||
"""
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
name = FuzzyText()
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.conf import settings
|
||||
from . import factories
|
||||
|
||||
|
||||
class mock_ecommerce_api_endpoint(object):
|
||||
class mock_ecommerce_api_endpoint:
|
||||
"""
|
||||
Base class for contextmanagers used to mock calls to api endpoints.
|
||||
|
||||
@@ -95,11 +95,11 @@ class mock_basket_order(mock_ecommerce_api_endpoint):
|
||||
method = httpretty.GET
|
||||
|
||||
def __init__(self, basket_id, **kwargs):
|
||||
super(mock_basket_order, self).__init__(**kwargs) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(**kwargs)
|
||||
self.basket_id = basket_id
|
||||
|
||||
def get_path(self):
|
||||
return '/baskets/{}/order/'.format(self.basket_id)
|
||||
return f'/baskets/{self.basket_id}/order/'
|
||||
|
||||
|
||||
class mock_create_refund(mock_ecommerce_api_endpoint):
|
||||
@@ -131,11 +131,11 @@ class mock_process_refund(mock_ecommerce_api_endpoint):
|
||||
method = httpretty.PUT
|
||||
|
||||
def __init__(self, refund_id, **kwargs):
|
||||
super(mock_process_refund, self).__init__(**kwargs) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(**kwargs)
|
||||
self.refund_id = refund_id
|
||||
|
||||
def get_path(self):
|
||||
return '/refunds/{}/process/'.format(self.refund_id)
|
||||
return f'/refunds/{self.refund_id}/process/'
|
||||
|
||||
|
||||
class mock_order_endpoint(mock_ecommerce_api_endpoint):
|
||||
@@ -145,11 +145,11 @@ class mock_order_endpoint(mock_ecommerce_api_endpoint):
|
||||
method = httpretty.GET
|
||||
|
||||
def __init__(self, order_number, **kwargs):
|
||||
super(mock_order_endpoint, self).__init__(**kwargs) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(**kwargs)
|
||||
self.order_number = order_number
|
||||
|
||||
def get_path(self):
|
||||
return '/orders/{}/'.format(self.order_number)
|
||||
return f'/orders/{self.order_number}/'
|
||||
|
||||
|
||||
class mock_get_orders(mock_ecommerce_api_endpoint):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# coding=UTF-8
|
||||
"""
|
||||
Tests for signal handling in commerce djangoapp.
|
||||
"""
|
||||
@@ -6,17 +5,18 @@ Tests for signal handling in commerce djangoapp.
|
||||
|
||||
import base64
|
||||
import json
|
||||
from unittest import mock
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import pytest
|
||||
import ddt
|
||||
import httpretty
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from requests import Timeout
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.signals import REFUND_ORDER
|
||||
@@ -40,7 +40,7 @@ class TestRefundSignal(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestRefundSignal, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
|
||||
# Ensure the E-Commerce service user exists
|
||||
UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True)
|
||||
@@ -306,7 +306,7 @@ class TestRefundSignal(TestCase):
|
||||
expected = {
|
||||
'content-type': JSON,
|
||||
'Authorization': 'Basic {}'.format(base64.b64encode(
|
||||
'{user}/token:{pwd}'.format(user=ZENDESK_USER, pwd=ZENDESK_API_KEY).encode('utf8')).decode('utf8')
|
||||
f'{ZENDESK_USER}/token:{ZENDESK_API_KEY}'.encode('utf8')).decode('utf8')
|
||||
)
|
||||
}
|
||||
self.assertDictContainsSubset(expected, last_request.headers)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
@@ -9,19 +11,17 @@ from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from waffle.testutils import override_switch
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.utils import EcommerceService, refund_entitlement, refund_seat
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.core.lib.log_utils import audit_log
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -61,7 +61,7 @@ class EcommerceServiceTests(TestCase):
|
||||
self.user = UserFactory.create()
|
||||
self.request = self.request_factory.get("foo")
|
||||
update_commerce_config(enabled=True)
|
||||
super(EcommerceServiceTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
|
||||
def test_is_enabled(self):
|
||||
"""Verify that is_enabled() returns True when ecomm checkout is enabled. """
|
||||
@@ -105,7 +105,7 @@ class EcommerceServiceTests(TestCase):
|
||||
"""Verify that the proper Receipt page URL is returned."""
|
||||
order_number = 'ORDER1'
|
||||
url = EcommerceService().get_receipt_page_url(order_number)
|
||||
expected_url = 'http://ecommerce_url/checkout/receipt/?order_number={}'.format(order_number)
|
||||
expected_url = f'http://ecommerce_url/checkout/receipt/?order_number={order_number}'
|
||||
assert url == expected_url
|
||||
|
||||
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
|
||||
@@ -187,7 +187,7 @@ class RefundUtilMethodTests(ModuleStoreTestCase):
|
||||
"""Tests for Refund Utilities"""
|
||||
|
||||
def setUp(self):
|
||||
super(RefundUtilMethodTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory()
|
||||
UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True)
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class UserMixin(object):
|
||||
class UserMixin:
|
||||
""" Mixin for tests involving users. """
|
||||
|
||||
def setUp(self):
|
||||
super(UserMixin, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory()
|
||||
|
||||
def _login(self):
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
import requests
|
||||
import six
|
||||
import waffle
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from six.moves.urllib.parse import urlencode, urljoin
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.models import CourseEnrollment # lint-amnesty, pylint: disable=unused-import
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, is_commerce_service_configured
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
@@ -38,7 +38,7 @@ def is_account_activation_requirement_disabled():
|
||||
return waffle.switch_is_active(switch_name)
|
||||
|
||||
|
||||
class EcommerceService(object):
|
||||
class EcommerceService:
|
||||
""" Helper class for ecommerce service integration. """
|
||||
def __init__(self):
|
||||
self.config = CommerceConfiguration.current()
|
||||
@@ -157,7 +157,7 @@ def refund_entitlement(course_entitlement):
|
||||
|
||||
if not is_commerce_service_configured():
|
||||
log.error(
|
||||
u'Ecommerce service is not configured, cannot refund for user [%s], course entitlement [%s].',
|
||||
'Ecommerce service is not configured, cannot refund for user [%s], course entitlement [%s].',
|
||||
enrollee.id,
|
||||
entitlement_uuid
|
||||
)
|
||||
@@ -167,7 +167,7 @@ def refund_entitlement(course_entitlement):
|
||||
api_client = ecommerce_api_client(service_user)
|
||||
|
||||
log.info(
|
||||
u'Attempting to create a refund for user [%s], course entitlement [%s]...',
|
||||
'Attempting to create a refund for user [%s], course entitlement [%s]...',
|
||||
enrollee.id,
|
||||
entitlement_uuid
|
||||
)
|
||||
@@ -183,8 +183,8 @@ def refund_entitlement(course_entitlement):
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Catch any possible exceptions from the Ecommerce service to ensure we fail gracefully
|
||||
log.exception(
|
||||
u"Unexpected exception while attempting to initiate refund for user [%s], "
|
||||
u"course entitlement [%s] message: [%s]",
|
||||
"Unexpected exception while attempting to initiate refund for user [%s], "
|
||||
"course entitlement [%s] message: [%s]",
|
||||
enrollee.id,
|
||||
course_entitlement.uuid,
|
||||
str(exc)
|
||||
@@ -193,7 +193,7 @@ def refund_entitlement(course_entitlement):
|
||||
|
||||
if refund_ids:
|
||||
log.info(
|
||||
u'Refund successfully opened for user [%s], course entitlement [%s]: %r',
|
||||
'Refund successfully opened for user [%s], course entitlement [%s]: %r',
|
||||
enrollee.id,
|
||||
entitlement_uuid,
|
||||
refund_ids,
|
||||
@@ -207,7 +207,7 @@ def refund_entitlement(course_entitlement):
|
||||
always_notify=True,
|
||||
)
|
||||
else:
|
||||
log.warning(u'No refund opened for user [%s], course entitlement [%s]', enrollee.id, entitlement_uuid)
|
||||
log.warning('No refund opened for user [%s], course entitlement [%s]', enrollee.id, entitlement_uuid)
|
||||
return False
|
||||
|
||||
|
||||
@@ -229,18 +229,18 @@ def refund_seat(course_enrollment, change_mode=False):
|
||||
exceptions.Timeout: if the attempt to reach the commerce service timed out.
|
||||
"""
|
||||
User = get_user_model() # pylint:disable=invalid-name
|
||||
course_key_str = six.text_type(course_enrollment.course_id)
|
||||
course_key_str = str(course_enrollment.course_id)
|
||||
enrollee = course_enrollment.user
|
||||
|
||||
service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
|
||||
api_client = ecommerce_api_client(service_user)
|
||||
|
||||
log.info(u'Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str)
|
||||
log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str)
|
||||
|
||||
refund_ids = api_client.refunds.post({'course_id': course_key_str, 'username': enrollee.username})
|
||||
|
||||
if refund_ids:
|
||||
log.info(u'Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids)
|
||||
log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids)
|
||||
|
||||
_process_refund(
|
||||
refund_ids=refund_ids,
|
||||
@@ -253,7 +253,7 @@ def refund_seat(course_enrollment, change_mode=False):
|
||||
is_active=False, skip_refund=True)
|
||||
course_enrollment.save()
|
||||
else:
|
||||
log.info(u'No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)
|
||||
log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)
|
||||
|
||||
return refund_ids
|
||||
|
||||
@@ -285,10 +285,10 @@ def _process_refund(refund_ids, api_client, mode, user, always_notify=False):
|
||||
# We are then able to approve payment. Additionally, this ensures we don't tie up an
|
||||
# additional web worker when the E-Commerce Service tries to unenroll the learner.
|
||||
api_client.refunds(refund_id).process.put({'action': 'approve_payment_only'})
|
||||
log.info(u'Refund [%d] successfully approved.', refund_id)
|
||||
log.info('Refund [%d] successfully approved.', refund_id)
|
||||
except: # pylint: disable=bare-except
|
||||
# Push the refund to Support to process
|
||||
log.exception(u'Failed to automatically approve refund [%d]!', refund_id)
|
||||
log.exception('Failed to automatically approve refund [%d]!', refund_id)
|
||||
refunds_requiring_approval.append(refund_id)
|
||||
else:
|
||||
refunds_requiring_approval = refund_ids
|
||||
@@ -303,7 +303,7 @@ def _process_refund(refund_ids, api_client, mode, user, always_notify=False):
|
||||
# 'verified' is the only enrollment mode that should presently
|
||||
# result in opening a refund request.
|
||||
log.info(
|
||||
u'Skipping refund support notification for non-verified mode for user [%s], mode: [%s]',
|
||||
'Skipping refund support notification for non-verified mode for user [%s], mode: [%s]',
|
||||
user.id,
|
||||
mode,
|
||||
)
|
||||
@@ -344,18 +344,18 @@ def _send_refund_notification(user, refund_ids):
|
||||
def _generate_refund_notification_body(student, refund_ids):
|
||||
""" Returns a refund notification message body. """
|
||||
msg = _(
|
||||
u'A refund request has been initiated for {username} ({email}). '
|
||||
'A refund request has been initiated for {username} ({email}). '
|
||||
'To process this request, please visit the link(s) below.'
|
||||
).format(username=student.username, email=student.email)
|
||||
|
||||
ecommerce_url_root = configuration_helpers.get_value(
|
||||
'ECOMMERCE_PUBLIC_URL_ROOT', settings.ECOMMERCE_PUBLIC_URL_ROOT,
|
||||
)
|
||||
refund_urls = [urljoin(ecommerce_url_root, '/dashboard/refunds/{}/'.format(refund_id))
|
||||
refund_urls = [urljoin(ecommerce_url_root, f'/dashboard/refunds/{refund_id}/')
|
||||
for refund_id in refund_ids]
|
||||
|
||||
# emails contained in this message could contain unicode characters so encode as such
|
||||
return u'{msg}\n\n{urls}'.format(msg=msg, urls='\n'.join(refund_urls))
|
||||
return '{msg}\n\n{urls}'.format(msg=msg, urls='\n'.join(refund_urls))
|
||||
|
||||
|
||||
def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=None):
|
||||
@@ -378,7 +378,7 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
|
||||
'ticket': {
|
||||
'requester': {
|
||||
'name': requester_name,
|
||||
'email': six.text_type(requester_email)
|
||||
'email': str(requester_email)
|
||||
},
|
||||
'subject': subject,
|
||||
'comment': {'body': body},
|
||||
@@ -391,7 +391,7 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
|
||||
|
||||
# Set the request parameters
|
||||
url = urljoin(settings.ZENDESK_URL, '/api/v2/tickets.json')
|
||||
user = '{}/token'.format(settings.ZENDESK_USER)
|
||||
user = f'{settings.ZENDESK_USER}/token'
|
||||
pwd = settings.ZENDESK_API_KEY
|
||||
headers = {'content-type': 'application/json'}
|
||||
|
||||
@@ -400,7 +400,7 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
|
||||
|
||||
# Check for HTTP codes other than 201 (Created)
|
||||
if response.status_code != 201:
|
||||
log.error(u'Failed to create ticket. Status: [%d], Body: [%s]', response.status_code, response.content)
|
||||
log.error('Failed to create ticket. Status: [%d], Body: [%s]', response.status_code, response.content)
|
||||
return False
|
||||
else:
|
||||
log.debug('Successfully created ticket.')
|
||||
|
||||
Reference in New Issue
Block a user