Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
7593ad3a
Commit
7593ad3a
authored
Sep 16, 2014
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5050 from edx/muhhshoaib/Ex-74-registration-code-redemption
Ex-74 Registration Code redemption
parents
5b61a534
739d2f09
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
592 additions
and
3 deletions
+592
-3
lms/djangoapps/shoppingcart/migrations/0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco.py
+189
-0
lms/djangoapps/shoppingcart/models.py
+11
-1
lms/djangoapps/shoppingcart/tests/test_views.py
+125
-1
lms/djangoapps/shoppingcart/urls.py
+1
-0
lms/djangoapps/shoppingcart/views.py
+90
-1
lms/static/sass/views/_shoppingcart.scss
+95
-0
lms/templates/shoppingcart/registration_code_receipt.html
+81
-0
No files found.
lms/djangoapps/shoppingcart/migrations/0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco.py
0 → 100644
View file @
7593ad3a
# -*- coding: utf-8 -*-
import
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding field 'CourseRegistrationCode.order'
db
.
add_column
(
'shoppingcart_courseregistrationcode'
,
'order'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
'purchase_order'
,
null
=
True
,
to
=
orm
[
'shoppingcart.Order'
]),
keep_default
=
False
)
# Changing field 'RegistrationCodeRedemption.order'
db
.
alter_column
(
'shoppingcart_registrationcoderedemption'
,
'order_id'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'shoppingcart.Order'
],
null
=
True
))
def
backwards
(
self
,
orm
):
# Deleting field 'CourseRegistrationCode.order'
db
.
delete_column
(
'shoppingcart_courseregistrationcode'
,
'order_id'
)
# Changing field 'RegistrationCodeRedemption.order'
db
.
alter_column
(
'shoppingcart_registrationcoderedemption'
,
'order_id'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
default
=
''
,
to
=
orm
[
'shoppingcart.Order'
]))
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, 9, 3, 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'
,
[],
{
'unique'
:
'True'
,
'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, 9, 3, 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'
}),
'invoice'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['shoppingcart.Invoice']"
,
'null'
:
'True'
}),
'order'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'purchase_order'"
,
'null'
:
'True'
,
'to'
:
"orm['shoppingcart.Order']"
})
},
'shoppingcart.invoice'
:
{
'Meta'
:
{
'object_name'
:
'Invoice'
},
'address_line_1'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'address_line_2'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'address_line_3'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'city'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'company_contact_email'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'company_contact_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'company_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'country'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'64'
,
'null'
:
'True'
}),
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'customer_reference_number'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'63'
,
'null'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'internal_reference'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'is_valid'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'recipient_email'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'recipient_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'state'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
}),
'total_amount'
:
(
'django.db.models.fields.FloatField'
,
[],
{}),
'zip'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'15'
,
'null'
:
'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'
})
},
'shoppingcart.registrationcoderedemption'
:
{
'Meta'
:
{
'object_name'
:
'RegistrationCodeRedemption'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'order'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['shoppingcart.Order']"
,
'null'
:
'True'
}),
'redeemed_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime(2014, 9, 3, 0, 0)'
,
'null'
:
'True'
}),
'redeemed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
}),
'registration_code'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['shoppingcart.CourseRegistrationCode']"
})
},
'student.courseenrollment'
:
{
'Meta'
:
{
'ordering'
:
"('user', 'course_id')"
,
'unique_together'
:
"(('user', 'course_id'),)"
,
'object_name'
:
'CourseEnrollment'
},
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'null'
:
'True'
,
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'mode'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'honor'"
,
'max_length'
:
'100'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
}
}
complete_apps
=
[
'shoppingcart'
]
\ No newline at end of file
lms/djangoapps/shoppingcart/models.py
View file @
7593ad3a
...
...
@@ -384,6 +384,7 @@ class CourseRegistrationCode(models.Model):
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
created_by
=
models
.
ForeignKey
(
User
,
related_name
=
'created_by_user'
)
created_at
=
models
.
DateTimeField
(
default
=
datetime
.
now
(
pytz
.
utc
))
order
=
models
.
ForeignKey
(
Order
,
db_index
=
True
,
null
=
True
,
related_name
=
"purchase_order"
)
invoice
=
models
.
ForeignKey
(
Invoice
,
null
=
True
)
@classmethod
...
...
@@ -410,7 +411,7 @@ class RegistrationCodeRedemption(models.Model):
"""
This model contains the registration-code redemption info
"""
order
=
models
.
ForeignKey
(
Order
,
db_index
=
True
)
order
=
models
.
ForeignKey
(
Order
,
db_index
=
True
,
null
=
True
)
registration_code
=
models
.
ForeignKey
(
CourseRegistrationCode
,
db_index
=
True
)
redeemed_by
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
redeemed_at
=
models
.
DateTimeField
(
default
=
datetime
.
now
(
pytz
.
utc
),
null
=
True
)
...
...
@@ -444,6 +445,15 @@ class RegistrationCodeRedemption(models.Model):
log
.
warning
(
"Course item does not exist against registration code '{0}'"
.
format
(
course_reg_code
.
code
))
raise
ItemDoesNotExistAgainstRegCodeException
@classmethod
def
create_invoice_generated_registration_redemption
(
cls
,
course_reg_code
,
user
):
"""
This function creates a RegistrationCodeRedemption entry in case the registration codes were invoice generated
and thus the order_id is missing.
"""
code_redemption
=
RegistrationCodeRedemption
(
registration_code
=
course_reg_code
,
redeemed_by
=
user
)
code_redemption
.
save
()
class
SoftDeleteCouponManager
(
models
.
Manager
):
""" Use this manager to get objects that have a is_active=True """
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
7593ad3a
...
...
@@ -13,12 +13,18 @@ from django.contrib.admin.sites import AdminSite
from
django.contrib.auth.models
import
Group
,
User
from
django.contrib.messages.storage.fallback
import
FallbackStorage
from
django.core.cache
import
cache
from
pytz
import
UTC
from
freezegun
import
freeze_time
from
datetime
import
datetime
,
timedelta
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
shoppingcart.views
import
_can_download_report
,
_get_date_from_str
from
shoppingcart.models
import
Order
,
CertificateItem
,
PaidCourseRegistration
,
Coupon
,
CourseRegistrationCode
from
shoppingcart.models
import
Order
,
CertificateItem
,
PaidCourseRegistration
,
Coupon
,
CourseRegistrationCode
,
RegistrationCodeRedemption
from
student.tests.factories
import
UserFactory
,
AdminFactory
from
courseware.tests.factories
import
InstructorFactory
from
student.models
import
CourseEnrollment
from
course_modes.models
import
CourseMode
from
edxmako.shortcuts
import
render_to_response
...
...
@@ -734,6 +740,124 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
RegistrationCodeRedemptionCourseEnrollment
(
ModuleStoreTestCase
):
"""
Test suite for RegistrationCodeRedemption Course Enrollments
"""
def
setUp
(
self
,
**
kwargs
):
self
.
user
=
UserFactory
.
create
()
self
.
user
.
set_password
(
'password'
)
self
.
user
.
save
()
self
.
cost
=
40
self
.
course
=
CourseFactory
.
create
(
org
=
'MITx'
,
number
=
'999'
,
display_name
=
'Robot Super Course'
)
self
.
course_key
=
self
.
course
.
id
self
.
course_mode
=
CourseMode
(
course_id
=
self
.
course_key
,
mode_slug
=
"honor"
,
mode_display_name
=
"honor cert"
,
min_price
=
self
.
cost
)
self
.
course_mode
.
save
()
def
login_user
(
self
):
"""
Helper fn to login self.user
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
"password"
)
def
test_registration_redemption_post_request_ratelimited
(
self
):
"""
Try (and fail) registration code redemption 30 times
in a row on an non-existing registration code post request
"""
cache
.
clear
()
url
=
reverse
(
'register_code_redemption'
,
args
=
[
'asdasd'
])
self
.
login_user
()
for
i
in
xrange
(
30
):
# pylint: disable=W0612
response
=
self
.
client
.
post
(
url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
404
)
# then the rate limiter should kick in and give a HttpForbidden response
response
=
self
.
client
.
post
(
url
)
self
.
assertEquals
(
response
.
status_code
,
403
)
# now reset the time to 5 mins from now in future in order to unblock
reset_time
=
datetime
.
now
(
UTC
)
+
timedelta
(
seconds
=
300
)
with
freeze_time
(
reset_time
):
response
=
self
.
client
.
post
(
url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
404
)
cache
.
clear
()
def
test_registration_redemption_get_request_ratelimited
(
self
):
"""
Try (and fail) registration code redemption 30 times
in a row on an non-existing registration code get request
"""
cache
.
clear
()
url
=
reverse
(
'register_code_redemption'
,
args
=
[
'asdasd'
])
self
.
login_user
()
for
i
in
xrange
(
30
):
# pylint: disable=W0612
response
=
self
.
client
.
get
(
url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
404
)
# then the rate limiter should kick in and give a HttpForbidden response
response
=
self
.
client
.
get
(
url
)
self
.
assertEquals
(
response
.
status_code
,
403
)
# now reset the time to 5 mins from now in future in order to unblock
reset_time
=
datetime
.
now
(
UTC
)
+
timedelta
(
seconds
=
300
)
with
freeze_time
(
reset_time
):
response
=
self
.
client
.
get
(
url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
404
)
cache
.
clear
()
def
test_course_enrollment_active_registration_code_redemption
(
self
):
"""
Test for active registration code course enrollment
"""
cache
.
clear
()
instructor
=
InstructorFactory
(
course_key
=
self
.
course_key
)
self
.
client
.
login
(
username
=
instructor
.
username
,
password
=
'test'
)
url
=
reverse
(
'generate_registration_codes'
,
kwargs
=
{
'course_id'
:
self
.
course
.
id
.
to_deprecated_string
()})
data
=
{
'total_registration_codes'
:
12
,
'company_name'
:
'Test Group'
,
'company_contact_name'
:
'Test@company.com'
,
'company_contact_email'
:
'Test@company.com'
,
'sale_price'
:
122.45
,
'recipient_name'
:
'Test123'
,
'recipient_email'
:
'test@123.com'
,
'address_line_1'
:
'Portland Street'
,
'address_line_2'
:
''
,
'address_line_3'
:
''
,
'city'
:
''
,
'state'
:
''
,
'zip'
:
''
,
'country'
:
''
,
'customer_reference_number'
:
'123A23F'
,
'internal_reference'
:
''
,
'invoice'
:
''
}
response
=
self
.
client
.
post
(
url
,
data
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
200
)
# get the first registration from the newly created registration codes
registration_code
=
CourseRegistrationCode
.
objects
.
all
()[
0
]
.
code
redeem_url
=
reverse
(
'register_code_redemption'
,
args
=
[
registration_code
])
self
.
login_user
()
response
=
self
.
client
.
get
(
redeem_url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
200
)
# check button text
self
.
assertTrue
(
'Activate Course Enrollment'
in
response
.
content
)
#now activate the user by enrolling him/her to the course
response
=
self
.
client
.
post
(
redeem_url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertTrue
(
'View Course'
in
response
.
content
)
#now check that the registration code has already been redeemed and user is already registered in the course
RegistrationCodeRedemption
.
objects
.
filter
(
registration_code__code
=
registration_code
)
response
=
self
.
client
.
get
(
redeem_url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertEquals
(
len
(
RegistrationCodeRedemption
.
objects
.
filter
(
registration_code__code
=
registration_code
)),
1
)
self
.
assertTrue
(
"You've clicked a link for an enrollment code that has already been used."
in
response
.
content
)
#now check that the registration code has already been redeemed
response
=
self
.
client
.
post
(
redeem_url
,
**
{
'HTTP_HOST'
:
'localhost'
})
self
.
assertTrue
(
"You've clicked a link for an enrollment code that has already been used."
in
response
.
content
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
CSVReportViewsTest
(
ModuleStoreTestCase
):
"""
Test suite for CSV Purchase Reporting
...
...
lms/djangoapps/shoppingcart/urls.py
View file @
7593ad3a
...
...
@@ -14,6 +14,7 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']:
url
(
r'^clear/$'
,
'clear_cart'
),
url
(
r'^remove_item/$'
,
'remove_item'
),
url
(
r'^add/course/{}/$'
.
format
(
settings
.
COURSE_ID_PATTERN
),
'add_course_to_cart'
,
name
=
'add_course_to_cart'
),
url
(
r'^register/redeem/(?P<registration_code>[0-9A-Za-z]+)/$'
,
'register_code_redemption'
,
name
=
'register_code_redemption'
),
url
(
r'^use_code/$'
,
'use_code'
),
url
(
r'^register_courses/$'
,
'register_courses'
),
)
...
...
lms/djangoapps/shoppingcart/views.py
View file @
7593ad3a
...
...
@@ -6,12 +6,16 @@ from django.contrib.auth.models import Group
from
django.http
import
(
HttpResponse
,
HttpResponseRedirect
,
HttpResponseNotFound
,
HttpResponseBadRequest
,
HttpResponseForbidden
,
Http404
)
from
django.utils.translation
import
ugettext
as
_
from
django.views.decorators.http
import
require_POST
from
django.views.decorators.http
import
require_POST
,
require_http_methods
from
django.core.urlresolvers
import
reverse
from
django.views.decorators.csrf
import
csrf_exempt
from
microsite_configuration
import
microsite
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
from
django.contrib.auth.decorators
import
login_required
from
edxmako.shortcuts
import
render_to_response
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
courseware.courses
import
get_course_by_id
from
courseware.views
import
registered_for_course
from
shoppingcart.reports
import
RefundReport
,
ItemizedPurchaseReport
,
UniversityRevenueShareReport
,
CertificateStatusReport
from
student.models
import
CourseEnrollment
from
.exceptions
import
ItemAlreadyInCartException
,
AlreadyEnrolledInCourseException
,
CourseDoesNotExistException
,
ReportTypeDoesNotExistException
,
\
...
...
@@ -22,6 +26,7 @@ from .processors import process_postpay_callback, render_purchase_form_html
import
json
log
=
logging
.
getLogger
(
"shoppingcart"
)
AUDIT_LOG
=
logging
.
getLogger
(
"audit"
)
EVENT_NAME_USER_UPGRADED
=
'edx.course.enrollment.upgrade.succeeded'
...
...
@@ -168,6 +173,90 @@ def use_code(request):
return
use_coupon_code
(
coupons
,
request
.
user
)
def
get_reg_code_validity
(
registration_code
,
request
,
limiter
):
"""
This function checks if the registration code is valid, and then checks if it was already redeemed.
"""
reg_code_already_redeemed
=
False
course_registration
=
None
try
:
course_registration
=
CourseRegistrationCode
.
objects
.
get
(
code
=
registration_code
)
except
CourseRegistrationCode
.
DoesNotExist
:
reg_code_is_valid
=
False
else
:
reg_code_is_valid
=
True
try
:
RegistrationCodeRedemption
.
objects
.
get
(
registration_code__code
=
registration_code
)
except
RegistrationCodeRedemption
.
DoesNotExist
:
reg_code_already_redeemed
=
False
else
:
reg_code_already_redeemed
=
True
if
not
reg_code_is_valid
:
#tick the rate limiter counter
AUDIT_LOG
.
info
(
"Redemption of a non existing RegistrationCode {code}"
.
format
(
code
=
registration_code
))
limiter
.
tick_bad_request_counter
(
request
)
raise
Http404
()
return
reg_code_is_valid
,
reg_code_already_redeemed
,
course_registration
@require_http_methods
([
"GET"
,
"POST"
])
@login_required
def
register_code_redemption
(
request
,
registration_code
):
"""
This view allows the student to redeem the registration code
and enroll in the course.
"""
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
site_name
=
microsite
.
get_value
(
'SITE_NAME'
,
settings
.
SITE_NAME
)
limiter
=
BadRequestRateLimiter
()
if
limiter
.
is_rate_limit_exceeded
(
request
):
AUDIT_LOG
.
warning
(
"Rate limit exceeded in registration code redemption."
)
return
HttpResponseForbidden
()
template_to_render
=
'shoppingcart/registration_code_receipt.html'
if
request
.
method
==
"GET"
:
reg_code_is_valid
,
reg_code_already_redeemed
,
course_registration
=
get_reg_code_validity
(
registration_code
,
request
,
limiter
)
course
=
get_course_by_id
(
getattr
(
course_registration
,
'course_id'
),
depth
=
0
)
context
=
{
'reg_code_already_redeemed'
:
reg_code_already_redeemed
,
'reg_code_is_valid'
:
reg_code_is_valid
,
'reg_code'
:
registration_code
,
'site_name'
:
site_name
,
'course'
:
course
,
'registered_for_course'
:
registered_for_course
(
course
,
request
.
user
)
}
return
render_to_response
(
template_to_render
,
context
)
elif
request
.
method
==
"POST"
:
reg_code_is_valid
,
reg_code_already_redeemed
,
course_registration
=
get_reg_code_validity
(
registration_code
,
request
,
limiter
)
course
=
get_course_by_id
(
getattr
(
course_registration
,
'course_id'
),
depth
=
0
)
if
reg_code_is_valid
and
not
reg_code_already_redeemed
:
#now redeem the reg code.
RegistrationCodeRedemption
.
create_invoice_generated_registration_redemption
(
course_registration
,
request
.
user
)
CourseEnrollment
.
enroll
(
request
.
user
,
course
.
id
)
context
=
{
'redemption_success'
:
True
,
'reg_code'
:
registration_code
,
'site_name'
:
site_name
,
'course'
:
course
,
}
else
:
context
=
{
'reg_code_is_valid'
:
reg_code_is_valid
,
'reg_code_already_redeemed'
:
reg_code_already_redeemed
,
'redemption_success'
:
False
,
'reg_code'
:
registration_code
,
'site_name'
:
site_name
,
'course'
:
course
,
}
return
render_to_response
(
template_to_render
,
context
)
def
use_registration_code
(
course_reg
,
user
):
"""
This method utilize course registration code
...
...
lms/static/sass/views/_shoppingcart.scss
View file @
7593ad3a
...
...
@@ -171,3 +171,97 @@
}
}
}
.confirm-enrollment
{
.title
{
font-size
:
24px
;
border-bottom
:
1px
solid
#f2f2f2
;
text-align
:
left
;
line-height
:
70px
;
}
.course-image
{
display
:
inline-block
;
width
:
223px
;
margin-right
:
10px
;
vertical-align
:
top
;
}
.enrollment-details
{
margin-bottom
:
20px
;
display
:
inline-block
;
width
:
calc
(
100%
-
237px
);
.sub-title
{
font-size
:
18px
;
text-transform
:
uppercase
;
color
:
#9b9b93
;
}
.course-date-label
{
float
:
right
;
color
:
#9b9b93
;
}
.course-dates
{
float
:
right
;
font-size
:
18px
;
}
.course-title
{
h1
{
color
:
#4a4a46
;
font-size
:
26px
;
text-align
:
left
;
font-weight
:
600
;
}
}
.enrollment-text
{
color
:
#4A4A46
;
font-family
:
'Open Sans'
,
Verdana
,
Geneva
,
sans
;
line-height
:
normal
;
a
{
font-family
:
'Open Sans'
,
Verdana
,
Geneva
,
sans
;
}
}
}
a
.contact-support-bg-color
{
background-color
:
#9b9b9b
;
background-image
:
linear-gradient
(
#9b9b9b
,
#9b9b9b
);
border
:
16px
solid
#9b9b9b
;
box-shadow
:
0
1px
0
0
#9b9b9b
inset
;
text-shadow
:
0
1px
0
#9b9b9b
;
}
a
.course-link-bg-color
{
background-color
:
#00A1E5
;
background-image
:
linear-gradient
(
#00A1E5
,
#00A1E5
);
border
:
16px
solid
#00A1E5
;
box-shadow
:
0
1px
0
0
#00A1E5
inset
;
text-shadow
:
0
1px
0
#00A1E5
;
}
a
.link-button
{
text-transform
:
none
;
width
:
250px
;
background-clip
:
padding-box
;
float
:
right
;
border-radius
:
3px
;
color
:
#FFFFFF
;
display
:
inline-block
;
padding
:
6px
18px
;
text-decoration
:
none
;
font-size
:
24px
;
text-align
:
center
;
}
input
[
type
=
"submit"
]
{
text-transform
:
none
;
width
:
450px
;
height
:
70px
;
background-clip
:
padding-box
;
background-color
:
#00a1e5
;
background-image
:
linear-gradient
(
#00A1E5
,
#00A1E5
);
float
:
right
;
border
:
1px
solid
#00A1E5
;
border-radius
:
3px
;
box-shadow
:
0
1px
0
0
#00A1E5
inset
;
color
:
#FFFFFF
;
display
:
inline-block
;
padding
:
7px
18px
;
text-decoration
:
none
;
text-shadow
:
0
1px
0
#00A1E5
;
font-size
:
24px
;
}
}
\ No newline at end of file
lms/templates/shoppingcart/registration_code_receipt.html
0 → 100644
View file @
7593ad3a
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
from
courseware
.
courses
import
course_image_url
,
get_course_about_section
%
>
<
%
inherit
file=
"../main.html"
/>
<
%
namespace
name=
'static'
file=
'/static_content.html'
/>
<
%
block
name=
"pagetitle"
>
${_("Confirm Enrollment")}
</
%
block>
<
%
block
name=
"content"
>
<div
class=
"container"
>
<section
class=
"wrapper confirm-enrollment"
>
<header
class=
"page-header"
>
<h1
class=
"title"
>
${_("{site_name} - Confirm Enrollment").format(site_name=site_name)}
</h1>
</header>
<section>
<div
class=
"course-image"
>
<img
style=
"width: 100%; height: auto;"
src=
"${course_image_url(course)}"
alt=
"${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Cover Image"
/>
</div>
<div
class=
"enrollment-details"
>
<div
class=
"sub-title"
>
${_("Confirm your enrollment for:")}
<span
class=
"course-date-label"
>
${_("course dates")}
</span>
<div
class=
"clearfix"
></div>
</div>
<div
class=
"course-title"
>
<h1>
${_("{course_name}").format(course_name=course.display_name)}
<span
class=
"course-dates"
>
${_("{start_date}").format(start_date=course.start_date_text)} - ${_("{end_date}").format(end_date=course.end_date_text)}
</span>
</h1>
</div>
<hr>
<div>
% if reg_code_already_redeemed:
<
%
dashboard_url =
reverse('dashboard')%
>
<p
class=
"enrollment-text"
>
${_("You've clicked a link for an enrollment code that has already been used."
" Check your
<a
href=
{dashboard_url}
>
course dashboard
</a>
to see if you're enrolled in the course,"
" or contact your company's administrator.").format(dashboard_url=dashboard_url)}
</p>
% elif redemption_success:
<p
class=
"enrollment-text"
>
${_("You have successfully enrolled in {course_name}."
" This course has now been added to your dashboard.").format(course_name=course.display_name)}
</p>
% elif registered_for_course:
<
%
dashboard_url =
reverse('dashboard')%
>
<p
class=
"enrollment-text"
>
${_("You're already registered for this course."
" Visit your
<a
href=
{dashboard_url}
>
dashboard
</a>
to see the course.").format(dashboard_url=dashboard_url)}
</p>
% else:
<p
class=
"enrollment-text"
>
${_("You're about to activate an enrollment code for {course_name} by {site_name}. "
"This code can only be used one time, so you should only activate this code if you're its intended"
" recipient.").format(course_name=course.display_name, site_name=site_name)}
</p>
% endif
</div>
</div>
% if not reg_code_already_redeemed:
%if redemption_success:
<
%
course_url =
reverse('info',
args=
[course.id.to_deprecated_string()])
%
>
<a
href=
"${course_url}"
class=
"link-button course-link-bg-color"
>
${_("View Course
▸
")}
</a>
%elif not registered_for_course:
<form
method=
"post"
>
<input
type=
"hidden"
name=
"csrfmiddlewaretoken"
value=
"${ csrf_token }"
>
<input
type=
"submit"
value=
"Activate Course Enrollment ▸"
id=
"id_active_course_enrollment"
name=
"active_course_enrollment"
>
</form>
%endif
%endif
</section>
</section>
</div>
</
%
block>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment