Commit 4ee9ef61 by Diana Huang

Clean up some old pep8/pylint violations

Also, deletes some unused code.
parent 3ca74c01
"""
Views for the course_mode module
"""
import decimal import decimal
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import ( from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404 HttpResponseBadRequest, Http404
) )
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic.base import View from django.views.generic.base import View
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.http import urlencode
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
...@@ -18,10 +21,19 @@ from student.models import CourseEnrollment ...@@ -18,10 +21,19 @@ from student.models import CourseEnrollment
from student.views import course_from_id from student.views import course_from_id
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
class ChooseModeView(View):
class ChooseModeView(View):
"""
View used when the user is asked to pick a mode
When a get request is used, shows the selection page.
When a post request is used, assumes that it is a form submission
from the selection page, parses the response, and then sends user
to the next step in the flow
"""
@method_decorator(login_required) @method_decorator(login_required)
def get(self, request, course_id, error=None): def get(self, request, course_id, error=None):
""" Displays the course mode choice page """
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
modes = CourseMode.modes_for_course_dict(course_id) modes = CourseMode.modes_for_course_dict(course_id)
...@@ -34,8 +46,8 @@ class ChooseModeView(View): ...@@ -34,8 +46,8 @@ class ChooseModeView(View):
"course_id": course_id, "course_id": course_id,
"modes": modes, "modes": modes,
"course_name": course.display_name_with_default, "course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default, "course_org": course.display_org_with_default,
"course_num" : course.display_number_with_default, "course_num": course.display_number_with_default,
"chosen_price": chosen_price, "chosen_price": chosen_price,
"error": error, "error": error,
} }
...@@ -48,6 +60,7 @@ class ChooseModeView(View): ...@@ -48,6 +60,7 @@ class ChooseModeView(View):
@method_decorator(login_required) @method_decorator(login_required)
def post(self, request, course_id): def post(self, request, course_id):
""" Takes the form submission from the page and parses it """
user = request.user user = request.user
# This is a bit redundant with logic in student.views.change_enrollement, # This is a bit redundant with logic in student.views.change_enrollement,
...@@ -102,6 +115,10 @@ class ChooseModeView(View): ...@@ -102,6 +115,10 @@ class ChooseModeView(View):
) )
def get_requested_mode(self, user_choice): def get_requested_mode(self, user_choice):
"""
Given the text of `user_choice`, return the
corresponding course mode slug
"""
choices = { choices = {
"Select Audit": "audit", "Select Audit": "audit",
"Select Certificate": "verified" "Select Certificate": "verified"
......
...@@ -26,12 +26,11 @@ from django.conf import settings ...@@ -26,12 +26,11 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from model_utils.models import StatusModel from model_utils.models import StatusModel
from model_utils import Choices from model_utils import Choices
from verify_student.ssencrypt import ( from verify_student.ssencrypt import (
random_aes_key, decode_and_decrypt, encrypt_and_encode, random_aes_key, encrypt_and_encode,
generate_signed_message, rsa_encrypt generate_signed_message, rsa_encrypt
) )
...@@ -57,15 +56,18 @@ def status_before_must_be(*valid_start_statuses): ...@@ -57,15 +56,18 @@ def status_before_must_be(*valid_start_statuses):
distracting boilerplate when looking at a Model that needs to go through a distracting boilerplate when looking at a Model that needs to go through a
workflow process. workflow process.
""" """
def decorator_func(fn): def decorator_func(func):
@functools.wraps(fn) """
Decorator function that gets returned
"""
@functools.wraps(func)
def with_status_check(obj, *args, **kwargs): def with_status_check(obj, *args, **kwargs):
if obj.status not in valid_start_statuses: if obj.status not in valid_start_statuses:
exception_msg = ( exception_msg = (
u"Error calling {} {}: status is '{}', must be one of: {}" u"Error calling {} {}: status is '{}', must be one of: {}"
).format(fn, obj, obj.status, valid_start_statuses) ).format(func, obj, obj.status, valid_start_statuses)
raise VerificationException(exception_msg) raise VerificationException(exception_msg)
return fn(obj, *args, **kwargs) return func(obj, *args, **kwargs)
return with_status_check return with_status_check
...@@ -367,7 +369,7 @@ class PhotoVerification(StatusModel): ...@@ -367,7 +369,7 @@ class PhotoVerification(StatusModel):
Status should be moved to `must_retry`. Status should be moved to `must_retry`.
""" """
if self.status in ["approved", "denied"]: if self.status in ["approved", "denied"]:
return # If we were already approved or denied, just leave it. return # If we were already approved or denied, just leave it.
self.error_msg = error_msg self.error_msg = error_msg
self.error_code = error_code self.error_code = error_code
...@@ -408,7 +410,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -408,7 +410,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
# encode that. The result is saved here. Actual expected length is 344. # encode that. The result is saved here. Actual expected length is 344.
photo_id_key = models.TextField(max_length=1024) photo_id_key = models.TextField(max_length=1024)
IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
@status_before_must_be("created") @status_before_must_be("created")
def upload_face_image(self, img_data): def upload_face_image(self, img_data):
...@@ -444,8 +446,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -444,8 +446,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
self.status = "must_retry" self.status = "must_retry"
self.error_msg = response.text self.error_msg = response.text
self.save() self.save()
except Exception as e: except Exception as error:
log.exception(e) log.exception(error)
def image_url(self, name): def image_url(self, name):
""" """
...@@ -466,7 +468,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -466,7 +468,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
bucket = conn.get_bucket(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["S3_BUCKET"]) bucket = conn.get_bucket(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["S3_BUCKET"])
key = Key(bucket) key = Key(bucket)
key.key = "{}/{}".format(prefix, self.receipt_id); key.key = "{}/{}".format(prefix, self.receipt_id)
return key return key
...@@ -507,7 +509,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -507,7 +509,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
"Content-Type": "application/json", "Content-Type": "application/json",
"Date": formatdate(timeval=None, localtime=False, usegmt=True) "Date": formatdate(timeval=None, localtime=False, usegmt=True)
} }
message, _, authorization = generate_signed_message( _message, _sig, authorization = generate_signed_message(
"POST", headers, body, access_key, secret_key "POST", headers, body, access_key, secret_key
) )
headers['Authorization'] = authorization headers['Authorization'] = authorization
...@@ -515,16 +517,18 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -515,16 +517,18 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return headers, body return headers, body
def request_message_txt(self): def request_message_txt(self):
""" This is the body of the request we send across """
headers, body = self.create_request() headers, body = self.create_request()
header_txt = "\n".join( header_txt = "\n".join(
"{}: {}".format(h, v) for h,v in sorted(headers.items()) "{}: {}".format(h, v) for h, v in sorted(headers.items())
) )
body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8') body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8')
return header_txt + "\n\n" + body_txt return header_txt + "\n\n" + body_txt
def send_request(self): def send_request(self):
""" sends the request across to the endpoint """
headers, body = self.create_request() headers, body = self.create_request()
response = requests.post( response = requests.post(
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"],
...@@ -538,4 +542,4 @@ class SoftwareSecurePhotoVerification(PhotoVerification): ...@@ -538,4 +542,4 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
log.debug("Return code: {}".format(response.status_code)) log.debug("Return code: {}".format(response.status_code))
log.debug("Return message:\n\n{}\n\n".format(response.text)) log.debug("Return message:\n\n{}\n\n".format(response.text))
return response return response
\ No newline at end of file
...@@ -22,16 +22,11 @@ In case of PEM encoding, the private key can be encrypted with DES or 3TDES ...@@ -22,16 +22,11 @@ In case of PEM encoding, the private key can be encrypted with DES or 3TDES
according to a certain pass phrase. Only OpenSSL-compatible pass phrases are according to a certain pass phrase. Only OpenSSL-compatible pass phrases are
supported. supported.
""" """
from collections import OrderedDict
from email.utils import formatdate
from hashlib import md5, sha256 from hashlib import md5, sha256
from uuid import uuid4
import base64 import base64
import binascii import binascii
import json
import hmac import hmac
import logging import logging
import sys
from Crypto import Random from Crypto import Random
from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.Cipher import AES, PKCS1_OAEP
...@@ -39,12 +34,17 @@ from Crypto.PublicKey import RSA ...@@ -39,12 +34,17 @@ from Crypto.PublicKey import RSA
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def encrypt_and_encode(data, key): def encrypt_and_encode(data, key):
""" Encrypts and endcodes `data` using `key' """
return base64.urlsafe_b64encode(aes_encrypt(data, key)) return base64.urlsafe_b64encode(aes_encrypt(data, key))
def decode_and_decrypt(encoded_data, key): def decode_and_decrypt(encoded_data, key):
""" Decrypts and decodes `data` using `key' """
return aes_decrypt(base64.urlsafe_b64decode(encoded_data), key) return aes_decrypt(base64.urlsafe_b64decode(encoded_data), key)
def aes_encrypt(data, key): def aes_encrypt(data, key):
""" """
Return a version of the `data` that has been encrypted to Return a version of the `data` that has been encrypted to
...@@ -53,11 +53,16 @@ def aes_encrypt(data, key): ...@@ -53,11 +53,16 @@ def aes_encrypt(data, key):
padded_data = pad(data) padded_data = pad(data)
return cipher.encrypt(padded_data) return cipher.encrypt(padded_data)
def aes_decrypt(encrypted_data, key): def aes_decrypt(encrypted_data, key):
"""
Decrypt `encrypted_data` using `key`
"""
cipher = aes_cipher_from_key(key) cipher = aes_cipher_from_key(key)
padded_data = cipher.decrypt(encrypted_data) padded_data = cipher.decrypt(encrypted_data)
return unpad(padded_data) return unpad(padded_data)
def aes_cipher_from_key(key): def aes_cipher_from_key(key):
""" """
Given an AES key, return a Cipher object that has `encrypt()` and Given an AES key, return a Cipher object that has `encrypt()` and
...@@ -66,6 +71,7 @@ def aes_cipher_from_key(key): ...@@ -66,6 +71,7 @@ def aes_cipher_from_key(key):
""" """
return AES.new(key, AES.MODE_CBC, generate_aes_iv(key)) return AES.new(key, AES.MODE_CBC, generate_aes_iv(key))
def generate_aes_iv(key): def generate_aes_iv(key):
""" """
Return the initialization vector Software Secure expects for a given AES Return the initialization vector Software Secure expects for a given AES
...@@ -73,17 +79,23 @@ def generate_aes_iv(key): ...@@ -73,17 +79,23 @@ def generate_aes_iv(key):
""" """
return md5(key + md5(key).hexdigest()).hexdigest()[:AES.block_size] return md5(key + md5(key).hexdigest()).hexdigest()[:AES.block_size]
def random_aes_key(): def random_aes_key():
return Random.new().read(32) return Random.new().read(32)
def pad(data): def pad(data):
""" Pad the given `data` such that it fits into the proper AES block size """
bytes_to_pad = AES.block_size - len(data) % AES.block_size bytes_to_pad = AES.block_size - len(data) % AES.block_size
return data + (bytes_to_pad * chr(bytes_to_pad)) return data + (bytes_to_pad * chr(bytes_to_pad))
def unpad(padded_data): def unpad(padded_data):
""" remove all padding from `padded_data` """
num_padded_bytes = ord(padded_data[-1]) num_padded_bytes = ord(padded_data[-1])
return padded_data[:-num_padded_bytes] return padded_data[:-num_padded_bytes]
def rsa_encrypt(data, rsa_pub_key_str): def rsa_encrypt(data, rsa_pub_key_str):
""" """
`rsa_pub_key` is a string with the public key `rsa_pub_key` is a string with the public key
...@@ -93,11 +105,16 @@ def rsa_encrypt(data, rsa_pub_key_str): ...@@ -93,11 +105,16 @@ def rsa_encrypt(data, rsa_pub_key_str):
encrypted_data = cipher.encrypt(data) encrypted_data = cipher.encrypt(data)
return encrypted_data return encrypted_data
def rsa_decrypt(data, rsa_priv_key_str): def rsa_decrypt(data, rsa_priv_key_str):
"""
When given some `data` and an RSA private key, decrypt the data
"""
key = RSA.importKey(rsa_priv_key_str) key = RSA.importKey(rsa_priv_key_str)
cipher = PKCS1_OAEP.new(key) cipher = PKCS1_OAEP.new(key)
return cipher.decrypt(data) return cipher.decrypt(data)
def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key): def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key):
""" """
Given a message (either request or response), say whether it has a valid Given a message (either request or response), say whether it has a valid
...@@ -123,6 +140,7 @@ def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key) ...@@ -123,6 +140,7 @@ def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key)
return True return True
def generate_signed_message(method, headers_dict, body_dict, access_key, secret_key): def generate_signed_message(method, headers_dict, body_dict, access_key, secret_key):
""" """
Returns a (message, signature) pair. Returns a (message, signature) pair.
...@@ -137,6 +155,7 @@ def generate_signed_message(method, headers_dict, body_dict, access_key, secret_ ...@@ -137,6 +155,7 @@ def generate_signed_message(method, headers_dict, body_dict, access_key, secret_
message += '\n' message += '\n'
return message, signature, authorization_header return message, signature, authorization_header
def signing_format_message(method, headers_dict, body_dict): def signing_format_message(method, headers_dict, body_dict):
""" """
Given a dictionary of headers and a dictionary of the JSON for the body, Given a dictionary of headers and a dictionary of the JSON for the body,
...@@ -149,6 +168,7 @@ def signing_format_message(method, headers_dict, body_dict): ...@@ -149,6 +168,7 @@ def signing_format_message(method, headers_dict, body_dict):
return message return message
def header_string(headers_dict): def header_string(headers_dict):
"""Given a dictionary of headers, return a canonical string representation.""" """Given a dictionary of headers, return a canonical string representation."""
header_list = [] header_list = []
...@@ -160,7 +180,8 @@ def header_string(headers_dict): ...@@ -160,7 +180,8 @@ def header_string(headers_dict):
if 'Content-MD5' in headers_dict: if 'Content-MD5' in headers_dict:
header_list.append(headers_dict['Content-MD5'] + "\n") header_list.append(headers_dict['Content-MD5'] + "\n")
return "".join(header_list) # Note that trailing \n's are important return "".join(header_list) # Note that trailing \n's are important
def body_string(body_dict, prefix=""): def body_string(body_dict, prefix=""):
""" """
...@@ -183,5 +204,5 @@ def body_string(body_dict, prefix=""): ...@@ -183,5 +204,5 @@ def body_string(body_dict, prefix=""):
value = "null" value = "null"
body_list.append(u"{}{}:{}\n".format(prefix, key, value).encode('utf-8')) body_list.append(u"{}{}:{}\n".format(prefix, key, value).encode('utf-8'))
return "".join(body_list) # Note that trailing \n's are important return "".join(body_list) # Note that trailing \n's are important
...@@ -35,11 +35,4 @@ urlpatterns = patterns( ...@@ -35,11 +35,4 @@ urlpatterns = patterns(
name="verify_student_results_callback", name="verify_student_results_callback",
), ),
url(
r'^show_verification_page/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.show_verification_page,
name="verify_student/show_verification_page"
),
) )
""" """
Views for the verification flow
""" """
import json import json
...@@ -37,6 +37,12 @@ class VerifyView(View): ...@@ -37,6 +37,12 @@ class VerifyView(View):
@method_decorator(login_required) @method_decorator(login_required)
def get(self, request, course_id): def get(self, request, course_id):
""" """
Displays the main verification view, which contains three separate steps:
- Taking the standard face photo
- Taking the id photo
- Confirming that the photos and payment price are correct
before proceeding to payment
""" """
# If the user has already been verified within the given time period, # If the user has already been verified within the given time period,
# redirect straight to the payment -- no need to verify again. # redirect straight to the payment -- no need to verify again.
...@@ -69,8 +75,8 @@ class VerifyView(View): ...@@ -69,8 +75,8 @@ class VerifyView(View):
"user_full_name": request.user.profile.name, "user_full_name": request.user.profile.name,
"course_id": course_id, "course_id": course_id,
"course_name": course.display_name_with_default, "course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default, "course_org": course.display_org_with_default,
"course_num" : course.display_number_with_default, "course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(), "purchase_endpoint": get_purchase_endpoint(),
"suggested_prices": [ "suggested_prices": [
decimal.Decimal(price) decimal.Decimal(price)
...@@ -106,8 +112,8 @@ class VerifiedView(View): ...@@ -106,8 +112,8 @@ class VerifiedView(View):
context = { context = {
"course_id": course_id, "course_id": course_id,
"course_name": course.display_name_with_default, "course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default, "course_org": course.display_org_with_default,
"course_num" : course.display_number_with_default, "course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(), "purchase_endpoint": get_purchase_endpoint(),
"currency": verify_mode.currency.upper(), "currency": verify_mode.currency.upper(),
"chosen_price": chosen_price, "chosen_price": chosen_price,
...@@ -162,8 +168,9 @@ def create_order(request): ...@@ -162,8 +168,9 @@ def create_order(request):
return HttpResponse(json.dumps(params), content_type="text/json") return HttpResponse(json.dumps(params), content_type="text/json")
@require_POST @require_POST
@csrf_exempt # SS does its own message signing, and their API won't have a cookie value @csrf_exempt # SS does its own message signing, and their API won't have a cookie value
def results_callback(request): def results_callback(request):
""" """
Software Secure will call this callback to tell us whether a user is Software Secure will call this callback to tell us whether a user is
...@@ -194,7 +201,7 @@ def results_callback(request): ...@@ -194,7 +201,7 @@ def results_callback(request):
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_SECRET_KEY"] settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_SECRET_KEY"]
) )
_, access_key_and_sig = headers["Authorization"].split(" ") _response, access_key_and_sig = headers["Authorization"].split(" ")
access_key = access_key_and_sig.split(":")[0] access_key = access_key_and_sig.split(":")[0]
# This is what we should be doing... # This is what we should be doing...
...@@ -234,10 +241,11 @@ def results_callback(request): ...@@ -234,10 +241,11 @@ def results_callback(request):
return HttpResponse("OK!") return HttpResponse("OK!")
@login_required @login_required
def show_requirements(request, course_id): def show_requirements(request, course_id):
""" """
Show the requirements necessary for Show the requirements necessary for the verification flow.
""" """
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
...@@ -246,69 +254,8 @@ def show_requirements(request, course_id): ...@@ -246,69 +254,8 @@ def show_requirements(request, course_id):
context = { context = {
"course_id": course_id, "course_id": course_id,
"course_name": course.display_name_with_default, "course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default, "course_org": course.display_org_with_default,
"course_num" : course.display_number_with_default, "course_num": course.display_number_with_default,
"is_not_active": not request.user.is_active, "is_not_active": not request.user.is_active,
} }
return render_to_response("verify_student/show_requirements.html", context) return render_to_response("verify_student/show_requirements.html", context)
def show_verification_page(request):
pass
def enroll(user, course_id, mode_slug):
"""
Enroll the user in a course for a certain mode.
This is the view you send folks to when they click on the enroll button.
This does NOT cover changing enrollment modes -- it's intended for new
enrollments only, and will just redirect to the dashboard if it detects
that an enrollment already exists.
"""
# If the user is already enrolled, jump to the dashboard. Yeah, we could
# do upgrades here, but this method is complicated enough.
if CourseEnrollment.is_enrolled(user, course_id):
return HttpResponseRedirect(reverse('dashboard'))
available_modes = CourseModes.modes_for_course(course_id)
# If they haven't chosen a mode...
if not mode_slug:
# Does this course support multiple modes of Enrollment? If so, redirect
# to a page that lets them choose which mode they want.
if len(available_modes) > 1:
return HttpResponseRedirect(
reverse('choose_enroll_mode', kwargs={'course_id': course_id})
)
# Otherwise, we use the only mode that's supported...
else:
mode_slug = available_modes[0].slug
# If the mode is one of the simple, non-payment ones, do the enrollment and
# send them to their dashboard.
if mode_slug in ("honor", "audit"):
CourseEnrollment.enroll(user, course_id, mode=mode_slug)
return HttpResponseRedirect(reverse('dashboard'))
if mode_slug == "verify":
if SoftwareSecurePhotoVerification.has_submitted_recent_request(user):
# Capture payment info
# Create an order
# Create a VerifiedCertificate order item
return HttpResponse.Redirect(reverse('verified'))
# There's always at least one mode available (default is "honor"). If they
# haven't specified a mode, we just assume it's
if not mode:
mode = available_modes[0]
elif len(available_modes) == 1:
if mode != available_modes[0]:
raise Exception()
mode = available_modes[0]
if mode == "honor":
CourseEnrollment.enroll(user, course_id)
return HttpResponseRedirect(reverse('dashboard'))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment