Merge pull request #4172 from edx/cdodge/ecommerce-improvements
eCommerce enhancements
This commit is contained in:
2
AUTHORS
2
AUTHORS
@@ -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)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- 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']
|
||||
@@ -0,0 +1,180 @@
|
||||
# -*- 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']
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- 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']
|
||||
@@ -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'
|
||||
|
||||
51
common/djangoapps/student/tests/test_microsite.py
Normal file
51
common/djangoapps/student/tests/test_microsite.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
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
|
||||
|
||||
193
lms/djangoapps/instructor/tests/test_ecommerce.py
Normal file
193
lms/djangoapps/instructor/tests/test_ecommerce.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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)
|
||||
135
lms/djangoapps/instructor/views/coupons.py
Normal file
135
lms/djangoapps/instructor/views/coupons.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# -*- 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']
|
||||
@@ -0,0 +1,216 @@
|
||||
# -*- 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']
|
||||
@@ -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
|
||||
@@ -319,6 +392,19 @@ class PaidCourseRegistration(OrderItem):
|
||||
return course_id in [item.paidcourseregistration.course_id
|
||||
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(_(
|
||||
"""
|
||||
|
||||
404
lms/djangoapps/shoppingcart/processors/CyberSource2.py
Normal file
404
lms/djangoapps/shoppingcart/processors/CyberSource2.py
Normal file
@@ -0,0 +1,404 @@
|
||||
### 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<%! 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>
|
||||
256
lms/templates/instructor/instructor_dashboard_2/e-commerce.html
Normal file
256
lms/templates/instructor/instructor_dashboard_2/e-commerce.html
Normal file
@@ -0,0 +1,256 @@
|
||||
<%! 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>
|
||||
@@ -0,0 +1,63 @@
|
||||
<%! 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>
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user