* Update Financial Assistance logic

Use the zendesk proxy app instead of the unsupported zendesk library.

* Move to pre-fetching the group IDs.

Rather than making extra requests to zendesk to list all groups and find
a specific group ID. Just make a pre-filled list of group IDs for the
groups we care about.  When a group name is passed in, it is checked
against this list and the ticket is created in the correct group so the
right people can respond to it.
This commit is contained in:
Ayub
2019-08-16 22:05:35 +05:00
committed by Feanil Patel
parent f5f875401a
commit 658cd5c62e
11 changed files with 155 additions and 309 deletions

View File

@@ -38,6 +38,7 @@ from xblock.fields import Scope, String
import courseware.views.views as views
import shoppingcart
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
@@ -484,11 +485,9 @@ class ViewsTestCase(ModuleStoreTestCase):
def assert_enrollment_link_present(self, is_anonymous):
"""
Prepare ecommerce checkout data and assert if the ecommerce link is contained in the response.
Arguments:
is_anonymous(bool): Tell the method to use an anonymous user or the logged in one.
_id(bool): Tell the method to either expect an id in the href or not.
"""
sku = 'TEST123'
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
@@ -606,7 +605,6 @@ class ViewsTestCase(ModuleStoreTestCase):
"""
Visits the about page for `course_id` and tests that both the text "Classes End", as well
as the specified `expected_end_text`, is present on the page.
If `expected_end_text` is None, verifies that the about page *does not* contain the text
"Classes End".
"""
@@ -846,8 +844,8 @@ class ViewsTestCase(ModuleStoreTestCase):
url = reverse('submit_financial_assistance_request')
return self.client.post(url, json.dumps(data), content_type='application/json')
@patch.object(views, '_record_feedback_in_zendesk')
def test_submit_financial_assistance_request(self, mock_record_feedback):
@patch.object(views, 'create_zendesk_ticket', return_value=200)
def test_submit_financial_assistance_request(self, mock_create_zendesk_ticket):
username = self.user.username
course = six.text_type(self.course_key)
legal_name = 'Jesse Pinkman'
@@ -871,10 +869,12 @@ class ViewsTestCase(ModuleStoreTestCase):
response = self._submit_financial_assistance_form(data)
self.assertEqual(response.status_code, 204)
__, __, ticket_subject, __, tags, additional_info = mock_record_feedback.call_args[0]
mocked_kwargs = mock_record_feedback.call_args[1]
group_name = mocked_kwargs['group_name']
require_update = mocked_kwargs['require_update']
__, __, ticket_subject, __ = mock_create_zendesk_ticket.call_args[0]
mocked_kwargs = mock_create_zendesk_ticket.call_args[1]
group_name = mocked_kwargs['group']
tags = mocked_kwargs['tags']
additional_info = mocked_kwargs['additional_info']
private_comment = '\n'.join(list(additional_info.values()))
for info in (country, income, reason_for_applying, goals, effort, username, legal_name, course):
self.assertIn(info, private_comment)
@@ -891,10 +891,9 @@ class ViewsTestCase(ModuleStoreTestCase):
self.assertDictContainsSubset({'course_id': course}, tags)
self.assertIn('Client IP', additional_info)
self.assertEqual(group_name, 'Financial Assistance')
self.assertTrue(require_update)
@patch.object(views, '_record_feedback_in_zendesk', return_value=False)
def test_zendesk_submission_failed(self, _mock_record_feedback):
@patch.object(views, 'create_zendesk_ticket', return_value=500)
def test_zendesk_submission_failed(self, _mock_create_zendesk_ticket):
response = self._submit_financial_assistance_form({
'username': self.user.username,
'course': six.text_type(self.course.id),
@@ -1029,7 +1028,6 @@ class BaseDueDateTests(ModuleStoreTestCase):
def set_up_course(self, **course_kwargs):
"""
Create a stock course with a specific due date.
:param course_kwargs: All kwargs are passed to through to the :class:`CourseFactory`
"""
course = CourseFactory.create(**course_kwargs)
@@ -1158,7 +1156,6 @@ class StartDateTests(ModuleStoreTestCase):
def set_up_course(self):
"""
Create a stock course with a specific due date.
:param course_kwargs: All kwargs are passed to through to the :class:`CourseFactory`
"""
course = CourseFactory.create(start=datetime(2013, 9, 16, 7, 17, 28))
@@ -1291,7 +1288,6 @@ class ProgressPageTests(ProgressPageBaseTests):
def test_unenrolled_student_progress_for_credit_course(self, default_store):
"""
Test that student progress page does not break while checking for an unenrolled student.
Scenario: When instructor checks the progress of a student who is not enrolled in credit course.
It should return 200 response.
"""
@@ -2746,7 +2742,6 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
def test_vertical_positions(self, input_position, expected_position):
"""
Tests the following cases:
* Load first position when negative position inputted.
* Load first position when 0/-0 position inputted.
* Load given position when 0 < input_position <= num_positions_available.
@@ -2971,7 +2966,6 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock):
class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
"""
Ensure that courseware index requests do not trigger student state writes.
This is to prevent locking issues that have caused latency spikes in the
courseware_studentmodule table when concurrent requests each try to update
the same rows for sequence, section, and course positions.

View File

@@ -94,6 +94,7 @@ from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_duration_limits.access import generate_course_expired_fragment
from openedx.features.course_experience import (
@@ -112,7 +113,7 @@ from track import segment
from util.cache import cache, cache_if_anonymous
from util.db import outer_atomic
from util.milestones_helpers import get_prerequisite_courses_display
from util.views import _record_feedback_in_zendesk, ensure_valid_course_key, ensure_valid_usage_key
from util.views import ensure_valid_course_key, ensure_valid_usage_key
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
@@ -287,9 +288,7 @@ def jump_to_id(request, course_id, module_id):
def jump_to(_request, course_id, location):
"""
Show the page that contains a specific location.
If the location is invalid or not in any class, return a 404.
Otherwise, delegates to the index view to figure out whether this user
has access, and what they should see.
"""
@@ -314,7 +313,6 @@ def jump_to(_request, course_id, location):
def course_info(request, course_id):
"""
Display the course's info.html, or 404 if there is no such course.
Assumes the course_id is in a valid format.
"""
# TODO: LEARNER-611: This can be deleted with Course Info removal. The new
@@ -709,7 +707,6 @@ class CourseTabView(EdxFragmentView):
def syllabus(request, course_id):
"""
Display the course's syllabus.html, or 404 if there is no such course.
Assumes the course_id is in a valid format.
"""
@@ -739,15 +736,12 @@ def registered_for_course(course, user):
class EnrollStaffView(View):
"""
Displays view for registering in the course to a global staff user.
User can either choose to 'Enroll' or 'Don't Enroll' in the course.
Enroll: Enrolls user in course and redirects to the courseware.
Don't Enroll: Redirects user to course about page.
Arguments:
- request : HTTP request
- course_id : course id
Returns:
- RedirectResponse
"""
@@ -978,9 +972,7 @@ def progress(request, course_id, student_id=None):
def _progress(request, course_key, student_id):
"""
Unwrapped version of "progress".
User progress. We show the grade bar and every problem score.
Course staff are allowed to see the progress of students in their class.
"""
@@ -1109,13 +1101,11 @@ def _certificate_message(student, course, enrollment_mode):
def _get_cert_data(student, course, enrollment_mode, course_grade=None):
"""Returns students course certificate related data.
Arguments:
student (User): Student for whom certificate to retrieve.
course (Course): Course object for which certificate data to retrieve.
enrollment_mode (String): Course mode in which student is enrolled.
course_grade (CourseGrade): Student's course grade record.
Returns:
returns dict if course certificate is available else None.
"""
@@ -1134,14 +1124,11 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None):
def _credit_course_requirements(course_key, student):
"""Return information about which credit requirements a user has satisfied.
Arguments:
course_key (CourseKey): Identifier for the course.
student (User): Currently logged in user.
Returns: dict if the credit eligibility enabled and it is a credit course
and the user is enrolled in either verified or credit mode, and None otherwise.
"""
# If credit eligibility is not enabled or this is not a credit course,
# short-circuit and return `None`. This indicates that credit requirements
@@ -1198,7 +1185,6 @@ def _course_home_redirect_enabled():
"""
Return True value if user needs to be redirected to course home based on value of
`ENABLE_MKTG_SITE` and `ENABLE_COURSE_HOME_REDIRECT feature` flags
Returns: boolean True or False
"""
if configuration_helpers.get_value(
@@ -1318,16 +1304,13 @@ def get_static_tab_fragment(request, course, tab):
def get_course_lti_endpoints(request, course_id):
"""
View that, given a course_id, returns the a JSON object that enumerates all of the LTI endpoints for that course.
The LTI 2.0 result service spec at
http://www.imsglobal.org/lti/ltiv2p0/uml/purl.imsglobal.org/vocab/lis/v2/outcomes/Result/service.html
says "This specification document does not prescribe a method for discovering the endpoint URLs." This view
function implements one way of discovering these endpoints, returning a JSON array when accessed.
Arguments:
request (django request object): the HTTP request object that triggered this view function
course_id (unicode): id associated with the course
Returns:
(django response object): HTTP response. 404 if course is not found, otherwise 200 with JSON body.
"""
@@ -1404,12 +1387,10 @@ def course_survey(request, course_id):
def is_course_passed(student, course, course_grade=None):
"""
check user's course passing status. return True if passed
Arguments:
student : user object
course : course object
course_grade (CourseGrade) : contains student grade details.
Returns:
returns bool value
"""
@@ -1423,24 +1404,19 @@ def is_course_passed(student, course, course_grade=None):
@require_POST
def generate_user_cert(request, course_id):
"""Start generating a new certificate for the user.
Certificate generation is allowed if:
* The user has passed the course, and
* The user does not already have a pending/completed certificate.
Note that if an error occurs during certificate generation
(for example, if the queue is down), then we simply mark the
certificate generation task status as "error" and re-run
the task with a management command. To students, the certificate
will appear to be "generating" until it is re-run.
Args:
request (HttpRequest): The POST request to this view.
course_id (unicode): The identifier for the course.
Returns:
HttpResponse: 200 on success, 400 if a new certificate cannot be generated.
"""
if not request.user.is_authenticated:
@@ -1491,13 +1467,11 @@ def generate_user_cert(request, course_id):
def _track_successful_certificate_generation(user_id, course_id):
"""
Track a successful certificate generation event.
Arguments:
user_id (str): The ID of the user generating the certificate.
course_id (CourseKey): Identifier for the course.
Returns:
None
"""
event_name = 'edx.bi.user.certificate.generate'
segment.track(user_id, event_name, {
@@ -1632,7 +1606,7 @@ def financial_assistance_request(request):
# Thrown if fields are missing
return HttpResponseBadRequest(u'The field {} is required.'.format(text_type(err)))
zendesk_submitted = _record_feedback_in_zendesk(
zendesk_submitted = create_zendesk_ticket(
legal_name,
email,
u'Financial assistance request for learner {username} in course {course_name}'.format(
@@ -1640,12 +1614,12 @@ def financial_assistance_request(request):
course_name=course.display_name
),
u'Financial Assistance Request',
{'course_id': course_id},
tags={'course_id': course_id},
# Send the application as additional info on the ticket so
# that it is not shown when support replies. This uses
# OrderedDict so that information is presented in the right
# order.
OrderedDict((
additional_info=OrderedDict((
('Username', username),
('Full Name', legal_name),
('Course ID', course_id),
@@ -1657,11 +1631,9 @@ def financial_assistance_request(request):
(FA_EFFORT_LABEL, '\n' + effort + '\n\n'),
('Client IP', ip_address),
)),
group_name='Financial Assistance',
require_update=True
group='Financial Assistance',
)
if not zendesk_submitted:
if not (zendesk_submitted == 200 or zendesk_submitted == 201):
# The call to Zendesk failed. The frontend will display a
# message to the user.
return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR)