Commit e8196d97 by Julia Hansbrough

Events for entering verified flow & buying

parent 09befd2a
...@@ -17,10 +17,12 @@ from edxmako.shortcuts import render_to_response ...@@ -17,10 +17,12 @@ from edxmako.shortcuts import render_to_response
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.access import has_access from courseware.access import has_access
from student.models import CourseEnrollment from student.models import CourseEnrollment, UserMethods
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
EVENT_NAME_USER_CLICKED_UPGRADE = 'edx.user.upgrade.clicked'
class ChooseModeView(View): class ChooseModeView(View):
""" """
...@@ -37,6 +39,8 @@ class ChooseModeView(View): ...@@ -37,6 +39,8 @@ class ChooseModeView(View):
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id)
upgrade = request.GET.get('upgrade', False) upgrade = request.GET.get('upgrade', False)
if upgrade == "True":
UserMethods.emit_event(request.user, course_id, EVENT_NAME_USER_CLICKED_UPGRADE)
# verified users do not need to register or upgrade # verified users do not need to register or upgrade
if enrollment_mode == 'verified': if enrollment_mode == 'verified':
......
...@@ -16,6 +16,12 @@ import json ...@@ -16,6 +16,12 @@ import json
import logging import logging
import uuid import uuid
import crum
from track import contexts
from track.views import server_track
from eventtracking import tracker
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.auth.signals import user_logged_in, user_logged_out
...@@ -35,9 +41,7 @@ from track import contexts ...@@ -35,9 +41,7 @@ from track import contexts
from track.views import server_track from track.views import server_track
from eventtracking import tracker from eventtracking import tracker
unenroll_done = Signal(providing_args=["course_enrollment"]) unenroll_done = Signal(providing_args=["course_enrollment"])
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
......
...@@ -340,7 +340,14 @@ class EnrollInCourseTest(TestCase): ...@@ -340,7 +340,14 @@ class EnrollInCourseTest(TestCase):
user=user, user=user,
course_id=course_id course_id=course_id
) )
self.assertFalse(enrollment_record.is_active)
def test_user_emitted_events(self):
user = User.objects.create_user("joe", "joe@joe.com", "password")
course_id = "edX/Test101/2013"
course_id_partial = "edX/Test101"
with patch('eventtracking.tracker.get_tracker', side_effect=Exception):
UserMethods.emit_event(user, course_id, "fake")
self.assertTrue(True)
# Make sure mode is updated properly if user unenrolls & re-enrolls # Make sure mode is updated properly if user unenrolls & re-enrolls
enrollment = CourseEnrollment.enroll(user, course_id, "verified") enrollment = CourseEnrollment.enroll(user, course_id, "verified")
......
...@@ -57,3 +57,13 @@ def course_context_from_course_id(course_id): ...@@ -57,3 +57,13 @@ def course_context_from_course_id(course_id):
) )
return context return context
def user_context(user):
"""
Creates a user context from `user`
"""
context = {
'user': user,
}
return context
...@@ -94,5 +94,7 @@ Feature: LMS.Verified certificates ...@@ -94,5 +94,7 @@ Feature: LMS.Verified certificates
And I navigate to my dashboard And I navigate to my dashboard
Then I see the course on my dashboard Then I see the course on my dashboard
And I see that I am on the verified track And I see that I am on the verified track
And a "edx.user.upgrade.clicked" server event is emitted
And a "edx.user.upgrade.purchased" sever event is emitted
And a "edx.course.enrollment.activated" server event is emitted And a "edx.course.enrollment.activated" server event is emitted
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'CertificateItem.upgrade'
db.add_column('shoppingcart_certificateitem', 'upgrade',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CertificateItem.upgrade'
db.delete_column('shoppingcart_certificateitem', 'upgrade')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'shoppingcart.certificateitem': {
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}),
'upgrade': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'shoppingcart.order': {
'Meta': {'object_name': 'Order'},
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.orderitem': {
'Meta': {'object_name': 'OrderItem'},
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.paidcourseregistration': {
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['shoppingcart']
\ No newline at end of file
...@@ -25,7 +25,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -25,7 +25,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from course_modes.models import CourseMode from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from student.views import course_from_id from student.views import course_from_id
from student.models import CourseEnrollment, unenroll_done from student.models import CourseEnrollment, unenroll_done, UserMethods
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
...@@ -43,6 +43,8 @@ ORDER_STATUSES = ( ...@@ -43,6 +43,8 @@ ORDER_STATUSES = (
# we need a tuple to represent the primary key of various OrderItem subclasses # we need a tuple to represent the primary key of various OrderItem subclasses
OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103 OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103
EVENT_NAME_USER_UPGRADED = 'edx.user.upgrade.purchased'
class Order(models.Model): class Order(models.Model):
""" """
...@@ -489,6 +491,7 @@ class CertificateItem(OrderItem): ...@@ -489,6 +491,7 @@ class CertificateItem(OrderItem):
course_id = models.CharField(max_length=128, db_index=True) course_id = models.CharField(max_length=128, db_index=True)
course_enrollment = models.ForeignKey(CourseEnrollment) course_enrollment = models.ForeignKey(CourseEnrollment)
mode = models.SlugField() mode = models.SlugField()
upgrade = models.BooleanField()
@receiver(unenroll_done) @receiver(unenroll_done)
def refund_cert_callback(sender, course_enrollment=None, **kwargs): def refund_cert_callback(sender, course_enrollment=None, **kwargs):
...@@ -557,7 +560,14 @@ class CertificateItem(OrderItem): ...@@ -557,7 +560,14 @@ class CertificateItem(OrderItem):
""" """
super(CertificateItem, cls).add_to_order(order, course_id, cost, currency=currency) super(CertificateItem, cls).add_to_order(order, course_id, cost, currency=currency)
course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id)
try:
# If a course_enrollment already exists, this is an "upgrade" order
course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id)
upgrade = True
except ObjectDoesNotExist:
course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id)
upgrade = False
# do some validation on the enrollment mode # do some validation on the enrollment mode
valid_modes = CourseMode.modes_for_course_dict(course_id) valid_modes = CourseMode.modes_for_course_dict(course_id)
...@@ -570,7 +580,8 @@ class CertificateItem(OrderItem): ...@@ -570,7 +580,8 @@ class CertificateItem(OrderItem):
user=order.user, user=order.user,
course_id=course_id, course_id=course_id,
course_enrollment=course_enrollment, course_enrollment=course_enrollment,
mode=mode mode=mode,
upgrade=upgrade,
) )
item.status = order.status item.status = order.status
item.qty = 1 item.qty = 1
...@@ -595,7 +606,8 @@ class CertificateItem(OrderItem): ...@@ -595,7 +606,8 @@ class CertificateItem(OrderItem):
log.exception( log.exception(
"Could not submit verification attempt for enrollment {}".format(self.course_enrollment) "Could not submit verification attempt for enrollment {}".format(self.course_enrollment)
) )
if self.upgrade is True:
UserMethods.emit_event(self.user, self.course_enrollment.course_id, EVENT_NAME_USER_UPGRADED)
self.course_enrollment.change_mode(self.mode) self.course_enrollment.change_mode(self.mode)
self.course_enrollment.activate() self.course_enrollment.activate()
......
...@@ -6,7 +6,7 @@ import StringIO ...@@ -6,7 +6,7 @@ import StringIO
from textwrap import dedent from textwrap import dedent
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
from mock import patch, MagicMock from mock import patch, MagicMock, sentinel
from django.core import mail from django.core import mail
from django.conf import settings from django.conf import settings
from django.db import DatabaseError from django.db import DatabaseError
...@@ -425,15 +425,37 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -425,15 +425,37 @@ class CertificateItemTest(ModuleStoreTestCase):
min_price=self.cost) min_price=self.cost)
course_mode.save() course_mode.save()
patcher = patch('student.models.server_track')
self.mock_server_track = patcher.start()
self.addCleanup(patcher.stop)
crum_patcher = patch('student.models.crum.get_current_request')
self.mock_get_current_request = crum_patcher.start()
self.addCleanup(crum_patcher.stop)
self.mock_get_current_request.return_value = sentinel.request
def test_existing_enrollment(self): def test_existing_enrollment(self):
CourseEnrollment.enroll(self.user, self.course_id) enrollment = CourseEnrollment.enroll(self.user, self.course_id)
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
# verify that we are still enrolled # verify that we are still enrolled
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id))
self.mock_server_track.reset_mock()
cart.purchase() cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
self.assertEquals(enrollment.mode, u'verified') self.assertEquals(enrollment.mode, u'verified')
self.assert_upgrade_event_was_emitted(self.user, self.course_id)
def assert_upgrade_event_was_emitted(self, user, course_id):
""" Helper function; checks that a particular was called only once """
self.mock_server_track.assert_called_once_with(
sentinel.request,
'edx.user.upgrade.purchased',
{
'user': user,
'course_id': course_id,
}
)
self.mock_server_track.reset_mock()
def test_single_item_template(self): def test_single_item_template(self):
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
......
...@@ -83,6 +83,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -83,6 +83,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
def test_add_course_to_cart_success(self): def test_add_course_to_cart_success(self):
self.login_user() self.login_user()
reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id]))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id)) self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id))
......
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