Commit 665ccdb7 by chrisndodge

Merge pull request #4172 from edx/cdodge/ecommerce-improvements

eCommerce enhancements
parents 256c6bb4 5734d2a8
......@@ -158,3 +158,5 @@ Tim Babych <tim.babych@gmail.com>
Brandon DeRosier <btd@cheesekeg.com>
Daniel Li <swli@edx.org>
Daniel Friedman <dfriedman@edx.org>
Asad Iqbal <aiqbal@edx.org>
Muhammad Shoaib <mshoaib@edx.org>
......@@ -3,7 +3,7 @@ django admin pages for courseware model
'''
from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
from student.models import CourseEnrollment, Registration, PendingNameChange
from student.models import CourseEnrollment, Registration, PendingNameChange, CourseAccessRole
from ratelimitbackend import admin
admin.site.register(UserProfile)
......@@ -17,3 +17,5 @@ admin.site.register(CourseEnrollmentAllowed)
admin.site.register(Registration)
admin.site.register(PendingNameChange)
admin.site.register(CourseAccessRole)
# -*- 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 model 'CourseRegistrationCode'
db.create_table('student_courseregistrationcode', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('transaction_group_name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='created_by_user', to=orm['auth.User'])),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 24, 0, 0))),
('redeemed_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='redeemed_by_user', null=True, to=orm['auth.User'])),
('redeemed_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 24, 0, 0), null=True)),
))
db.send_create_signal('student', ['CourseRegistrationCode'])
def backwards(self, orm):
# Deleting model 'CourseRegistrationCode'
db.delete_table('student_courseregistrationcode')
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'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'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']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseregistrationcode': {
'Meta': {'object_name': 'CourseRegistrationCode'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 24, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 24, 0, 0)', 'null': 'True'}),
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'redeemed_by_user'", 'null': 'True', 'to': "orm['auth.User']"}),
'transaction_group_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
\ No newline at end of file
# -*- 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 model 'UserSignupSource'
db.create_table('student_usersignupsource', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('site', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
))
db.send_create_signal('student', ['UserSignupSource'])
def backwards(self, orm):
# Deleting model 'UserSignupSource'
db.delete_table('student_usersignupsource')
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'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'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']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseregistrationcode': {
'Meta': {'object_name': 'CourseRegistrationCode'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 25, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 25, 0, 0)', 'null': 'True'}),
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'redeemed_by_user'", 'null': 'True', 'to': "orm['auth.User']"}),
'transaction_group_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usersignupsource': {
'Meta': {'object_name': 'UserSignupSource'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'user_id': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
\ No newline at end of file
# -*- 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):
# Deleting model 'CourseRegistrationCode'
db.delete_table('student_courseregistrationcode')
def backwards(self, orm):
# Adding model 'CourseRegistrationCode'
db.create_table('student_courseregistrationcode', (
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('transaction_group_name', self.gf('django.db.models.fields.CharField')(blank=True, max_length=255, null=True, db_index=True)),
('redeemed_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='redeemed_by_user', null=True, to=orm['auth.User'])),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('redeemed_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 25, 0, 0), null=True)),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 25, 0, 0))),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='created_by_user', to=orm['auth.User'])),
))
db.send_create_signal('student', ['CourseRegistrationCode'])
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'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'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']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usersignupsource': {
'Meta': {'object_name': 'UserSignupSource'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'user_id': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
\ No newline at end of file
......@@ -271,6 +271,15 @@ class UserProfile(models.Model):
self.save()
class UserSignupSource(models.Model):
"""
This table contains information about users registering
via Micro-Sites
"""
user_id = models.ForeignKey(User, db_index=True)
site = models.CharField(max_length=255, db_index=True)
def unique_id_for_user(user, save=True):
"""
Return a unique id for a user, suitable for inserting into
......@@ -1035,6 +1044,9 @@ class CourseAccessRole(models.Model):
"""
return self._key < other._key
def __unicode__(self):
return "[CourseAccessRole] user: {} role: {} org: {} course: {}".format(self.user.username, self.role, self.org, self.course_id)
#### Helper methods for use from python manage.py shell and other classes.
......
......@@ -201,6 +201,13 @@ class CourseInstructorRole(CourseRole):
super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs)
class CourseFinanceAdminRole(CourseRole):
"""A course Instructor"""
ROLE = 'finance_admin'
def __init__(self, *args, **kwargs):
super(CourseFinanceAdminRole, self).__init__(self.ROLE, *args, **kwargs)
class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester"""
ROLE = 'beta_testers'
......
"""
Test for User Creation from Micro-Sites
"""
from django.test import TestCase
from student.models import UserSignupSource
import mock
from django.core.urlresolvers import reverse
def fake_site_name(name, default=None): # pylint: disable=W0613
"""
create a fake microsite site name
"""
if name == 'SITE_NAME':
return 'openedx.localhost'
else:
return None
class TestMicrosite(TestCase):
"""Test for Account Creation from a white labeled Micro-Sites"""
def setUp(self):
self.username = "test_user"
self.url = reverse("create_account")
self.params = {
"username": self.username,
"email": "test@example.org",
"password": "testpass",
"name": "Test User",
"honor_code": "true",
"terms_of_service": "true",
}
@mock.patch("microsite_configuration.microsite.get_value", fake_site_name)
def test_user_signup_source(self):
"""
test to create a user form the microsite and see that it record has been
saved in the UserSignupSource Table
"""
response = self.client.post(self.url, self.params)
self.assertEqual(response.status_code, 200)
self.assertGreater(len(UserSignupSource.objects.filter(site='openedx.localhost')), 0)
def test_user_signup_from_non_micro_site(self):
"""
test to create a user form the non-microsite. The record should not be saved
in the UserSignupSource Table
"""
response = self.client.post(self.url, self.params)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(UserSignupSource.objects.filter(site='openedx.localhost')), 0)
......@@ -30,6 +30,9 @@ from django.utils.translation import ugettext as _, get_language
from django.views.decorators.cache import never_cache
from django.views.decorators.http import require_POST, require_GET
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.response import TemplateResponse
from ratelimitbackend.exceptions import RateLimitException
......@@ -42,7 +45,7 @@ from student.models import (
Registration, UserProfile, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user,
CourseEnrollmentAllowed, UserStanding, LoginFailures,
create_comments_service_user, PasswordHistory
create_comments_service_user, PasswordHistory, UserSignupSource
)
from student.forms import PasswordResetFormNoActive
......@@ -1021,6 +1024,21 @@ class AccountValidationError(Exception):
super(AccountValidationError, self).__init__(message)
self.field = field
@receiver(post_save, sender=User)
def user_signup_handler(sender, **kwargs): # pylint: disable=W0613
"""
handler that saves the user Signup Source
when the user is created
"""
if 'created' in kwargs and kwargs['created']:
site = microsite.get_value('SITE_NAME')
if site:
user_signup_source = UserSignupSource(user_id=kwargs['instance'], site=site)
user_signup_source.save()
log.info(u'user {} originated from a white labeled "Microsite"'.format(kwargs['instance'].id))
def _do_create_account(post_vars):
"""
Given cleaned post variables, create the User and UserProfile objects, as well as the
......
"""
Unit tests for Ecommerce feature flag in new instructor dashboard.
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode
from shoppingcart.models import Coupon, PaidCourseRegistration
from mock import patch
from student.roles import CourseFinanceAdminRole
# pylint: disable=E1101
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestECommerceDashboardViews(ModuleStoreTestCase):
"""
Check for email view on the new instructor dashboard
for Mongo-backed courses
"""
def setUp(self):
self.course = CourseFactory.create()
# Create instructor account
self.instructor = AdminFactory.create()
self.client.login(username=self.instructor.username, password="test")
mode = CourseMode(
course_id=self.course.id.to_deprecated_string(), mode_slug='honor',
mode_display_name='honor', min_price=10, currency='usd'
)
mode.save()
# URL for instructor dash
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
self.e_commerce_link = '<a href="" data-section="e-commerce">E-Commerce</a>'
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
def tearDown(self):
"""
Undo all patches.
"""
patch.stopall()
def test_pass_e_commerce_tab_in_instructor_dashboard(self):
"""
Test Pass E-commerce Tab is in the Instructor Dashboard
"""
response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content)
def test_user_has_finance_admin_rights_in_e_commerce_tab(self):
response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content)
# Total amount html should render in e-commerce page, total amount will be 0
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
self.assertTrue('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
# removing the course finance_admin role of login user
CourseFinanceAdminRole(self.course.id).remove_users(self.instructor)
# total amount should not be visible in e-commerce page if the user is not finance admin
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url)
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
self.assertFalse('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
def test_add_coupon(self):
"""
Test Add Coupon Scenarios. Handle all the HttpResponses return by add_coupon view
"""
# URL for add_coupon
add_coupon_url = reverse('add_coupon', kwargs={'course_id': self.course.id.to_deprecated_string()})
data = {
'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(),
'description': 'ADSADASDSAD', 'created_by': self.instructor, 'discount': 5
}
response = self.client.post(add_coupon_url, data)
self.assertTrue("coupon with the coupon code ({code}) added successfully".format(code=data['code']) in response.content)
data = {
'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(),
'description': 'asdsasda', 'created_by': self.instructor, 'discount': 111
}
response = self.client.post(add_coupon_url, data)
self.assertTrue("coupon with the coupon code ({code}) already exist".format(code='A2314') in response.content)
response = self.client.post(self.url)
self.assertTrue('<td>ADSADASDSAD</td>' in response.content)
self.assertTrue('<td>A2314</td>' in response.content)
self.assertFalse('<td>111</td>' in response.content)
def test_delete_coupon(self):
"""
Test Delete Coupon Scenarios. Handle all the HttpResponses return by remove_coupon view
"""
coupon = Coupon(
code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(),
percentage_discount=10, created_by=self.instructor
)
coupon.save()
response = self.client.post(self.url)
self.assertTrue('<td>AS452</td>' in response.content)
# URL for remove_coupon
delete_coupon_url = reverse('remove_coupon', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(delete_coupon_url, {'id': coupon.id})
self.assertTrue('coupon with the coupon id ({coupon_id}) updated successfully'.format(coupon_id=coupon.id) in response.content)
coupon.is_active = False
coupon.save()
response = self.client.post(delete_coupon_url, {'id': coupon.id})
self.assertTrue('coupon with the coupon id ({coupon_id}) is already inactive'.format(coupon_id=coupon.id) in response.content)
response = self.client.post(delete_coupon_url, {'id': 24454})
self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=24454) in response.content)
response = self.client.post(delete_coupon_url, {'id': ''})
self.assertTrue('coupon id is None' in response.content)
def test_get_coupon_info(self):
"""
Test Edit Coupon Info Scenarios. Handle all the HttpResponses return by edit_coupon_info view
"""
coupon = Coupon(
code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(),
percentage_discount=10, created_by=self.instructor
)
coupon.save()
# URL for edit_coupon_info
edit_url = reverse('get_coupon_info', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(edit_url, {'id': coupon.id})
self.assertTrue('coupon with the coupon id ({coupon_id}) updated successfully'.format(coupon_id=coupon.id) in response.content)
response = self.client.post(edit_url, {'id': 444444})
self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=444444) in response.content)
response = self.client.post(edit_url, {'id': ''})
self.assertTrue('coupon id not found"' in response.content)
coupon.is_active = False
coupon.save()
response = self.client.post(edit_url, {'id': coupon.id})
self.assertTrue("coupon with the coupon id ({coupon_id}) is already inactive".format(coupon_id=coupon.id) in response.content)
def test_update_coupon(self):
"""
Test Update Coupon Info Scenarios. Handle all the HttpResponses return by update_coupon view
"""
coupon = Coupon(
code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(),
percentage_discount=10, created_by=self.instructor
)
coupon.save()
response = self.client.post(self.url)
self.assertTrue('<td>AS452</td>' in response.content)
data = {
'coupon_id': coupon.id, 'code': 'update_code', 'discount': '12',
'course_id': coupon.course_id.to_deprecated_string()
}
# URL for update_coupon
update_coupon_url = reverse('update_coupon', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon with the coupon id ({coupon_id}) updated Successfully'.format(coupon_id=coupon.id)in response.content)
response = self.client.post(self.url)
self.assertTrue('<td>update_code</td>' in response.content)
self.assertTrue('<td>12</td>' in response.content)
data['coupon_id'] = 1000 # Coupon Not Exist with this ID
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=1000) in response.content)
data['coupon_id'] = '' # Coupon id is not provided
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon id not found' in response.content)
coupon1 = Coupon(
code='11111', description='coupon', course_id=self.course.id.to_deprecated_string(),
percentage_discount=20, created_by=self.instructor
)
coupon1.save()
data = {'coupon_id': coupon.id, 'code': '11111', 'discount': '12'}
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon with the coupon id ({coupon_id}) already exist'.format(coupon_id=coupon.id) in response.content)
"""
E-commerce Tab Instructor Dashboard Coupons Operations views
"""
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.views.decorators.http import require_POST
from django.utils.translation import ugettext as _
from util.json_request import JsonResponse
from django.http import HttpResponse, HttpResponseNotFound
from shoppingcart.models import Coupon
import logging
log = logging.getLogger(__name__)
@require_POST
@login_required
def remove_coupon(request, course_id): # pylint: disable=W0613
"""
remove the coupon against the coupon id
set the coupon is_active flag to false
"""
coupon_id = request.POST.get('id', None)
if not coupon_id:
return JsonResponse({
'message': _('coupon id is None')
}, status=400) # status code 400: Bad Request
try:
coupon = Coupon.objects.get(id=coupon_id)
except ObjectDoesNotExist:
return JsonResponse({
'message': _('coupon with the coupon id ({coupon_id}) DoesNotExist').format(coupon_id=coupon_id)
}, status=400) # status code 400: Bad Request
if not coupon.is_active:
return JsonResponse({
'message': _('coupon with the coupon id ({coupon_id}) is already inactive').format(coupon_id=coupon_id)
}, status=400) # status code 400: Bad Request
coupon.is_active = False
coupon.save()
return JsonResponse({
'message': _('coupon with the coupon id ({coupon_id}) updated successfully').format(coupon_id=coupon_id)
}) # status code 200: OK by default
@require_POST
@login_required
def add_coupon(request, course_id): # pylint: disable=W0613
"""
add coupon in the Coupons Table
"""
code = request.POST.get('code')
# check if the code is already in the Coupons Table and active
coupon = Coupon.objects.filter(is_active=True, code=code)
if coupon:
return HttpResponseNotFound(_("coupon with the coupon code ({code}) already exist").format(code=code))
description = request.POST.get('description')
course_id = request.POST.get('course_id')
discount = request.POST.get('discount')
coupon = Coupon(
code=code, description=description, course_id=course_id,
percentage_discount=discount, created_by_id=request.user.id
)
coupon.save()
return HttpResponse(_("coupon with the coupon code ({code}) added successfully").format(code=code))
@require_POST
@login_required
def update_coupon(request, course_id): # pylint: disable=W0613
"""
update the coupon object in the database
"""
coupon_id = request.POST.get('coupon_id', None)
if not coupon_id:
return HttpResponseNotFound(_("coupon id not found"))
try:
coupon = Coupon.objects.get(pk=coupon_id)
except ObjectDoesNotExist:
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) DoesNotExist").format(coupon_id=coupon_id))
code = request.POST.get('code')
filtered_coupons = Coupon.objects.filter(~Q(id=coupon_id), code=code, is_active=True)
if filtered_coupons:
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) already exists").format(coupon_id=coupon_id))
description = request.POST.get('description')
course_id = request.POST.get('course_id')
discount = request.POST.get('discount')
coupon.code = code
coupon.description = description
coupon.course_id = course_id
coupon.percentage_discount = discount
coupon.save()
return HttpResponse(_("coupon with the coupon id ({coupon_id}) updated Successfully").format(coupon_id=coupon_id))
@require_POST
@login_required
def get_coupon_info(request, course_id): # pylint: disable=W0613
"""
get the coupon information to display in the pop up form
"""
coupon_id = request.POST.get('id', None)
if not coupon_id:
return JsonResponse({
'message': _("coupon id not found")
}, status=400) # status code 400: Bad Request
try:
coupon = Coupon.objects.get(id=coupon_id)
except ObjectDoesNotExist:
return JsonResponse({
'message': _("coupon with the coupon id ({coupon_id}) DoesNotExist").format(coupon_id=coupon_id)
}, status=400) # status code 400: Bad Request
if not coupon.is_active:
return JsonResponse({
'message': _("coupon with the coupon id ({coupon_id}) is already inactive").format(coupon_id=coupon_id)
}, status=400) # status code 400: Bad Request
return JsonResponse({
'coupon_code': coupon.code,
'coupon_description': coupon.description,
'coupon_course_id': coupon.course_id.to_deprecated_string(),
'coupon_discount': coupon.percentage_discount,
'message': _('coupon with the coupon id ({coupon_id}) updated successfully').format(coupon_id=coupon_id)
}) # status code 200: OK by default
......@@ -26,6 +26,10 @@ from courseware.courses import get_course_by_id, get_cms_course_link, get_course
from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration
from course_modes.models import CourseMode
from student.roles import CourseFinanceAdminRole
from bulk_email.models import CourseAuthorization
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
......@@ -49,6 +53,7 @@ def instructor_dashboard_2(request, course_id):
access = {
'admin': request.user.is_staff,
'instructor': has_access(request.user, 'instructor', course),
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
'staff': has_access(request.user, 'staff', course),
'forum_admin': has_forum_access(
request.user, course_key, FORUM_ROLE_ADMINISTRATOR
......@@ -66,6 +71,12 @@ def instructor_dashboard_2(request, course_id):
_section_analytics(course_key, access),
]
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
course_honor_mode = CourseMode.mode_for_course(course_key, 'honor')
course_mode_has_price = False
if course_honor_mode and course_honor_mode.min_price > 0:
course_mode_has_price = True
if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']):
sections.insert(3, _section_extensions(course))
......@@ -77,6 +88,11 @@ def instructor_dashboard_2(request, course_id):
if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
sections.append(_section_metrics(course_key, access))
# Gate access to Ecommerce tab
if course_mode_has_price:
sections.append(_section_e_commerce(course_key, access))
studio_url = None
if is_studio_course:
studio_url = get_cms_course_link(course)
......@@ -111,6 +127,29 @@ section_display_name will be used to generate link titles in the nav bar.
""" # pylint: disable=W0105
def _section_e_commerce(course_key, access):
""" Provide data for the corresponding dashboard section """
coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active')
total_amount = None
if access['finance_admin']:
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key)
section_data = {
'section_key': 'e-commerce',
'section_display_name': _('E-Commerce'),
'access': access,
'course_id': course_key.to_deprecated_string(),
'ajax_remove_coupon_url': reverse('remove_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_get_coupon_info': reverse('get_coupon_info', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_update_coupon': reverse('update_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'instructor_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}),
'coupons': coupons,
'total_amount': total_amount,
}
return section_data
def _section_course_info(course_key, access):
""" Provide data for the corresponding dashboard section """
course = get_course_by_id(course_key, depth=None)
......
......@@ -28,6 +28,18 @@ class CourseDoesNotExistException(InvalidCartItem):
pass
class CouponDoesNotExistException(InvalidCartItem):
pass
class CouponAlreadyExistException(InvalidCartItem):
pass
class ItemDoesNotExistAgainstCouponException(InvalidCartItem):
pass
class ReportException(Exception):
pass
......
# -*- 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 model 'Coupons'
db.create_table('shoppingcart_coupons', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)),
('percentage_discount', self.gf('django.db.models.fields.IntegerField')(default=0)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 24, 0, 0))),
('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('shoppingcart', ['Coupons'])
# Adding model 'CouponRedemption'
db.create_table('shoppingcart_couponredemption', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('order', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shoppingcart.Order'])),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('coupon', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shoppingcart.Coupons'])),
))
db.send_create_signal('shoppingcart', ['CouponRedemption'])
# Changing field 'CertificateItem.course_id'
db.alter_column('shoppingcart_certificateitem', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=128))
# Changing field 'PaidCourseRegistrationAnnotation.course_id'
db.alter_column('shoppingcart_paidcourseregistrationannotation', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=128))
# Changing field 'PaidCourseRegistration.course_id'
db.alter_column('shoppingcart_paidcourseregistration', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=128))
# Adding field 'OrderItem.discount_price'
db.add_column('shoppingcart_orderitem', 'discount_price',
self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=30, decimal_places=2),
keep_default=False)
def backwards(self, orm):
# Deleting model 'Coupons'
db.delete_table('shoppingcart_coupons')
# Deleting model 'CouponRedemption'
db.delete_table('shoppingcart_couponredemption')
# Changing field 'CertificateItem.course_id'
db.alter_column('shoppingcart_certificateitem', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=128))
# Changing field 'PaidCourseRegistrationAnnotation.course_id'
db.alter_column('shoppingcart_paidcourseregistrationannotation', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=128, unique=True))
# Changing field 'PaidCourseRegistration.course_id'
db.alter_column('shoppingcart_paidcourseregistration', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=128))
# Deleting field 'OrderItem.discount_price'
db.delete_column('shoppingcart_orderitem', 'discount_price')
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': ('xmodule_django.models.CourseKeyField', [], {'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'})
},
'shoppingcart.couponredemption': {
'Meta': {'object_name': 'CouponRedemption'},
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupons']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.coupons': {
'Meta': {'object_name': 'Coupons'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 24, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'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'}),
'refunded_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'}),
'discount_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': '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'}),
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
'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': ('xmodule_django.models.CourseKeyField', [], {'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'})
},
'shoppingcart.paidcourseregistrationannotation': {
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'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
# -*- 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):
# Deleting model 'Coupons'
db.delete_table('shoppingcart_coupons')
# Adding model 'CourseRegistrationCode'
db.create_table('shoppingcart_courseregistrationcode', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('transaction_group_name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='created_by_user', to=orm['auth.User'])),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 7, 1, 0, 0))),
('redeemed_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='redeemed_by_user', null=True, to=orm['auth.User'])),
('redeemed_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 7, 1, 0, 0), null=True)),
))
db.send_create_signal('shoppingcart', ['CourseRegistrationCode'])
# Adding model 'Coupon'
db.create_table('shoppingcart_coupon', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)),
('percentage_discount', self.gf('django.db.models.fields.IntegerField')(default=0)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 7, 1, 0, 0))),
('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('shoppingcart', ['Coupon'])
# Changing field 'CouponRedemption.coupon'
db.alter_column('shoppingcart_couponredemption', 'coupon_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shoppingcart.Coupon']))
# Deleting field 'OrderItem.discount_price'
db.delete_column('shoppingcart_orderitem', 'discount_price')
# Adding field 'OrderItem.list_price'
db.add_column('shoppingcart_orderitem', 'list_price',
self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=30, decimal_places=2),
keep_default=False)
def backwards(self, orm):
# Adding model 'Coupons'
db.create_table('shoppingcart_coupons', (
('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('percentage_discount', self.gf('django.db.models.fields.IntegerField')(default=0)),
('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)),
('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 6, 24, 0, 0))),
('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
))
db.send_create_signal('shoppingcart', ['Coupons'])
# Deleting model 'CourseRegistrationCode'
db.delete_table('shoppingcart_courseregistrationcode')
# Deleting model 'Coupon'
db.delete_table('shoppingcart_coupon')
# Changing field 'CouponRedemption.coupon'
db.alter_column('shoppingcart_couponredemption', 'coupon_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shoppingcart.Coupons']))
# Adding field 'OrderItem.discount_price'
db.add_column('shoppingcart_orderitem', 'discount_price',
self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=30, decimal_places=2),
keep_default=False)
# Deleting field 'OrderItem.list_price'
db.delete_column('shoppingcart_orderitem', 'list_price')
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': ('xmodule_django.models.CourseKeyField', [], {'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'})
},
'shoppingcart.coupon': {
'Meta': {'object_name': 'Coupon'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 7, 1, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'shoppingcart.couponredemption': {
'Meta': {'object_name': 'CouponRedemption'},
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.courseregistrationcode': {
'Meta': {'object_name': 'CourseRegistrationCode'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 7, 1, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 7, 1, 0, 0)', 'null': 'True'}),
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'redeemed_by_user'", 'null': 'True', 'to': "orm['auth.User']"}),
'transaction_group_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
},
'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'}),
'refunded_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', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
'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': ('xmodule_django.models.CourseKeyField', [], {'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'})
},
'shoppingcart.paidcourseregistrationannotation': {
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'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
......@@ -31,7 +31,7 @@ from xmodule_django.models import CourseKeyField
from verify_student.models import SoftwareSecurePhotoVerification
from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException,
AlreadyEnrolledInCourseException, CourseDoesNotExistException)
AlreadyEnrolledInCourseException, CourseDoesNotExistException, CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException)
from microsite_configuration import microsite
......@@ -217,6 +217,7 @@ class OrderItem(models.Model):
status = models.CharField(max_length=32, default='cart', choices=ORDER_STATUSES, db_index=True)
qty = models.IntegerField(default=1)
unit_cost = models.DecimalField(default=0.0, decimal_places=2, max_digits=30)
list_price = models.DecimalField(decimal_places=2, max_digits=30, null=True)
line_desc = models.CharField(default="Misc. Item", max_length=1024)
currency = models.CharField(default="usd", max_length=8) # lower case ISO currency codes
fulfilled_time = models.DateTimeField(null=True, db_index=True)
......@@ -304,6 +305,78 @@ class OrderItem(models.Model):
return ''
class CourseRegistrationCode(models.Model):
"""
This table contains registration codes
With registration code, a user can register for a course for free
"""
code = models.CharField(max_length=32, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
transaction_group_name = models.CharField(max_length=255, db_index=True, null=True, blank=True)
created_by = models.ForeignKey(User, related_name='created_by_user')
created_at = models.DateTimeField(default=datetime.now(pytz.utc))
redeemed_by = models.ForeignKey(User, null=True, related_name='redeemed_by_user')
redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True)
class Coupon(models.Model):
"""
This table contains coupon codes
A user can get a discount offer on course if provide coupon code
"""
code = models.CharField(max_length=32, db_index=True)
description = models.CharField(max_length=255, null=True, blank=True)
course_id = CourseKeyField(max_length=255)
percentage_discount = models.IntegerField(default=0)
created_by = models.ForeignKey(User)
created_at = models.DateTimeField(default=datetime.now(pytz.utc))
is_active = models.BooleanField(default=True)
class CouponRedemption(models.Model):
"""
This table contain coupon redemption info
"""
order = models.ForeignKey(Order, db_index=True)
user = models.ForeignKey(User, db_index=True)
coupon = models.ForeignKey(Coupon, db_index=True)
@classmethod
def get_discount_price(cls, percentage_discount, value):
"""
return discounted price against coupon
"""
discount = Decimal("{0:.2f}".format(Decimal(percentage_discount / 100.00) * value))
return value - discount
@classmethod
def add_coupon_redemption(cls, coupon, order):
"""
add coupon info into coupon_redemption model
"""
cart_items = order.orderitem_set.all().select_subclasses()
for item in cart_items:
if getattr(item, 'course_id'):
if item.course_id == coupon.course_id:
coupon_redemption, created = cls.objects.get_or_create(order=order, user=order.user, coupon=coupon)
if not created:
log.exception("Coupon '{0}' already exist for user '{1}' against order id '{2}'"
.format(coupon.code, order.user.username, order.id))
raise CouponAlreadyExistException
discount_price = cls.get_discount_price(coupon.percentage_discount, item.unit_cost)
item.list_price = item.unit_cost
item.unit_cost = discount_price
item.save()
log.info("Discount generated for user {0} against order id '{1}' "
.format(order.user.username, order.id))
return coupon_redemption
log.warning("Course item does not exist for coupon '{0}'".format(coupon.code))
raise ItemDoesNotExistAgainstCouponException
class PaidCourseRegistration(OrderItem):
"""
This is an inventory item for paying for a course registration
......@@ -320,6 +393,19 @@ class PaidCourseRegistration(OrderItem):
for item in order.orderitem_set.all().select_subclasses("paidcourseregistration")]
@classmethod
def get_total_amount_of_purchased_item(cls, course_key):
"""
This will return the total amount of money that a purchased course generated
"""
total_cost = 0
result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=E1101
if result['total'] is not None:
total_cost = result['total']
return total_cost
@classmethod
@transaction.commit_on_success
def add_to_order(cls, order, course_id, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None):
"""
......
......@@ -16,8 +16,25 @@ from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_string
from shoppingcart.models import Order
from shoppingcart.processors.exceptions import *
from microsite_configuration import microsite
def get_cybersource_config():
"""
This method will return any microsite specific cybersource configuration, otherwise
we return the default configuration
"""
config_key = microsite.get_value('cybersource_config_key')
config = {}
if config_key:
# The microsite CyberSource configuration will be subkeys inside of the normal default
# CyberSource configuration
config = settings.CC_PROCESSOR['CyberSource']['microsites'][config_key]
else:
config = settings.CC_PROCESSOR['CyberSource']
return config
def process_postpay_callback(params):
"""
The top level call to this module, basically
......@@ -53,7 +70,7 @@ def processor_hash(value):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
"""
shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET', '')
shared_secret = get_cybersource_config().get('SHARED_SECRET', '')
hash_obj = hmac.new(shared_secret.encode('utf-8'), value.encode('utf-8'), sha1)
return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want
......@@ -63,9 +80,9 @@ def sign(params, signed_fields_key='orderPage_signedFields', full_sig_key='order
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource
"""
merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID', '')
order_page_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION', '7')
serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER', '')
merchant_id = get_cybersource_config().get('MERCHANT_ID', '')
order_page_version = get_cybersource_config().get('ORDERPAGE_VERSION', '7')
serial_number = get_cybersource_config().get('SERIAL_NUMBER', '')
params['merchantID'] = merchant_id
params['orderPage_timestamp'] = int(time.time() * 1000)
......@@ -123,7 +140,7 @@ def get_purchase_params(cart):
return params
def get_purchase_endpoint():
return settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT', '')
return get_cybersource_config().get('PURCHASE_ENDPOINT', '')
def payment_accepted(params):
"""
......@@ -215,7 +232,9 @@ def record_purchase(params, order):
def get_processor_decline_html(params):
"""Have to parse through the error codes to return a helpful message"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
# see if we have an override in the microsites
payment_support_email = microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)
msg = dedent(_(
"""
......@@ -238,7 +257,8 @@ def get_processor_decline_html(params):
def get_processor_exception_html(exception):
"""Return error HTML associated with exception"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
# see if we have an override in the microsites
payment_support_email = microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)
if isinstance(exception, CCProcessorDataException):
msg = dedent(_(
"""
......
### Implementation of support for the Cybersource Credit card processor using the new
### Secure Acceptance API. The previous Hosted Order Page API is being deprecated as of 9/14
### It is mostly the same as the CyberSource.py file, but we have a new file so that we can
### maintain some backwards-compatibility in case of a need to quickly roll back (i.e.
### configuration change rather than code rollback )
### The name of this file should be used as the key of the dict in the CC_PROCESSOR setting
### Implementes interface as specified by __init__.py
import hmac
import binascii
import re
import json
import uuid
from datetime import datetime
from collections import OrderedDict, defaultdict
from decimal import Decimal, InvalidOperation
from hashlib import sha256
from textwrap import dedent
from django.conf import settings
from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_string
from shoppingcart.models import Order
from shoppingcart.processors.exceptions import *
from microsite_configuration import microsite
from django.core.urlresolvers import reverse
def get_cybersource_config():
"""
This method will return any microsite specific cybersource configuration, otherwise
we return the default configuration
"""
config_key = microsite.get_value('cybersource_config_key')
config = {}
if config_key:
# The microsite CyberSource configuration will be subkeys inside of the normal default
# CyberSource configuration
config = settings.CC_PROCESSOR['CyberSource2']['microsites'][config_key]
else:
config = settings.CC_PROCESSOR['CyberSource2']
return config
def process_postpay_callback(params):
"""
The top level call to this module, basically
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external Hosted Order Page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
try:
result = payment_accepted(params)
if result['accepted']:
# SUCCESS CASE first, rest are some sort of oddity
record_purchase(params, result['order'])
return {'success': True,
'order': result['order'],
'error_html': ''}
else:
return {'success': False,
'order': result['order'],
'error_html': get_processor_decline_html(params)}
except CCProcessorException as error:
return {'success': False,
'order': None, # due to exception we may not have the order
'error_html': get_processor_exception_html(error)}
def processor_hash(value):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
"""
secret_key = get_cybersource_config().get('SECRET_KEY', '')
hash_obj = hmac.new(secret_key, value, sha256)
return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want
def sign(params, signed_fields_key='signed_field_names', full_sig_key='signature'):
"""
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource
"""
fields = u",".join(params.keys())
params[signed_fields_key] = fields
signed_fields = params.get(signed_fields_key, '').split(',')
values = u",".join([u"{0}={1}".format(i, params.get(i, '')) for i in signed_fields])
params[full_sig_key] = processor_hash(values)
params[signed_fields_key] = fields
return params
def render_purchase_form_html(cart):
"""
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
"""
return render_to_string('shoppingcart/cybersource_form.html', {
'action': get_purchase_endpoint(),
'params': get_signed_purchase_params(cart),
})
def get_signed_purchase_params(cart):
"""
This method will return a digitally signed set of CyberSource parameters
"""
return sign(get_purchase_params(cart))
def get_purchase_params(cart):
"""
This method will build out a dictionary of parameters needed by CyberSource to complete the transaction
"""
total_cost = cart.total_cost
amount = "{0:0.2f}".format(total_cost)
params = OrderedDict()
params['amount'] = amount
params['currency'] = cart.currency
params['orderNumber'] = "OrderId: {0:d}".format(cart.id)
params['access_key'] = get_cybersource_config().get('ACCESS_KEY', '')
params['profile_id'] = get_cybersource_config().get('PROFILE_ID', '')
params['reference_number'] = cart.id
params['transaction_type'] = 'sale'
params['locale'] = 'en'
params['signed_date_time'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
params['signed_field_names'] = 'access_key,profile_id,amount,currency,transaction_type,reference_number,signed_date_time,locale,transaction_uuid,signed_field_names,unsigned_field_names,orderNumber'
params['unsigned_field_names'] = ''
params['transaction_uuid'] = uuid.uuid4()
params['payment_method'] = 'card'
if hasattr(cart, 'context') and 'request_domain' in cart.context:
params['override_custom_receipt_page'] = '{0}{1}'.format(
cart.context['request_domain'],
reverse('shoppingcart.views.postpay_callback')
)
return params
def get_purchase_endpoint():
"""
Helper function to return the CyberSource endpoint configuration
"""
return get_cybersource_config().get('PURCHASE_ENDPOINT', '')
def payment_accepted(params):
"""
Check that cybersource has accepted the payment
params: a dictionary of POST parameters returned by CyberSource in their post-payment callback
returns: true if the payment was correctly accepted, for the right amount
false if the payment was not accepted
raises: CCProcessorDataException if the returned message did not provide required parameters
CCProcessorWrongAmountException if the amount charged is different than the order amount
"""
#make sure required keys are present and convert their values to the right type
valid_params = {}
for key, key_type in [('req_reference_number', int),
('req_currency', str),
('decision', str)]:
if key not in params:
raise CCProcessorDataException(
_("The payment processor did not return a required parameter: {0}".format(key))
)
try:
valid_params[key] = key_type(params[key])
except ValueError:
raise CCProcessorDataException(
_("The payment processor returned a badly-typed value {0} for param {1}.".format(params[key], key))
)
try:
order = Order.objects.get(id=valid_params['req_reference_number'])
except Order.DoesNotExist:
raise CCProcessorDataException(_("The payment processor accepted an order whose number is not in our system."))
if valid_params['decision'] == 'ACCEPT':
try:
# Moved reading of charged_amount here from the valid_params loop above because
# only 'ACCEPT' messages have a 'ccAuthReply_amount' parameter
charged_amt = Decimal(params['auth_amount'])
except InvalidOperation:
raise CCProcessorDataException(
_("The payment processor returned a badly-typed value {0} for param {1}.".format(
params['auth_amount'], 'auth_amount'))
)
if charged_amt == order.total_cost and valid_params['req_currency'] == order.currency:
return {'accepted': True,
'amt_charged': charged_amt,
'currency': valid_params['req_currency'],
'order': order}
else:
raise CCProcessorWrongAmountException(
_("The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}."
.format(charged_amt, valid_params['req_currency'],
order.total_cost, order.currency))
)
else:
return {'accepted': False,
'amt_charged': 0,
'currency': 'usd',
'order': order}
def record_purchase(params, order):
"""
Record the purchase and run purchased_callbacks
"""
ccnum_str = params.get('req_card_number', '')
mm = re.search("\d", ccnum_str)
if mm:
ccnum = ccnum_str[mm.start():]
else:
ccnum = "####"
order.purchase(
first=params.get('req_bill_to_forename', ''),
last=params.get('req_bill_to_surname', ''),
street1=params.get('req_bill_to_address_line1', ''),
street2=params.get('req_bill_to_address_line2', ''),
city=params.get('req_bill_to_address_city', ''),
state=params.get('req_bill_to_address_state', ''),
country=params.get('req_bill_to_address_country', ''),
postalcode=params.get('req_bill_to_address_postal_code', ''),
ccnum=ccnum,
cardtype=CARDTYPE_MAP[params.get('req_card_type', '')],
processor_reply_dump=json.dumps(params)
)
def get_processor_decline_html(params):
"""Have to parse through the error codes to return a helpful message"""
payment_support_email = microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)
msg = dedent(_(
"""
<p class="error_msg">
Sorry! Our payment processor did not accept your payment.
The decision they returned was <span class="decision">{decision}</span>,
and the reason was <span class="reason">{reason_code}:{reason_msg}</span>.
You were not charged. Please try a different form of payment.
Contact us with payment-related questions at {email}.
</p>
"""))
return msg.format(
decision=params['decision'],
reason_code=params['reason_code'],
reason_msg=REASONCODE_MAP[params['reason_code']],
email=payment_support_email
)
def get_processor_exception_html(exception):
"""Return error HTML associated with exception"""
payment_support_email = microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL)
if isinstance(exception, CCProcessorDataException):
msg = dedent(_(
"""
<p class="error_msg">
Sorry! Our payment processor sent us back a payment confirmation that had inconsistent data!
We apologize that we cannot verify whether the charge went through and take further action on your order.
The specific error message is: <span class="exception_msg">{msg}</span>.
Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}.
</p>
""".format(msg=exception.message, email=payment_support_email)))
return msg
elif isinstance(exception, CCProcessorWrongAmountException):
msg = dedent(_(
"""
<p class="error_msg">
Sorry! Due to an error your purchase was charged for a different amount than the order total!
The specific error message is: <span class="exception_msg">{msg}</span>.
Your credit card has probably been charged. Contact us with payment-specific questions at {email}.
</p>
""".format(msg=exception.message, email=payment_support_email)))
return msg
# fallthrough case, which basically never happens
return '<p class="error_msg">EXCEPTION!</p>'
CARDTYPE_MAP = defaultdict(lambda: "UNKNOWN")
CARDTYPE_MAP.update(
{
'001': 'Visa',
'002': 'MasterCard',
'003': 'American Express',
'004': 'Discover',
'005': 'Diners Club',
'006': 'Carte Blanche',
'007': 'JCB',
'014': 'EnRoute',
'021': 'JAL',
'024': 'Maestro',
'031': 'Delta',
'033': 'Visa Electron',
'034': 'Dankort',
'035': 'Laser',
'036': 'Carte Bleue',
'037': 'Carta Si',
'042': 'Maestro Int.',
'043': 'GE Money UK card'
}
)
REASONCODE_MAP = defaultdict(lambda: "UNKNOWN REASON")
REASONCODE_MAP.update(
{
'100': _('Successful transaction.'),
'102': _('One or more fields in the request contains invalid data.'),
'104': dedent(_(
"""
The access_key and transaction_uuid fields for this authorization request matches the access_key and
transaction_uuid of another authorization request that you sent in the last 15 minutes.
Possible fix: retry the payment after 15 minutes.
""")),
'110': _('Only a partial amount was approved.'),
'200': dedent(_(
"""
The authorization request was approved by the issuing bank but declined by CyberSource
becouse it did not pass the Address Verification System (AVS).
""")),
'201': dedent(_(
"""
The issuing bank has questions about the request. You do not receive an
authorization code programmatically, but you might receive one verbally by calling the processor.
Possible fix: retry with another form of payment
""")),
'202': dedent(_(
"""
Expired card. You might also receive this if the expiration date you
provided does not match the date the issuing bank has on file.
Possible fix: retry with another form of payment
""")),
'203': dedent(_(
"""
General decline of the card. No other information provided by the issuing bank.
Possible fix: retry with another form of payment
""")),
'204': _('Insufficient funds in the account. Possible fix: retry with another form of payment'),
# 205 was Stolen or lost card. Might as well not show this message to the person using such a card.
'205': _('Stolen or lost card'),
'207': _('Issuing bank unavailable. Possible fix: retry again after a few minutes'),
'208': dedent(_(
"""
Inactive card or card not authorized for card-not-present transactions.
Possible fix: retry with another form of payment
""")),
'210': _('The card has reached the credit limit. Possible fix: retry with another form of payment'),
'211': _('Invalid card verification number (CVN). Possible fix: retry with another form of payment'),
# 221 was The customer matched an entry on the processor's negative file.
# Might as well not show this message to the person using such a card.
'221': _('The customer matched an entry on the processors negative file.'),
'222': _('Account frozen. Possible fix: retry with another form of payment'),
'230': dedent(_(
"""
The authorization request was approved by the issuing bank but declined by
CyberSource because it did not pass the CVN check.
Possible fix: retry with another form of payment
""")),
'231': _('Invalid account number. Possible fix: retry with another form of payment'),
'232': dedent(_(
"""
The card type is not accepted by the payment processor.
Possible fix: retry with another form of payment
""")),
'233': _('General decline by the processor. Possible fix: retry with another form of payment'),
'234': dedent(_(
"""
There is a problem with the information in your CyberSource account. Please let us know at {0}
""".format(settings.PAYMENT_SUPPORT_EMAIL))),
'236': _('Processor Failure. Possible fix: retry the payment'),
'240': dedent(_(
"""
The card type sent is invalid or does not correlate with the credit card number.
Possible fix: retry with the same card or another form of payment
""")),
'475': _('The cardholder is enrolled for payer authentication'),
'476': _('Payer authentication could not be authenticated'),
'520': dedent(_(
"""
The authorization request was approved by the issuing bank but declined by CyberSource based
on your legacy Smart Authorization settings.
Possible fix: retry with a different form of payment.
""")),
}
)
......@@ -10,6 +10,8 @@ from shoppingcart.models import Order, OrderItem
from shoppingcart.processors.CyberSource import *
from shoppingcart.processors.exceptions import *
from mock import patch, Mock
from microsite_configuration import microsite
import mock
TEST_CC_PROCESSOR = {
......@@ -19,10 +21,28 @@ TEST_CC_PROCESSOR = {
'SERIAL_NUMBER': '12345',
'ORDERPAGE_VERSION': '7',
'PURCHASE_ENDPOINT': '',
'microsites': {
'test_microsite': {
'SHARED_SECRET': 'secret_override',
'MERCHANT_ID': 'edx_test_override',
'SERIAL_NUMBER': '12345_override',
'ORDERPAGE_VERSION': '7',
'PURCHASE_ENDPOINT': '',
}
}
}
}
def fakemicrosite(name, default=None):
"""
This is a test mocking function to return a microsite configuration
"""
if name == 'cybersource_config_key':
return 'test_microsite'
else:
return None
@override_settings(CC_PROCESSOR=TEST_CC_PROCESSOR)
class CyberSourceTests(TestCase):
......@@ -33,6 +53,15 @@ class CyberSourceTests(TestCase):
self.assertEqual(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test')
self.assertEqual(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret')
def test_microsite_no_override_settings(self):
self.assertEqual(get_cybersource_config()['MERCHANT_ID'], 'edx_test')
self.assertEqual(get_cybersource_config()['SHARED_SECRET'], 'secret')
@mock.patch("microsite_configuration.microsite.get_value", fakemicrosite)
def test_microsite_override_settings(self):
self.assertEqual(get_cybersource_config()['MERCHANT_ID'], 'edx_test_override')
self.assertEqual(get_cybersource_config()['SHARED_SECRET'], 'secret_override')
def test_hash(self):
"""
Tests the hash function. Basically just hardcodes the answer.
......
......@@ -14,7 +14,7 @@ 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
from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration, Coupon
from student.tests.factories import UserFactory
from student.models import CourseEnrollment
from course_modes.models import CourseMode
......@@ -22,7 +22,8 @@ from edxmako.shortcuts import render_to_response
from shoppingcart.processors import render_purchase_form_html
from mock import patch, Mock
from shoppingcart.views import initialize_report
from decimal import Decimal
from student.tests.factories import AdminFactory
def mock_render_purchase_form_html(*args, **kwargs):
return render_purchase_form_html(*args, **kwargs)
......@@ -45,7 +46,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.user = UserFactory.create()
self.user.set_password('password')
self.user.save()
self.instructor = AdminFactory.create()
self.cost = 40
self.coupon_code = 'abcde'
self.percentage_discount = 10
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,
......@@ -58,6 +62,29 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.cart = Order.get_cart_for_user(self.user)
self.addCleanup(patcher.stop)
def get_discount(self):
"""
This method simple return the discounted amount
"""
val = Decimal("{0:.2f}".format(Decimal(self.percentage_discount / 100.00) * self.cost))
return self.cost - val
def add_coupon(self, course_key, is_active):
"""
add dummy coupon into models
"""
coupon = Coupon(code=self.coupon_code, description='testing code', course_id=course_key,
percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active)
coupon.save()
def add_course_to_user_cart(self):
"""
adding course to user cart
"""
self.login_user()
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
return reg_item
def login_user(self):
self.client.login(username=self.user.username, password="password")
......@@ -72,6 +99,141 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 400)
self.assertIn('The course {0} is already in your cart.'.format(self.course_key.to_deprecated_string()), resp.content)
def test_course_discount_invalid_coupon(self):
self.add_coupon(self.course_key, True)
self.add_course_to_user_cart()
non_existing_code = "non_existing_code"
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': non_existing_code})
self.assertEqual(resp.status_code, 404)
self.assertIn("Discount does not exist against coupon '{0}'.".format(non_existing_code), resp.content)
def test_course_discount_inactive_coupon(self):
self.add_coupon(self.course_key, False)
self.add_course_to_user_cart()
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 400)
self.assertIn("Coupon '{0}' is inactive.".format(self.coupon_code), resp.content)
def test_course_does_not_exist_in_cart_against_valid_coupon(self):
course_key = self.course_key.to_deprecated_string() + 'testing'
self.add_coupon(course_key, True)
self.add_course_to_user_cart()
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 404)
self.assertIn("Coupon '{0}' is not valid for any course in the shopping cart.".format(self.coupon_code), resp.content)
def test_course_discount_for_valid_active_coupon_code(self):
self.add_coupon(self.course_key, True)
self.add_course_to_user_cart()
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
# unit price should be updated for that course
item = self.cart.orderitem_set.all().select_subclasses()[0]
self.assertEquals(item.unit_cost, self.get_discount())
# after getting 10 percent discount
self.assertEqual(self.cart.total_cost, self.get_discount())
# now testing coupon code already used scenario, reusing the same coupon code
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 400)
self.assertIn("Coupon '{0}' already used.".format(self.coupon_code), resp.content)
@patch('shoppingcart.views.log.debug')
def test_non_existing_coupon_redemption_on_removing_item(self, debug_log):
reg_item = self.add_course_to_user_cart()
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
{'id': reg_item.id})
debug_log.assert_called_with(
'Coupon redemption does not exist for order item id={0}.'.format(reg_item.id))
self.assertEqual(resp.status_code, 200)
self.assertEquals(self.cart.orderitem_set.count(), 0)
@patch('shoppingcart.views.log.info')
def test_existing_coupon_redemption_on_removing_item(self, info_log):
self.add_coupon(self.course_key, True)
reg_item = self.add_course_to_user_cart()
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
{'id': reg_item.id})
self.assertEqual(resp.status_code, 200)
self.assertEquals(self.cart.orderitem_set.count(), 0)
info_log.assert_called_with(
'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id))
@patch('shoppingcart.views.log.info')
def test_coupon_discount_for_multiple_courses_in_cart(self, info_log):
reg_item = self.add_course_to_user_cart()
self.add_coupon(self.course_key, True)
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
self.assertEquals(self.cart.orderitem_set.count(), 2)
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
# unit_cost should be updated for that particular course for which coupon code is registered
items = self.cart.orderitem_set.all().select_subclasses()
for item in items:
if item.id == reg_item.id:
self.assertEquals(item.unit_cost, self.get_discount())
elif item.id == cert_item.id:
self.assertEquals(item.list_price, None)
# Delete the discounted item, corresponding coupon redemption should be removed for that particular discounted item
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
{'id': reg_item.id})
self.assertEqual(resp.status_code, 200)
self.assertEquals(self.cart.orderitem_set.count(), 1)
info_log.assert_called_with(
'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id))
@patch('shoppingcart.views.log.info')
def test_delete_certificate_item(self, info_log):
reg_item = self.add_course_to_user_cart()
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
self.assertEquals(self.cart.orderitem_set.count(), 2)
# Delete the discounted item, corresponding coupon redemption should be removed for that particular discounted item
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
{'id': cert_item.id})
self.assertEqual(resp.status_code, 200)
self.assertEquals(self.cart.orderitem_set.count(), 1)
info_log.assert_called_with(
'order item {0} removed for user {1}'.format(cert_item.id, self.user))
@patch('shoppingcart.views.log.info')
def test_remove_coupon_redemption_on_clear_cart(self, info_log):
reg_item = self.add_course_to_user_cart()
CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
self.assertEquals(self.cart.orderitem_set.count(), 2)
self.add_coupon(self.course_key, True)
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[]))
self.assertEqual(resp.status_code, 200)
self.assertEquals(self.cart.orderitem_set.count(), 0)
info_log.assert_called_with(
'Coupon redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id))
def test_add_course_to_cart_already_registered(self):
CourseEnrollment.enroll(self.user, self.course_key)
self.login_user()
......@@ -188,6 +350,41 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
resp2 = self.client.get(reverse('shoppingcart.views.show_receipt', args=[1000]))
self.assertEqual(resp2.status_code, 404)
def test_total_amount_of_purchased_course(self):
self.add_course_to_user_cart()
self.assertEquals(self.cart.orderitem_set.count(), 1)
self.add_coupon(self.course_key, True)
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
# Total amount of a particular course that is purchased by different users
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course_key)
self.assertEqual(total_amount, 36)
self.client.login(username=self.instructor.username, password="test")
cart = Order.get_cart_for_user(self.instructor)
PaidCourseRegistration.add_to_order(cart, self.course_key)
cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course_key)
self.assertEqual(total_amount, 76)
@patch('shoppingcart.views.render_to_response', render_mock)
def test_show_receipt_success_with_valid_coupon_code(self):
self.add_course_to_user_cart()
self.add_coupon(self.course_key, True)
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id]))
self.assertEqual(resp.status_code, 200)
self.assertIn('FirstNameTesting123', resp.content)
self.assertIn(str(self.get_discount()), resp.content)
@patch('shoppingcart.views.render_to_response', render_mock)
def test_show_receipt_success(self):
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
......
......@@ -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'^use_coupon/$', 'use_coupon'),
)
if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'):
......
......@@ -14,9 +14,10 @@ from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
from student.models import CourseEnrollment
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException
from .models import Order, PaidCourseRegistration, OrderItem
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException
from .models import Order, PaidCourseRegistration, OrderItem, Coupon, CouponRedemption
from .processors import process_postpay_callback, render_purchase_form_html
import json
log = logging.getLogger("shoppingcart")
......@@ -69,6 +70,16 @@ def show_cart(request):
cart = Order.get_cart_for_user(request.user)
total_cost = cart.total_cost
cart_items = cart.orderitem_set.all()
# add the request protocol, domain, and port to the cart object so that any specific
# CC_PROCESSOR implementation can construct callback URLs, if necessary
cart.context = {
'request_domain': '{0}://{1}'.format(
'https' if request.is_secure() else 'http',
request.get_host()
)
}
form_html = render_purchase_form_html(cart)
return render_to_response("shoppingcart/list.html",
{'shoppingcart_items': cart_items,
......@@ -81,6 +92,11 @@ def show_cart(request):
def clear_cart(request):
cart = Order.get_cart_for_user(request.user)
cart.clear()
coupon_redemption = CouponRedemption.objects.filter(user=request.user, order=cart.id)
if coupon_redemption:
coupon_redemption.delete()
log.info('Coupon redemption entry removed for user {0} for order {1}'.format(request.user, cart.id))
return HttpResponse('Cleared')
......@@ -90,12 +106,50 @@ def remove_item(request):
try:
item = OrderItem.objects.get(id=item_id, status='cart')
if item.user == request.user:
order_item_course_id = None
if hasattr(item, 'paidcourseregistration'):
order_item_course_id = item.paidcourseregistration.course_id
item.delete()
log.info('order item {0} removed for user {1}'.format(item_id, request.user))
try:
coupon_redemption = CouponRedemption.objects.get(user=request.user, order=item.order_id)
if order_item_course_id == coupon_redemption.coupon.course_id:
coupon_redemption.delete()
log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(coupon_redemption.coupon.code, request.user, item_id))
except CouponRedemption.DoesNotExist:
log.debug('Coupon redemption does not exist for order item id={0}.'.format(item_id))
except OrderItem.DoesNotExist:
log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id))
return HttpResponse('OK')
@login_required
def use_coupon(request):
"""
This method generate discount against valid coupon code and save its entry into coupon redemption table
"""
coupon_code = request.POST["coupon_code"]
try:
coupon = Coupon.objects.get(code=coupon_code)
except Coupon.DoesNotExist:
return HttpResponseNotFound(_("Discount does not exist against coupon '{0}'.".format(coupon_code)))
if coupon.is_active:
try:
cart = Order.get_cart_for_user(request.user)
CouponRedemption.add_coupon_redemption(coupon, cart)
except CouponAlreadyExistException:
return HttpResponseBadRequest(_("Coupon '{0}' already used.".format(coupon_code)))
except ItemDoesNotExistAgainstCouponException:
return HttpResponseNotFound(_("Coupon '{0}' is not valid for any course in the shopping cart.".format(coupon_code)))
response = HttpResponse(json.dumps({'response': 'success'}), content_type="application/json")
return response
else:
return HttpResponseBadRequest(_("Coupon '{0}' is inactive.".format(coupon_code)))
@csrf_exempt
@require_POST
def postpay_callback(request):
......@@ -122,6 +176,7 @@ def show_receipt(request, ordernum):
Displays a receipt for a particular order.
404 if order is not yet purchased or request.user != order.user
"""
try:
order = Order.objects.get(id=ordernum)
except Order.DoesNotExist:
......
......@@ -597,7 +597,7 @@ section.instructor-dashboard-content-2 {
float: left;
clear: both;
margin-top: 25px;
.metrics-left, .metrics-left-header {
position: relative;
width: 30%;
......@@ -611,7 +611,7 @@ section.instructor-dashboard-content-2 {
.metrics-section.metrics-left {
height: 640px;
}
.metrics-right, .metrics-right-header {
position: relative;
width: 65%;
......@@ -627,7 +627,7 @@ section.instructor-dashboard-content-2 {
.metrics-section.metrics-right {
height: 295px;
}
svg {
.stacked-bar {
cursor: pointer;
......@@ -775,3 +775,267 @@ input[name="subject"] {
font-weight: bold;
}
}
.ecommerce-wrapper{
h2{
height: 26px;
line-height: 26px;
span{
float: right;
font-size: 16px;
font-weight: bold;
span{
background: #ddd;
padding: 2px 9px;
border-radius: 2px;
float: none;
font-weight: 400;
}
}
}
span.tip{
padding: 10px 15px;
display: block;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
background: #f8f4ec;
color: #3c3c3c;
line-height: 30px;
.add{
@include button(simple, $blue);
@extend .button-reset;
font-size: em(13);
float: right;
}
}
}
#e-commerce{
.coupon-errors {
background: #FFEEF5;color:#B72667;text-align: center;padding: 10px 0px;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;font-size: 15px;
border-bottom: 1px solid #B72667;
margin-bottom: 20px;
display: none;
}
.content{
padding: 0 !important;
}
.coupons-table {
width: 100%;
tr:nth-child(even){
background-color: #f8f8f8;
border-bottom: 1px solid #f3f3f3;
}
tr.always-gray{
background: #eee !important;
border-top: 2px solid #FFFFFF;
}
tr.always-white{
background: #fff !important;
td{
padding: 30px 0px 10px;
}
}
.coupons-headings {
height: 40px;
border-bottom: 1px solid #BEBEBE;
th:nth-child(5){
text-align: center;
width: 120px;
}
th:first-child{
padding-left: 20px;
}
th {
text-align: left;
border-bottom: 1px solid $border-color-1;
&.c_code {
width: 170px;
}
&.c_count {
width: 85px;
}
&.c_course_id {
width: 320px;
word-wrap: break-word;
}
&.c_discount {
width: 90px;
}
&.c_action {
width: 89px;
}
&.c_dsc{
width: 260px;
word-wrap: break-word;
}
}
}
// in_active coupon rows style
.inactive_coupon{
background: #FFF0F0 !important;
text-decoration: line-through;
color: rgba(51,51,51,0.2);
border-bottom: 1px solid #fff;
td {
a {
color: rgba(51,51,51,0.2);
}
}
}
// coupon items style
.coupons-items {
td {
padding: 10px 0px;
position: relative;
line-height: normal;
span.old-price{
left: -75px;
position: relative;
text-decoration: line-through;
color: red;
font-size: 12px;
top: -1px;
}
}
td:nth-child(5),td:first-child{
padding-left: 20px;
}
td:nth-child(2){
line-height: 22px;
padding-right: 0px;
word-wrap: break-word;
}
td:nth-child(5){
padding-left: 0;
text-align: center;
}
td{
a.edit-right{
margin-left: 15px;
}
}
}
}
// coupon edit and add modals
#add-coupon-modal, #edit-coupon-modal{
.inner-wrapper {
background: #fff;
}
top:-95px !important;
width: 650px;
margin-left: -325px;
border-radius: 2px;
input[type="submit"]#update_coupon_button{
@include button(simple, $blue);
@extend .button-reset;
}
input[type="submit"]#add_coupon_button{
@include button(simple, $blue);
@extend .button-reset;
}
.modal-form-error {
box-shadow: inset 0 -1px 2px 0 #f3d9db;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 20px 0 10px 0 !important;
padding: 20px;
border: none;
border-bottom: 3px solid #a0050e;
background: #fbf2f3;
}
ol.list-input{
li{
width: 278px;
float: left;
label.required:after {
content: "*";
margin-left: 5px;
}
}
li:nth-child(even){
margin-left: 30px !important;
}
li:nth-child(3), li:nth-child(4){
margin-left: 0px !important;
width: 100%;
}
li:nth-child(3) {
margin-bottom: 0px !important;
textarea {
min-height: 100px;
}
}
li:last-child{
margin-bottom: 0px !important;
}
}
#coupon-content {
padding: 20px;
header {
margin: 0;
padding: 0;
h2 {
font-size: 24px;
font-weight: 100;
color: #1580b0;
text-align: left;
}
}
.instructions p {
margin-bottom: 5px;
}
form {
border-radius: 0;
box-shadow: none;
margin: 0;
border: none;
padding: 0;
.group-form {
margin: 0;
padding-top: 0;
padding-bottom: 20px;
}
.list-input {
margin: 0;
padding: 0;
list-style: none;
}
.readonly {
background-color: #eee !important;
color: #aaa;
}
.field {
margin: 0 0 20px 0;
}
.field.required label {
font-weight: 600;
}
.field label {
-webkit-transition: color 0.15s ease-in-out 0s;
-moz-transition: color 0.15s ease-in-out 0s;
transition: color 0.15s ease-in-out 0s;
margin: 0 0 5px 0;
color: #333;
}
.field.text input {
background: #fff;
margin-bottom: 0;
}
.field input {
width: 100%;
margin: 0;
padding: 10px 15px;
}
}
}
}
}
......@@ -12,14 +12,20 @@
border: 1px solid $red;
}
.cart-errors{
background: #FFEEF5;color:#B72667;text-align: center;padding: 10px 0px;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;font-size: 15px;
border-bottom: 1px solid #B72667;
margin-bottom: 20px;
display: none;
}
.cart-list {
padding: 30px;
margin-top: 40px;
border-radius: 3px;
border: 1px solid $border-color-1;
background-color: $action-primary-fg;
> h2 {
font-size: 1.5em;
color: $base-font-color;
......@@ -27,13 +33,42 @@
.cart-table {
width: 100%;
tr:nth-child(even){
background-color: #f8f8f8;
border-bottom: 1px solid #f3f3f3;
}
tr.always-gray{
background: #eee !important;
border-top: 2px solid #FFFFFF;
}
tr.always-white{
background: #fff !important;
td{
padding: 30px 0px 10px;
}
}
tr{
td.cart-total{
padding: 10px 0;
span{
display: inline-block;
margin-right: 15px;
margin-left: 15px;
font-weight: bold;
}
}
}
.cart-headings {
height: 35px;
border-bottom: 1px solid #BEBEBE;
th:nth-child(5),th:first-child{
text-align: center;
width: 120px;
}
th {
text-align: left;
padding-left: 5px;
border-bottom: 1px solid $border-color-1;
&.qty {
......@@ -48,12 +83,35 @@
&.cur {
width: 100px;
}
&.dsc{
width: 640px;
padding-right: 50px;
}
}
}
.cart-items {
td {
padding: 10px 25px;
padding: 10px 0px;
position: relative;
line-height: normal;
span.old-price{
left: -75px;
position: relative;
text-decoration: line-through;
color: red;
font-size: 12px;
top: -1px;
}
}
td:nth-child(5),td:first-child{
text-align: center;
}
td:nth-child(2){
line-height: 22px;
padding-right: 50px;
}
}
......@@ -64,6 +122,7 @@
font-weight: bold;
padding: 10px 25px;
}
}
}
}
......@@ -80,11 +139,10 @@
.items-ordered {
padding-top: 50px;
}
tr {
}
th {
text-align: left;
padding: 25px 0 15px 0;
......@@ -105,6 +163,9 @@
tr.order-item {
td {
padding-bottom: 10px;
span.old-price{
text-decoration: line-through !important;
}
}
}
}
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/>
<section id="add-coupon-modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Password Reset')}">
<div class="inner-wrapper">
<button class="close-modal">
<i class="icon-remove"></i>
<span class="sr">
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
${_('Close')}
</span>
</button>
<div id="coupon-content">
<header>
<h2>${_("Add Coupon")}</h2>
</header>
<div class="instructions">
<p>
${_("Please enter Coupon detail below")}</p>
</div>
<form id="add_coupon_form" action="${section_data['ajax_add_coupon']}" method="post" data-remote="true">
<div id="coupon_form_error" class="modal-form-error"></div>
<fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend>
<ol class="list-input">
<li class="field required text" id="add-coupon-modal-field-code">
<label for="coupon_code" class="required">${_("Code")}</label>
<input class="" id="coupon_code" type="text" name="code" maxlength="16" value="" placeholder="example: A123DS"
aria-required="true"/>
</li>
<li class="field required text" id="add-coupon-modal-field-discount">
<label for="coupon_discount" class="required text">${_("Percentage Discount")}</label>
<input class="field required" id="coupon_discount" type="text" name="discount" value="" maxlength="5"
aria-required="true"/>
</li>
<li class="field" id="add-coupon-modal-field-description">
<label for="coupon_description">${_("Description")}</label>
<textarea class="field" id="coupon_description" type="text" name="description" value=""
aria-describedby="pwd_reset_email-tip" aria-required="true"> </textarea>
</li>
<li class="field" id="add-coupon-modal-field-course_id">
<label for="coupon_course_id">${_("Course ID")}</label>
<input class="field readonly" id="coupon_course_id" type="text" name="course_id" value="${section_data['course_id']}"
readonly aria-required="true"/>
</li>
</ol>
</fieldset>
<div class="submit">
<input name="submit" type="submit" id="add_coupon_button" value="${_('Add Coupon')}"/>
</div>
</form>
</div>
</div>
</section>
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
<%include file="add_coupon_modal.html" args="section_data=section_data" />
<%include file="edit_coupon_modal.html" args="section_data=section_data" />
<div class="ecommerce-wrapper">
<h2>${_("Coupons List")}
%if section_data['total_amount'] is not None:
<span>${_("Total Amount: ")}<span>$${section_data['total_amount']}</span></span>
%endif
</h2>
<h3 class="coupon-errors" id="coupon-error"></h3>
<span class="tip">${_("Coupons Information")} <a id="add_coupon_link" href="#add-coupon-modal" rel="leanModal"
class="add blue-button">${_("+ Add Coupon")}</a></span>
</div>
<div class="wrapper-content wrapper">
<section class="content">
%if len(section_data['coupons']):
<table class="coupons-table">
<thead>
<tr class="coupons-headings">
<th class="c_code">${_("Code")}</th>
<th class="c_dsc">${_("Description")}</th>
<th class="c_course_id">${_("Course_id")}</th>
<th class="c_discount">${_("Discount(%)")}</th>
<th class="c_count">${_("Count")}</th>
<th class="c_action">${_("Actions")}</th>
</tr>
</thead>
<tbody>
%for coupon in section_data['coupons']:
%if coupon.is_active == False:
<tr class="coupons-items inactive_coupon">
%else:
<tr class="coupons-items">
%endif
<td>${coupon.code}</td>
<td>${coupon.description}</td>
<td>${coupon.course_id.to_deprecated_string()}</td>
<td>${coupon.percentage_discount}</td>
<td>
${ coupon.couponredemption_set.all().count() }
</td>
<!--<td>${coupon.is_active}</td>-->
<td><a data-item-id="${coupon.id}" class='remove_coupon' href='#'>[x]</a><a href="#edit-modal" data-item-id="${coupon.id}" class="edit-right">Edit</a></td>
</tr>
%endfor
</tbody>
</table>
<a id="edit-modal-trigger" href="#edit-coupon-modal" rel="leanModal"></a>
%endif
</section>
</div>
<script>
$(function () {
$('a[rel*=leanModal]').leanModal();
$.each($("a.edit-right"), function () {
if ($(this).parent().parent('tr').hasClass('inactive_coupon')) {
$(this).removeAttr('href')
}
});
$.each($("a.remove_coupon"), function () {
if ($(this).parent().parent('tr').hasClass('inactive_coupon')) {
$(this).removeAttr('href')
}
});
$('a.edit-right').click(function (event) {
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: none');
$('#edit_coupon_form #coupon_form_error').text();
event.preventDefault();
event.stopPropagation();
var coupon_id = $(this).data('item-id');
$('#coupon_id').val(coupon_id);
if ($(this).parent().parent('tr').hasClass('inactive_coupon')) {
return false;
}
$.ajax({
type: "POST",
data: {id: coupon_id},
url: "${section_data['ajax_get_coupon_info']}",
success: function (data) {
$('#coupon-error').val('');
$('#coupon-error').attr('style', 'display: none');
$('input#edit_coupon_code').val(data.coupon_code);
$('input#edit_coupon_discount').val(data.coupon_discount);
$('textarea#edit_coupon_description').val(data.coupon_description);
$('input#edit_coupon_course_id').val(data.coupon_course_id);
$('#edit-modal-trigger').click();
},
error: function(jqXHR, textStatus, errorThrown) {
var data = $.parseJSON(jqXHR.responseText);
$('#coupon-error').html(data.message).show();
}
});
});
$('a.remove_coupon').click(function (event) {
var anchor = $(this);
if (anchor.data("disabled")) {
return false;
}
anchor.data("disabled", "disabled");
event.preventDefault();
if ($(this).parent().parent('tr').hasClass('inactive_coupon')) {
return false;
}
$.ajax({
type: "POST",
data: {id: $(this).data('item-id')},
url: "${section_data['ajax_remove_coupon_url']}",
success: function (data) {
anchor.removeData("disabled");
location.reload(true);
},
error: function(jqXHR, textStatus, errorThrown) {
var data = $.parseJSON(jqXHR.responseText);
$('#coupon-error').html(data.message).show();
anchor.removeData("disabled");
}
});
});
$('#edit_coupon_form').submit(function () {
$("#update_coupon_button").attr('disabled', true);
// Get the Code and Discount value and trim it
var code = $.trim($('#edit_coupon_code').val());
var coupon_discount = $.trim($('#edit_coupon_discount').val());
// Check if empty of not
if (code === '') {
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#edit_coupon_form #coupon_form_error').text("${_('Please Enter the Coupon Code')}");
$("#update_coupon_button").removeAttr('disabled');
return false;
}
if (coupon_discount == '0') {
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#edit_coupon_form #coupon_form_error').text("${_('Please Enter the Value Greater than 0')}");
$("#update_coupon_button").removeAttr('disabled');
return false;
}
if (!$.isNumeric(coupon_discount)) {
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#edit_coupon_form #coupon_form_error').text("${_('Please Enter the Coupon Discount Value Greater than 0')}");
$("#update_coupon_button").removeAttr('disabled');
return false;
}
});
$('#add_coupon_link').click(function () {
reset_input_fields();
});
$('#add_coupon_form').submit(function () {
$("#add_coupon_button").attr('disabled', true);
// Get the Code and Discount value and trim it
var code = $.trim($('#coupon_code').val());
var coupon_discount = $.trim($('#coupon_discount').val());
// Check if empty of not
if (code === '') {
$("#add_coupon_button").removeAttr('disabled');
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text("${_('Please Enter the Coupon Code')}");
return false;
}
if (coupon_discount == '0') {
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text("${_('Please Enter the Coupon Discount Value Greater than 0')}");
$("#add_coupon_button").removeAttr('disabled');
return false;
}
if (!$.isNumeric(coupon_discount)) {
$("#add_coupon_button").removeAttr('disabled');
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text("${_('Please Enter the Numeric value for Discount')}");
return false;
}
});
$('#add_coupon_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true);
} else {
$("#add_coupon_button").removeAttr('disabled');
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text(xhr.responseText);
}
});
$('#edit_coupon_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true);
} else {
$("#update_coupon_button").removeAttr('disabled');
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#edit_coupon_form #coupon_form_error').text(xhr.responseText);
}
});
// removing close link's default behavior
$('.close-modal').click(function (e) {
$("#update_coupon_button").removeAttr('disabled');
$("#add_coupon_button").removeAttr('disabled');
reset_input_fields();
e.preventDefault();
});
var onModalClose = function () {
$("#add-coupon-modal").attr("aria-hidden", "true");
$(".remove_coupon").focus();
$("#edit-coupon-modal").attr("aria-hidden", "true");
$(".edit-right").focus();
$("#add_coupon_button").removeAttr('disabled');
$("#update_coupon_button").removeAttr('disabled');
reset_input_fields();
};
var cycle_modal_tab = function (from_element_name, to_element_name) {
$(from_element_name).on('keydown', function (e) {
var keyCode = e.keyCode || e.which;
var TAB_KEY = 9; // 9 corresponds to the tab key
if (keyCode === TAB_KEY) {
e.preventDefault();
$(to_element_name).focus();
}
});
};
$("#add-coupon-modal .close-modal").click(onModalClose);
$("#edit-coupon-modal .close-modal").click(onModalClose);
$("#add-coupon-modal .close-modal").click(reset_input_fields);
// Hitting the ESC key will exit the modal
$("#add-coupon-modal, #edit-coupon-modal").on("keydown", function (e) {
var keyCode = e.keyCode || e.which;
// 27 is the ESC key
if (keyCode === 27) {
e.preventDefault();
$("#add-coupon-modal .close-modal").click();
$("#edit-coupon-modal .close-modal").click();
}
});
});
var reset_input_fields = function () {
$('#coupon-error').val('');
$('#coupon-error').attr('style', 'display: none');
$('#add_coupon_form #coupon_form_error').attr('style', 'display: none');
$('#add_coupon_form #coupon_form_error').text();
$('input#coupon_code').val('');
$('input#coupon_discount').val('');
$('textarea#coupon_description').val('');
}
</script>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/>
<section id="edit-coupon-modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Edit Coupon')}">
<div class="inner-wrapper">
<button class="close-modal">
<i class="icon-remove"></i>
<span class="sr">
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
${_('Close')}
</span>
</button>
<div id="coupon-content">
<header>
<h2>${_("Update Coupon")}</h2>
</header>
<div class="instructions">
<p>
${_("Update Coupon Information")}</p>
</div>
<form id="edit_coupon_form" action="${section_data['ajax_update_coupon']}" method="post" data-remote="true">
<div id="coupon_form_error" class="modal-form-error"></div>
<fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend>
<ol class="list-input">
<li class="field required text" id="edit-coupon-modal-field-code">
<label for="edit_coupon_code" class="required">${_("Code")}</label>
<input class="field" id="edit_coupon_code" type="text" name="code" maxlength="16" value="" placeholder="example: A123DS"
aria-required="true"/>
</li>
<li class="field required text" id="edit-coupon-modal-field-discount">
<label for="edit_coupon_discount" class="required">${_("Percentage Discount")}</label>
<input class="field" id="edit_coupon_discount" type="text" name="discount" value="" maxlength="5"
aria-required="true"/>
</li>
<li class="field" id="edit-coupon-modal-field-description">
<label for="edit_coupon_description">${_("Description")}</label>
<textarea class="field" id="edit_coupon_description" type="text" name="description" value=""
aria-required="true"></textarea>
</li>
<li class="field" id="edit-coupon-modal-field-course_id">
<label for="edit_coupon_course_id">${_("Course ID")}</label>
<input class="field readonly" id="edit_coupon_course_id" type="text" name="course_id" value=""
readonly aria-required="true"/>
</li>
</ol>
</fieldset>
<div class="submit">
<input type="hidden" name="coupon_id" id="coupon_id"/>
<input name="submit" type="submit" id="update_coupon_button" value="${_('Update Coupon')}"/>
</div>
</form>
</div>
</div>
</section>
......@@ -2,5 +2,6 @@
% for pk, pv in params.iteritems():
<input type="hidden" name="${pk}" value="${pv}" />
% endfor
<input type="submit" value="Check Out" />
</form>
</form>
\ No newline at end of file
......@@ -8,6 +8,7 @@
<section class="container cart-list">
<h2>${_("Your selected items:")}</h2>
<h3 class="cart-errors" id="cart-error">Error goes here.</h3>
% if shoppingcart_items:
<table class="cart-table">
<thead>
......@@ -24,24 +25,39 @@
<tr class="cart-items">
<td>${item.qty}</td>
<td>${item.line_desc}</td>
<td>${"{0:0.2f}".format(item.unit_cost)}</td>
<td>
${"{0:0.2f}".format(item.unit_cost)}
% if item.list_price != None:
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
% endif
</td>
<td>${"{0:0.2f}".format(item.line_cost)}</td>
<td>${item.currency.upper()}</td>
<td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td>
</tr>
% endfor
<tr class="cart-headings">
<td colspan="4"></td>
<th>${_("Total Amount")}</th>
</tr>
<tr class="cart-totals">
<td colspan="4"></td>
<td class="cart-total-cost">${"{0:0.2f}".format(amount)}</td>
<tr class="always-gray">
<td colspan="3"></td>
<td colspan="3" valign="middle" class="cart-total" align="right">
<b>${_("Total Amount")}: <span> ${"{0:0.2f}".format(amount)} </span> </b>
</td>
</tr>
</tbody>
<tfoot>
<tr class="always-white">
<td colspan="2">
<input type="text" placeholder="Enter coupon code here" name="coupon_code" id="couponCode">
<input type="button" value="Use Coupon" id="cart-coupon">
</td>
<td colspan="4" align="right">
${form_html}
</td>
</tr>
</tfoot>
</table>
<!-- <input id="back_input" type="submit" value="Return" /> -->
${form_html}
% else:
<p>${_("You have selected no items for purchase.")}</p>
% endif
......@@ -60,9 +76,39 @@
});
});
$('#cart-coupon').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.use_coupon')}";
$.post(post_url,{
"coupon_code" : $('#couponCode').val(),
beforeSend: function(xhr, options){
if($('#couponCode').val() == "") {
showErrorMsgs('Must contain a valid coupon code')
xhr.abort();
}
}
}
)
.success(function(data) {
location.reload(true);
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText)
}
})
});
$('#back_input').click(function(){
history.back();
});
function showErrorMsgs(msg){
$(".cart-errors").css('display', 'block');
$("#cart-error").html(msg);
}
});
</script>
......@@ -3,6 +3,7 @@
<%! from django.conf import settings %>
<%inherit file="../main.html" />
<%block name="bodyclass">purchase-receipt</%block>
<%block name="pagetitle">${_("Register for [Course Name] | Receipt (Order")} ${order.id})</%block>
......@@ -37,16 +38,25 @@
<tr>
<th class="qty">${_("Qty")}</th>
<th class="desc">${_("Description")}</th>
<th class="url">${_("URL")}</th>
<th class="u-pr">${_("Unit Price")}</th>
<th class="pri">${_("Price")}</th>
<th class="curr">${_("Currency")}</th>
</tr>
% for item in order_items:
<% course_id = reverse('info', args=[item.course_id.to_deprecated_string()]) %>
<tr class="order-item">
% if item.status == "purchased":
<td>${item.qty}</td>
<td>${item.line_desc}</td>
<td>${"{0:0.2f}".format(item.unit_cost)}</td>
<td><a href="${course_id}" class="enter-course">${_('View Course')}</a></td>
<td>${"{0:0.2f}".format(item.unit_cost)}
% if item.list_price != None:
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
% endif
</td>
<td>${"{0:0.2f}".format(item.line_cost)}</td>
<td>${item.currency.upper()}</td></tr>
% elif item.status == "refunded":
......
......@@ -283,6 +283,14 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard"),
url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),
include('instructor.views.api_urls')),
url(r'^courses/{}/remove_coupon$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.coupons.remove_coupon', name="remove_coupon"),
url(r'^courses/{}/add_coupon$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.coupons.add_coupon', name="add_coupon"),
url(r'^courses/{}/update_coupon$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.coupons.update_coupon', name="update_coupon"),
url(r'^courses/{}/get_coupon_info$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.coupons.get_coupon_info', name="get_coupon_info"),
# see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls
......
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