From 1dfd3310f754a4b0b409e7626b9b747752c445d8 Mon Sep 17 00:00:00 2001 From: David Baumgold <david@davidbaumgold.com> Date: Fri, 7 Feb 2014 16:16:46 -0500 Subject: [PATCH] Refactor student views Fix pylint/pep8 warnings, use JsonResponse instead of HttpResponse where useful, put in TODOs to change HTTP status codes to be more accurate. --- common/djangoapps/student/tests/test_login.py | 7 +- common/djangoapps/student/tests/tests.py | 34 +++- common/djangoapps/student/views.py | 188 +++++++++++------- 3 files changed, 142 insertions(+), 87 deletions(-) diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py index 9be1e7de04b..871e662fcd0 100644 --- a/common/djangoapps/student/tests/test_login.py +++ b/common/djangoapps/student/tests/test_login.py @@ -23,6 +23,7 @@ from external_auth.models import ExternalAuthMap TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) + class LoginTest(TestCase): ''' Test student.views.login_user() view @@ -224,7 +225,11 @@ class ExternalAuthShibTest(ModuleStoreTestCase): """ response = self.client.post(reverse('login'), {'email': self.user_w_map.email, 'password': ''}) self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, json.dumps({'success': False, 'redirect': reverse('shib-login')})) + obj = json.loads(response.content) + self.assertEqual(obj, { + 'success': False, + 'redirect': reverse('shib-login'), + }) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test__get_course_enrollment_domain(self): diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 8df41609d69..cc25c1eb23a 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -68,8 +68,11 @@ class ResetPasswordTests(TestCase): bad_pwd_resp = password_reset(bad_pwd_req) # If they've got an unusable password, we return a successful response code self.assertEquals(bad_pwd_resp.status_code, 200) - self.assertEquals(bad_pwd_resp.content, json.dumps({'success': True, - 'value': "('registration/password_reset_done.html', [])"})) + obj = json.loads(bad_pwd_resp.content) + self.assertEquals(obj, { + 'success': True, + 'value': "('registration/password_reset_done.html', [])", + }) @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_nonexist_email_password_reset(self): @@ -80,12 +83,19 @@ class ResetPasswordTests(TestCase): # Note: even if the email is bad, we return a successful response code # This prevents someone potentially trying to "brute-force" find out which emails are and aren't registered with edX self.assertEquals(bad_email_resp.status_code, 200) - self.assertEquals(bad_email_resp.content, json.dumps({'success': True, - 'value': "('registration/password_reset_done.html', [])"})) - - @unittest.skipIf(settings.FEATURES.get('DISABLE_RESET_EMAIL_TEST', False), - dedent("""Skipping Test because CMS has not provided necessary templates for password reset. - If LMS tests print this message, that needs to be fixed.""")) + obj = json.loads(bad_email_resp.content) + self.assertEquals(obj, { + 'success': True, + 'value': "('registration/password_reset_done.html', [])", + }) + + @unittest.skipIf( + settings.FEATURES.get('DISABLE_RESET_EMAIL_TEST', False), + dedent(""" + Skipping Test because CMS has not provided necessary templates for password reset. + If LMS tests print this message, that needs to be fixed. + """) + ) @patch('django.core.mail.send_mail') @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_reset_password_email(self, send_email): @@ -94,9 +104,11 @@ class ResetPasswordTests(TestCase): good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) good_resp = password_reset(good_req) self.assertEquals(good_resp.status_code, 200) - self.assertEquals(good_resp.content, - json.dumps({'success': True, - 'value': "('registration/password_reset_done.html', [])"})) + obj = json.loads(good_resp.content) + self.assertEquals(obj, { + 'success': True, + 'value': "('registration/password_reset_done.html', [])", + }) ((subject, msg, from_addr, to_addrs), sm_kwargs) = send_email.call_args self.assertIn("Password reset", subject) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f1feb1f1d53..ac047cd6da4 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -4,9 +4,7 @@ Student Views import datetime import json import logging -import random import re -import string # pylint: disable=W0402 import urllib import uuid import time @@ -18,13 +16,11 @@ from django.contrib.auth import logout, authenticate, login from django.contrib.auth.models import User, AnonymousUser from django.contrib.auth.decorators import login_required from django.contrib.auth.views import password_reset_confirm -# from django.contrib.sessions.models import Session from django.core.cache import cache from django.core.context_processors import csrf from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.core.validators import validate_email, validate_slug, ValidationError -from django.core.exceptions import ObjectDoesNotExist from django.db import IntegrityError, transaction from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404) @@ -33,7 +29,6 @@ from django_future.csrf import ensure_csrf_cookie from django.utils.http import cookie_date, base36_to_int from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST, require_GET -from django.contrib.admin.views.decorators import staff_member_required from ratelimitbackend.exceptions import RateLimitException @@ -46,6 +41,7 @@ from student.models import ( CourseEnrollmentAllowed, UserStanding, LoginFailures ) from student.forms import PasswordResetFormNoActive +from student.firebase_token_generator import create_token from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow from certificates.models import CertificateStatuses, certificate_status_for_student @@ -69,7 +65,6 @@ import shoppingcart import track.views from dogapi import dog_stats_api -from pytz import UTC from util.json_request import JsonResponse @@ -265,7 +260,6 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set): .format(user.username, enrollment.course_id)) - def _cert_info(user, course, cert_status): """ Implements the logic for cert_info -- split out for testing. @@ -674,8 +668,10 @@ def accounts_login(request): def login_user(request, error=""): """AJAX request to log in the user.""" if 'email' not in request.POST or 'password' not in request.POST: - return HttpResponse(json.dumps({'success': False, - 'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message + return JsonResponse({ + "success": False, + "value": _('There was an error receiving your login information. Please email us.'), # TODO: User error message + }) # TODO: this should be status code 400 # pylint: disable=fixme email = request.POST['email'] password = request.POST['password'] @@ -692,7 +688,10 @@ def login_user(request, error=""): try: eamap = ExternalAuthMap.objects.get(user=user) if eamap.external_domain.startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX): - return HttpResponse(json.dumps({'success': False, 'redirect': reverse('shib-login')})) + return JsonResponse({ + "success": False, + "redirect": reverse('shib-login'), + }) # TODO: this should be status code 301 # pylint: disable=fixme except ExternalAuthMap.DoesNotExist: # This is actually the common case, logging in user without external linked login AUDIT_LOG.info("User %s w/o external auth attempting login", user) @@ -701,12 +700,10 @@ def login_user(request, error=""): user_found_by_email_lookup = user if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user_found_by_email_lookup): - return HttpResponse( - json.dumps({ - 'success': False, - 'value': _('This account has been temporarily locked due to excessive login failures. Try again later.') - }) - ) + return JsonResponse({ + "success": False, + "value": _('This account has been temporarily locked due to excessive login failures. Try again later.'), + }) # TODO: this should be status code 429 # pylint: disable=fixme # if the user doesn't exist, we want to set the username to an invalid # username so that authentication is guaranteed to fail and we can take @@ -716,8 +713,10 @@ def login_user(request, error=""): user = authenticate(username=username, password=password, request=request) # this occurs when there are too many attempts from the same IP address except RateLimitException: - return HttpResponse(json.dumps({'success': False, - 'value': _('Too many failed login attempts. Try again later.')})) + return JsonResponse({ + "success": False, + "value": _('Too many failed login attempts. Try again later.'), + }) # TODO: this should be status code 429 # pylint: disable=fixme if user is None: # tick the failed login counters if the user exists in the database if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): @@ -727,8 +726,10 @@ def login_user(request, error=""): # doesn't exist, and doesn't have a corresponding password if username != "": AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email)) - return HttpResponse(json.dumps({'success': False, - 'value': _('Email or password is incorrect.')})) + return JsonResponse({ + "success": False, + "value": _('Email or password is incorrect.'), + }) # TODO: this should be status code 400 # pylint: disable=fixme # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): @@ -753,7 +754,10 @@ def login_user(request, error=""): redirect_url = try_change_enrollment(request) dog_stats_api.increment("common.student.successful_login") - response = HttpResponse(json.dumps({'success': True, 'redirect_url': redirect_url})) + response = JsonResponse({ + "success": True, + "redirect_url": redirect_url, + }) # set the login cookie for the edx marketing site # we want this cookie to be accessed via javascript @@ -767,12 +771,11 @@ def login_user(request, error=""): expires_time = time.time() + max_age expires = cookie_date(expires_time) - response.set_cookie(settings.EDXMKTG_COOKIE_NAME, - 'true', max_age=max_age, - expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, - path='/', - secure=None, - httponly=None) + response.set_cookie( + settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age, + expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, + path='/', secure=None, httponly=None, + ) return response @@ -780,8 +783,10 @@ def login_user(request, error=""): reactivation_email_for_user(user) not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your e-mail for the activation instructions.") - return HttpResponse(json.dumps({'success': False, - 'value': not_activated_msg})) + return JsonResponse({ + "success": False, + "value": not_activated_msg, + }) # TODO: this should be status code 400 # pylint: disable=fixme @ensure_csrf_cookie @@ -799,11 +804,13 @@ def logout_user(request): else: target = '/' response = redirect(target) - response.delete_cookie(settings.EDXMKTG_COOKIE_NAME, - path='/', - domain=settings.SESSION_COOKIE_DOMAIN) + response.delete_cookie( + settings.EDXMKTG_COOKIE_NAME, + path='/', domain=settings.SESSION_COOKIE_DOMAIN, + ) return response + @require_GET @login_required @ensure_csrf_cookie @@ -890,8 +897,10 @@ def change_setting(request): up.location = request.POST['location'] up.save() - return HttpResponse(json.dumps({'success': True, - 'location': up.location, })) + return JsonResponse({ + "success": True, + "location": up.location, + }) def _do_create_account(post_vars): @@ -1106,8 +1115,8 @@ def create_account(request, post_override=None): '-' * 80 + '\n\n' + message) send_mail(subject, message, from_address, [dest_addr], fail_silently=False) else: - _res = user.email_user(subject, message, from_address) - except: + user.email_user(subject, message, from_address) + except Exception: # pylint: disable=broad-except log.warning('Unable to send activation email to user', exc_info=True) js['value'] = _('Could not send activation e-mail.') # What is the correct status code to use here? I think it's 500, because @@ -1298,8 +1307,10 @@ def password_reset(request): from_email=settings.DEFAULT_FROM_EMAIL, request=request, domain_override=request.get_host()) - return HttpResponse(json.dumps({'success': True, - 'value': render_to_string('registration/password_reset_done.html', {})})) + return JsonResponse({ + 'success': True, + 'value': render_to_string('registration/password_reset_done.html', {}), + }) def password_reset_confirm_wrapper( @@ -1330,23 +1341,30 @@ def reactivation_email_for_user(user): try: reg = Registration.objects.get(user=user) except Registration.DoesNotExist: - return HttpResponse(json.dumps({'success': False, - 'error': _('No inactive user with this e-mail exists')})) + return JsonResponse({ + "success": False, + "error": _('No inactive user with this e-mail exists'), + }) # TODO: this should be status code 400 # pylint: disable=fixme - d = {'name': user.profile.name, - 'key': reg.activation_key} + context = { + 'name': user.profile.name, + 'key': reg.activation_key, + } - subject = render_to_string('emails/activation_email_subject.txt', d) + subject = render_to_string('emails/activation_email_subject.txt', context) subject = ''.join(subject.splitlines()) - message = render_to_string('emails/activation_email.txt', d) + message = render_to_string('emails/activation_email.txt', context) try: - _res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) - except: + user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) + except Exception: # pylint: disable=broad-except log.warning('Unable to send reactivation email', exc_info=True) - return HttpResponse(json.dumps({'success': False, 'error': _('Unable to send reactivation email')})) + return JsonResponse({ + "success": False, + "error": _('Unable to send reactivation email') + }) # TODO: this should be status code 500 # pylint: disable=fixme - return HttpResponse(json.dumps({'success': True})) + return JsonResponse({"success": True}) @ensure_csrf_cookie @@ -1360,20 +1378,26 @@ def change_email_request(request): user = request.user if not user.check_password(request.POST['password']): - return HttpResponse(json.dumps({'success': False, - 'error': _('Invalid password')})) + return JsonResponse({ + "success": False, + "error": _('Invalid password'), + }) # TODO: this should be status code 400 # pylint: disable=fixme new_email = request.POST['new_email'] try: validate_email(new_email) except ValidationError: - return HttpResponse(json.dumps({'success': False, - 'error': _('Valid e-mail address required.')})) + return JsonResponse({ + "success": False, + "error": _('Valid e-mail address required.'), + }) # TODO: this should be status code 400 # pylint: disable=fixme if User.objects.filter(email=new_email).count() != 0: ## CRITICAL TODO: Handle case sensitivity for e-mails - return HttpResponse(json.dumps({'success': False, - 'error': _('An account with this e-mail already exists.')})) + return JsonResponse({ + "success": False, + "error": _('An account with this e-mail already exists.'), + }) # TODO: this should be status code 400 # pylint: disable=fixme pec_list = PendingEmailChange.objects.filter(user=request.user) if len(pec_list) == 0: @@ -1388,8 +1412,10 @@ def change_email_request(request): if pec.new_email == user.email: pec.delete() - return HttpResponse(json.dumps({'success': False, - 'error': _('Old email is the same as the new email.')})) + return JsonResponse({ + "success": False, + "error": _('Old email is the same as the new email.'), + }) # TODO: this should be status code 400 # pylint: disable=fixme context = { 'key': pec.activation_key, @@ -1407,9 +1433,9 @@ def change_email_request(request): settings.DEFAULT_FROM_EMAIL ) - _res = send_mail(subject, message, from_address, [pec.new_email]) + send_mail(subject, message, from_address, [pec.new_email]) - return HttpResponse(json.dumps({'success': True})) + return JsonResponse({"success": True}) @ensure_csrf_cookie @@ -1491,14 +1517,17 @@ def change_name_request(request): pnc.new_name = request.POST['new_name'] pnc.rationale = request.POST['rationale'] if len(pnc.new_name) < 2: - return HttpResponse(json.dumps({'success': False, 'error': _('Name required')})) + return JsonResponse({ + "success": False, + "error": _('Name required'), + }) # TODO: this should be status code 400 # pylint: disable=fixme pnc.save() # The following automatically accepts name change requests. Remove this to # go back to the old system where it gets queued up for admin approval. accept_name_change_by_id(pnc.id) - return HttpResponse(json.dumps({'success': True})) + return JsonResponse({"success": True}) @ensure_csrf_cookie @@ -1507,14 +1536,19 @@ def pending_name_changes(request): if not request.user.is_staff: raise Http404 - changes = list(PendingNameChange.objects.all()) - js = {'students': [{'new_name': c.new_name, - 'rationale': c.rationale, - 'old_name': UserProfile.objects.get(user=c.user).name, - 'email': c.user.email, - 'uid': c.user.id, - 'cid': c.id} for c in changes]} - return render_to_response('name_changes.html', js) + students = [] + for change in PendingNameChange.objects.all(): + profile = UserProfile.objects.get(user=change.user) + students.append({ + "new_name": change.new_name, + "rationale": change.rationale, + "old_name": profile.name, + "email": change.user.email, + "uid": change.user.id, + "cid": change.id, + }) + + return render_to_response("name_changes.html", {"students": students}) @ensure_csrf_cookie @@ -1526,17 +1560,23 @@ def reject_name_change(request): try: pnc = PendingNameChange.objects.get(id=int(request.POST['id'])) except PendingNameChange.DoesNotExist: - return HttpResponse(json.dumps({'success': False, 'error': _('Invalid ID')})) + return JsonResponse({ + "success": False, + "error": _('Invalid ID'), + }) # TODO: this should be status code 400 # pylint: disable=fixme pnc.delete() - return HttpResponse(json.dumps({'success': True})) + return JsonResponse({"success": True}) def accept_name_change_by_id(id): try: pnc = PendingNameChange.objects.get(id=id) except PendingNameChange.DoesNotExist: - return HttpResponse(json.dumps({'success': False, 'error': _('Invalid ID')})) + return JsonResponse({ + "success": False, + "error": _('Invalid ID'), + }) # TODO: this should be status code 400 # pylint: disable=fixme u = pnc.user up = UserProfile.objects.get(user=u) @@ -1552,7 +1592,7 @@ def accept_name_change_by_id(id): up.save() pnc.delete() - return HttpResponse(json.dumps({'success': True})) + return JsonResponse({"success": True}) @ensure_csrf_cookie @@ -1589,9 +1629,7 @@ def change_email_settings(request): log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id)) track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard') - return HttpResponse(json.dumps({'success': True})) - -from student.firebase_token_generator import create_token + return JsonResponse({"success": True}) @login_required -- GitLab