Commit 739d2f09 by Muhammad Shoaib

Ex-74 Registration Code redemption

fix the translation issues

added a check if a user is already registered in a course. Changed the messages

added course depth=0 and removed pep8 violations
parent 96bc5cc8
......@@ -351,6 +351,7 @@ class CourseRegistrationCode(models.Model):
course_id = CourseKeyField(max_length=255, db_index=True)
created_by = models.ForeignKey(User, related_name='created_by_user')
created_at = models.DateTimeField(default=datetime.now(pytz.utc))
order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order")
invoice = models.ForeignKey(Invoice, null=True)
@classmethod
......@@ -377,7 +378,7 @@ class RegistrationCodeRedemption(models.Model):
"""
This model contains the registration-code redemption info
"""
order = models.ForeignKey(Order, db_index=True)
order = models.ForeignKey(Order, db_index=True, null=True)
registration_code = models.ForeignKey(CourseRegistrationCode, db_index=True)
redeemed_by = models.ForeignKey(User, db_index=True)
redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True)
......@@ -411,6 +412,15 @@ class RegistrationCodeRedemption(models.Model):
log.warning("Course item does not exist against registration code '{0}'".format(course_reg_code.code))
raise ItemDoesNotExistAgainstRegCodeException
@classmethod
def create_invoice_generated_registration_redemption(cls, course_reg_code, user):
"""
This function creates a RegistrationCodeRedemption entry in case the registration codes were invoice generated
and thus the order_id is missing.
"""
code_redemption = RegistrationCodeRedemption(registration_code=course_reg_code, redeemed_by=user)
code_redemption.save()
class SoftDeleteCouponManager(models.Manager):
""" Use this manager to get objects that have a is_active=True """
......
......@@ -13,12 +13,18 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group, User
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core.cache import cache
from pytz import UTC
from freezegun import freeze_time
from datetime import datetime, timedelta
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from shoppingcart.views import _can_download_report, _get_date_from_str
from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration, Coupon, CourseRegistrationCode
from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration, Coupon, CourseRegistrationCode, RegistrationCodeRedemption
from student.tests.factories import UserFactory, AdminFactory
from courseware.tests.factories import InstructorFactory
from student.models import CourseEnrollment
from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_response
......@@ -696,6 +702,124 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
"""
Test suite for RegistrationCodeRedemption Course Enrollments
"""
def setUp(self, **kwargs):
self.user = UserFactory.create()
self.user.set_password('password')
self.user.save()
self.cost = 40
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
self.course_key = self.course.id
self.course_mode = CourseMode(course_id=self.course_key,
mode_slug="honor",
mode_display_name="honor cert",
min_price=self.cost)
self.course_mode.save()
def login_user(self):
"""
Helper fn to login self.user
"""
self.client.login(username=self.user.username, password="password")
def test_registration_redemption_post_request_ratelimited(self):
"""
Try (and fail) registration code redemption 30 times
in a row on an non-existing registration code post request
"""
cache.clear()
url = reverse('register_code_redemption', args=['asdasd'])
self.login_user()
for i in xrange(30): # pylint: disable=W0612
response = self.client.post(url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 404)
# then the rate limiter should kick in and give a HttpForbidden response
response = self.client.post(url)
self.assertEquals(response.status_code, 403)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
with freeze_time(reset_time):
response = self.client.post(url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 404)
cache.clear()
def test_registration_redemption_get_request_ratelimited(self):
"""
Try (and fail) registration code redemption 30 times
in a row on an non-existing registration code get request
"""
cache.clear()
url = reverse('register_code_redemption', args=['asdasd'])
self.login_user()
for i in xrange(30): # pylint: disable=W0612
response = self.client.get(url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 404)
# then the rate limiter should kick in and give a HttpForbidden response
response = self.client.get(url)
self.assertEquals(response.status_code, 403)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
with freeze_time(reset_time):
response = self.client.get(url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 404)
cache.clear()
def test_course_enrollment_active_registration_code_redemption(self):
"""
Test for active registration code course enrollment
"""
cache.clear()
instructor = InstructorFactory(course_key=self.course_key)
self.client.login(username=instructor.username, password='test')
url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()})
data = {
'total_registration_codes': 12, 'company_name': 'Test Group', 'company_contact_name': 'Test@company.com',
'company_contact_email': 'Test@company.com', 'sale_price': 122.45, 'recipient_name': 'Test123',
'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street',
'address_line_2': '', 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '',
'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': ''
}
response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 200)
# get the first registration from the newly created registration codes
registration_code = CourseRegistrationCode.objects.all()[0].code
redeem_url = reverse('register_code_redemption', args=[registration_code])
self.login_user()
response = self.client.get(redeem_url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 200)
# check button text
self.assertTrue('Activate Course Enrollment' in response.content)
#now activate the user by enrolling him/her to the course
response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(response.status_code, 200)
self.assertTrue('View Course' in response.content)
#now check that the registration code has already been redeemed and user is already registered in the course
RegistrationCodeRedemption.objects.filter(registration_code__code=registration_code)
response = self.client.get(redeem_url, **{'HTTP_HOST': 'localhost'})
self.assertEquals(len(RegistrationCodeRedemption.objects.filter(registration_code__code=registration_code)), 1)
self.assertTrue("You've clicked a link for an enrollment code that has already been used." in response.content)
#now check that the registration code has already been redeemed
response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'})
self.assertTrue("You've clicked a link for an enrollment code that has already been used." in response.content)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class CSVReportViewsTest(ModuleStoreTestCase):
"""
Test suite for CSV Purchase Reporting
......
......@@ -14,6 +14,7 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']:
url(r'^clear/$', 'clear_cart'),
url(r'^remove_item/$', 'remove_item'),
url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'),
url(r'^register/redeem/(?P<registration_code>[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'),
url(r'^use_code/$', 'use_code'),
url(r'^register_courses/$', 'register_courses'),
)
......
......@@ -6,12 +6,16 @@ from django.contrib.auth.models import Group
from django.http import (HttpResponse, HttpResponseRedirect, HttpResponseNotFound,
HttpResponseBadRequest, HttpResponseForbidden, Http404)
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_POST, require_http_methods
from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt
from microsite_configuration import microsite
from util.bad_request_rate_limiter import BadRequestRateLimiter
from django.contrib.auth.decorators import login_required
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_by_id
from courseware.views import registered_for_course
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
from student.models import CourseEnrollment
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, \
......@@ -21,6 +25,7 @@ from .processors import process_postpay_callback, render_purchase_form_html
import json
log = logging.getLogger("shoppingcart")
AUDIT_LOG = logging.getLogger("audit")
EVENT_NAME_USER_UPGRADED = 'edx.course.enrollment.upgrade.succeeded'
......@@ -168,6 +173,90 @@ def use_code(request):
return use_coupon_code(coupon, request.user)
def get_reg_code_validity(registration_code, request, limiter):
"""
This function checks if the registration code is valid, and then checks if it was already redeemed.
"""
reg_code_already_redeemed = False
course_registration = None
try:
course_registration = CourseRegistrationCode.objects.get(code=registration_code)
except CourseRegistrationCode.DoesNotExist:
reg_code_is_valid = False
else:
reg_code_is_valid = True
try:
RegistrationCodeRedemption.objects.get(registration_code__code=registration_code)
except RegistrationCodeRedemption.DoesNotExist:
reg_code_already_redeemed = False
else:
reg_code_already_redeemed = True
if not reg_code_is_valid:
#tick the rate limiter counter
AUDIT_LOG.info("Redemption of a non existing RegistrationCode {code}".format(code=registration_code))
limiter.tick_bad_request_counter(request)
raise Http404()
return reg_code_is_valid, reg_code_already_redeemed, course_registration
@require_http_methods(["GET", "POST"])
@login_required
def register_code_redemption(request, registration_code):
"""
This view allows the student to redeem the registration code
and enroll in the course.
"""
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("Rate limit exceeded in registration code redemption.")
return HttpResponseForbidden()
template_to_render = 'shoppingcart/registration_code_receipt.html'
if request.method == "GET":
reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter)
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0)
context = {
'reg_code_already_redeemed': reg_code_already_redeemed,
'reg_code_is_valid': reg_code_is_valid,
'reg_code': registration_code,
'site_name': site_name,
'course': course,
'registered_for_course': registered_for_course(course, request.user)
}
return render_to_response(template_to_render, context)
elif request.method == "POST":
reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter)
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0)
if reg_code_is_valid and not reg_code_already_redeemed:
#now redeem the reg code.
RegistrationCodeRedemption.create_invoice_generated_registration_redemption(course_registration, request.user)
CourseEnrollment.enroll(request.user, course.id)
context = {
'redemption_success': True,
'reg_code': registration_code,
'site_name': site_name,
'course': course,
}
else:
context = {
'reg_code_is_valid': reg_code_is_valid,
'reg_code_already_redeemed': reg_code_already_redeemed,
'redemption_success': False,
'reg_code': registration_code,
'site_name': site_name,
'course': course,
}
return render_to_response(template_to_render, context)
def use_registration_code(course_reg, user):
"""
This method utilize course registration code
......
......@@ -170,4 +170,98 @@
}
}
}
}
.confirm-enrollment {
.title {
font-size:24px;
border-bottom:1px solid #f2f2f2;
text-align: left;
line-height:70px;
}
.course-image {
display: inline-block;
width: 223px;
margin-right: 10px;
vertical-align: top;
}
.enrollment-details {
margin-bottom: 20px;
display: inline-block;
width: calc(100% - 237px);
.sub-title {
font-size: 18px;
text-transform: uppercase;
color: #9b9b93;
}
.course-date-label {
float: right;
color: #9b9b93;
}
.course-dates {
float: right;
font-size: 18px;
}
.course-title {
h1 {
color: #4a4a46;
font-size: 26px;
text-align: left;
font-weight: 600;
}
}
.enrollment-text {
color: #4A4A46;
font-family: 'Open Sans',Verdana,Geneva,sans;
line-height: normal;
a {
font-family: 'Open Sans',Verdana,Geneva,sans;
}
}
}
a.contact-support-bg-color {
background-color: #9b9b9b;
background-image: linear-gradient(#9b9b9b, #9b9b9b);
border: 16px solid #9b9b9b;
box-shadow: 0 1px 0 0 #9b9b9b inset;
text-shadow: 0 1px 0 #9b9b9b;
}
a.course-link-bg-color {
background-color: #00A1E5;
background-image: linear-gradient(#00A1E5, #00A1E5);
border: 16px solid #00A1E5;
box-shadow: 0 1px 0 0 #00A1E5 inset;
text-shadow: 0 1px 0 #00A1E5;
}
a.link-button {
text-transform: none;
width: 250px;
background-clip: padding-box;
float: right;
border-radius: 3px;
color: #FFFFFF;
display: inline-block;
padding: 6px 18px;
text-decoration: none;
font-size: 24px;
text-align: center;
}
input[type="submit"] {
text-transform: none;
width: 450px;
height: 70px;
background-clip: padding-box;
background-color: #00a1e5;
background-image: linear-gradient(#00A1E5,#00A1E5);
float: right;
border: 1px solid #00A1E5;
border-radius: 3px;
box-shadow: 0 1px 0 0 #00A1E5 inset;
color: #FFFFFF;
display: inline-block;
padding: 7px 18px;
text-decoration: none;
text-shadow: 0 1px 0 #00A1E5;
font-size: 24px;
}
}
\ No newline at end of file
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section
%>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("Confirm Enrollment")}</%block>
<%block name="content">
<div class="container">
<section class="wrapper confirm-enrollment">
<header class="page-header">
<h1 class="title">
${_("{site_name} - Confirm Enrollment").format(site_name=site_name)}
</h1>
</header>
<section>
<div class="course-image">
<img style="width: 100%; height: auto;" src="${course_image_url(course)}"
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Cover Image"/>
</div>
<div class="enrollment-details">
<div class="sub-title">${_("Confirm your enrollment for:")}
<span class="course-date-label">${_("course dates")}</span>
<div class="clearfix"></div>
</div>
<div class="course-title">
<h1>
${_("{course_name}").format(course_name=course.display_name)}
<span class="course-dates">${_("{start_date}").format(start_date=course.start_date_text)} - ${_("{end_date}").format(end_date=course.end_date_text)}
</span>
</h1>
</div>
<hr>
<div>
% if reg_code_already_redeemed:
<% dashboard_url = reverse('dashboard')%>
<p class="enrollment-text">
${_("You've clicked a link for an enrollment code that has already been used."
" Check your <a href={dashboard_url}>course dashboard</a> to see if you're enrolled in the course,"
" or contact your company's administrator.").format(dashboard_url=dashboard_url)}
</p>
% elif redemption_success:
<p class="enrollment-text">
${_("You have successfully enrolled in {course_name}."
" This course has now been added to your dashboard.").format(course_name=course.display_name)}
</p>
% elif registered_for_course:
<% dashboard_url = reverse('dashboard')%>
<p class="enrollment-text">
${_("You're already registered for this course."
" Visit your <a href={dashboard_url}>dashboard</a> to see the course.").format(dashboard_url=dashboard_url)}
</p>
% else:
<p class="enrollment-text">
${_("You're about to activate an enrollment code for {course_name} by {site_name}. "
"This code can only be used one time, so you should only activate this code if you're its intended"
" recipient.").format(course_name=course.display_name, site_name=site_name)}
</p>
% endif
</div>
</div>
% if not reg_code_already_redeemed:
%if redemption_success:
<% course_url = reverse('info', args=[course.id.to_deprecated_string()]) %>
<a href="${course_url}" class="link-button course-link-bg-color">${_("View Course &nbsp; &nbsp; &#x25b8;")}</a>
%elif not registered_for_course:
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<input type="submit" value="Activate Course Enrollment &#x25b8;" id="id_active_course_enrollment"
name="active_course_enrollment">
</form>
%endif
%endif
</section>
</section>
</div>
</%block>
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