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
36d1eb22
Commit
36d1eb22
authored
Jun 09, 2015
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8338 from edx/will/credit-provider-integration
Credit provider integration Python API (Part 1 of 3)
parents
242b3736
e5a62aaa
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
867 additions
and
116 deletions
+867
-116
cms/envs/common.py
+3
-0
lms/envs/common.py
+3
-0
openedx/core/djangoapps/credit/api.py
+220
-7
openedx/core/djangoapps/credit/exceptions.py
+31
-5
openedx/core/djangoapps/credit/migrations/0003_add_creditrequirementstatus_reason.py
+0
-0
openedx/core/djangoapps/credit/migrations/0006_auto__add_creditrequest__add_unique_creditrequest_username_course_prov.py
+190
-0
openedx/core/djangoapps/credit/models.py
+125
-29
openedx/core/djangoapps/credit/tests/test_api.py
+289
-71
openedx/core/djangoapps/credit/tests/test_models.py
+5
-4
requirements/edx/base.txt
+1
-0
No files found.
cms/envs/common.py
View file @
36d1eb22
...
...
@@ -680,6 +680,9 @@ INSTALLED_APPS = (
'south'
,
'method_override'
,
# History tables
'simple_history'
,
# Database-backed configuration
'config_models'
,
...
...
lms/envs/common.py
View file @
36d1eb22
...
...
@@ -1711,6 +1711,9 @@ INSTALLED_APPS = (
'djcelery'
,
'south'
,
# History tables
'simple_history'
,
# Database-backed configuration
'config_models'
,
...
...
openedx/core/djangoapps/credit/api.py
View file @
36d1eb22
""" Contains the APIs for course credit requirements """
import
logging
import
uuid
from
django.db
import
transaction
from
.exceptions
import
InvalidCreditRequirements
from
.models
import
CreditCourse
,
CreditRequirement
from
openedx.core.djangoapps.credit.exceptions
import
InvalidCreditCourse
from
student.models
import
User
from
.exceptions
import
(
InvalidCreditRequirements
,
InvalidCreditCourse
,
UserIsNotEligible
,
RequestAlreadyCompleted
,
CreditRequestNotFound
,
InvalidCreditStatus
,
)
from
.models
import
(
CreditCourse
,
CreditRequirement
,
CreditRequirementStatus
,
CreditRequest
,
CreditEligibility
,
)
log
=
logging
.
getLogger
(
__name__
)
def
set_credit_requirements
(
course_key
,
requirements
):
"""Add requirements to given course.
"""
Add requirements to given course.
Args:
course_key(CourseKey): The identifier for course
...
...
@@ -63,7 +84,8 @@ def set_credit_requirements(course_key, requirements):
def
get_credit_requirements
(
course_key
,
namespace
=
None
):
"""Get credit eligibility requirements of a given course and namespace.
"""
Get credit eligibility requirements of a given course and namespace.
Args:
course_key(CourseKey): The identifier for course
...
...
@@ -111,8 +133,198 @@ def get_credit_requirements(course_key, namespace=None):
]
@transaction.commit_on_success
def
create_credit_request
(
course_key
,
provider_id
,
username
):
"""
Initiate a request for credit from a credit provider.
This will return the parameters that the user's browser will need to POST
to the credit provider. It does NOT calculate the signature.
Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests.
A database record will be created to track the request with a 32-character UUID.
The returned dictionary can be used by the user's browser to send a POST request to the credit provider.
If a pending request already exists, this function should return a request description with the same UUID.
(Other parameters, such as the user's full name may be different than the original request).
If a completed request (either accepted or rejected) already exists, this function will
raise an exception. Users are not allowed to make additional requests once a request
has been completed.
Arguments:
course_key (CourseKey): The identifier for the course.
provider_id (str): The identifier of the credit provider.
user (User): The user initiating the request.
Returns: dict
Raises:
UserIsNotEligible: The user has not satisfied eligibility requirements for credit.
RequestAlreadyCompleted: The user has already submitted a request and received a response
from the credit provider.
Example Usage:
>>> create_credit_request(course.id, "hogwarts", "ron")
{
"uuid": "557168d0f7664fe59097106c67c3f847",
"timestamp": "2015-05-04T20:57:57.987119+00:00",
"course_org": "HogwartsX",
"course_num": "Potions101",
"course_run": "1T2015",
"final_grade": 0.95,
"user_username": "ron",
"user_email": "ron@example.com",
"user_full_name": "Ron Weasley",
"user_mailing_address": "",
"user_country": "US",
}
"""
try
:
user_eligibility
=
CreditEligibility
.
objects
.
select_related
(
'course'
,
'provider'
)
.
get
(
username
=
username
,
course__course_key
=
course_key
,
provider__provider_id
=
provider_id
)
credit_course
=
user_eligibility
.
course
credit_provider
=
user_eligibility
.
provider
except
CreditEligibility
.
DoesNotExist
:
raise
UserIsNotEligible
# Initiate a new request if one has not already been created
credit_request
,
created
=
CreditRequest
.
objects
.
get_or_create
(
course
=
credit_course
,
provider
=
credit_provider
,
username
=
username
,
)
# Check whether we've already gotten a response for a request,
# If so, we're not allowed to issue any further requests.
# Skip checking the status if we know that we just created this record.
if
not
created
and
credit_request
.
status
!=
"pending"
:
raise
RequestAlreadyCompleted
if
created
:
credit_request
.
uuid
=
uuid
.
uuid4
()
.
hex
# Retrieve user account and profile info
user
=
User
.
objects
.
select_related
(
'profile'
)
.
get
(
username
=
username
)
# Retrieve the final grade from the eligibility table
try
:
final_grade
=
CreditRequirementStatus
.
objects
.
filter
(
username
=
username
,
requirement__namespace
=
"grade"
,
requirement__name
=
"grade"
,
status
=
"satisfied"
)
.
latest
()
.
reason
[
"final_grade"
]
except
(
CreditRequirementStatus
.
DoesNotExist
,
TypeError
,
KeyError
):
log
.
exception
(
"Could not retrieve final grade from the credit eligibility table "
"for user
%
s in course
%
s."
,
user
.
id
,
course_key
)
raise
UserIsNotEligible
parameters
=
{
"uuid"
:
credit_request
.
uuid
,
"timestamp"
:
credit_request
.
timestamp
.
isoformat
(),
"course_org"
:
course_key
.
org
,
"course_num"
:
course_key
.
course
,
"course_run"
:
course_key
.
run
,
"final_grade"
:
final_grade
,
"user_username"
:
user
.
username
,
"user_email"
:
user
.
email
,
"user_full_name"
:
user
.
profile
.
name
,
"user_mailing_address"
:
(
user
.
profile
.
mailing_address
if
user
.
profile
.
mailing_address
is
not
None
else
""
),
"user_country"
:
(
user
.
profile
.
country
.
code
if
user
.
profile
.
country
.
code
is
not
None
else
""
),
}
credit_request
.
parameters
=
parameters
credit_request
.
save
()
return
parameters
def
update_credit_request_status
(
request_uuid
,
status
):
"""
Update the status of a credit request.
Approve or reject a request for a student to receive credit in a course
from a particular credit provider.
This function does NOT check that the status update is authorized.
The caller needs to handle authentication and authorization (checking the signature
of the message received from the credit provider)
The function is idempotent; if the request has already been updated to the status,
the function does nothing.
Arguments:
request_uuid (str): The unique identifier for the credit request.
status (str): Either "approved" or "rejected"
Returns: None
Raises:
CreditRequestNotFound: The request does not exist.
InvalidCreditStatus: The status is not either "approved" or "rejected".
"""
if
status
not
in
[
"approved"
,
"rejected"
]:
raise
InvalidCreditStatus
try
:
request
=
CreditRequest
.
objects
.
get
(
uuid
=
request_uuid
)
request
.
status
=
status
request
.
save
()
except
CreditRequest
.
DoesNotExist
:
raise
CreditRequestNotFound
def
get_credit_requests_for_user
(
username
):
"""
Retrieve the status of a credit request.
Returns either "pending", "accepted", or "rejected"
Arguments:
username (unicode): The username of the user who initiated the requests.
Returns: list
Example Usage:
>>> get_credit_request_status_for_user("bob")
[
{
"uuid": "557168d0f7664fe59097106c67c3f847",
"timestamp": "2015-05-04T20:57:57.987119+00:00",
"course_key": "course-v1:HogwartsX+Potions101+1T2015",
"provider": {
"id": "HogwartsX",
"display_name": "Hogwarts School of Witchcraft and Wizardry",
},
"status": "pending" # or "approved" or "rejected"
}
]
"""
return
CreditRequest
.
credit_requests_for_user
(
username
)
def
_get_requirements_to_disable
(
old_requirements
,
new_requirements
):
"""Get the ids of 'CreditRequirement' entries to be disabled that are
"""
Get the ids of 'CreditRequirement' entries to be disabled that are
deleted from the courseware.
Args:
...
...
@@ -136,7 +348,8 @@ def _get_requirements_to_disable(old_requirements, new_requirements):
def
_validate_requirements
(
requirements
):
"""Validate the requirements.
"""
Validate the requirements.
Args:
requirements(list): List of requirements
...
...
openedx/core/djangoapps/credit/exceptions.py
View file @
36d1eb22
"""
This module contains the exceptions raised in credit course requirements.
"""
"""Exceptions raised by the credit API. """
class
InvalidCreditRequirements
(
Exception
):
"""
The
exception occurs when the requirement dictionary
has invalid format.
The
requirement dictionary provided
has invalid format.
"""
pass
class
InvalidCreditCourse
(
Exception
):
"""
The exception occurs when the the course is not marked as a Credit Course.
The course is not configured for credit.
"""
pass
class
UserIsNotEligible
(
Exception
):
"""
The user has not satisfied eligibility requirements for credit.
"""
pass
class
RequestAlreadyCompleted
(
Exception
):
"""
The user has already submitted a request and received a response from the credit provider.
"""
pass
class
CreditRequestNotFound
(
Exception
):
"""
The request does not exist.
"""
pass
class
InvalidCreditStatus
(
Exception
):
"""
The status is not either "approved" or "rejected".
"""
pass
openedx/core/djangoapps/credit/migrations/0003_add_creditrequirementstatus_reason.py
View file @
36d1eb22
openedx/core/djangoapps/credit/migrations/0006_auto__add_creditrequest__add_unique_creditrequest_username_course_prov.py
0 → 100644
View file @
36d1eb22
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
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 'CreditRequest'
db
.
create_table
(
'credit_creditrequest'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'uuid'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
unique
=
True
,
max_length
=
32
,
db_index
=
True
)),
(
'username'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'course'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
'credit_requests'
,
to
=
orm
[
'credit.CreditCourse'
])),
(
'provider'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
'credit_requests'
,
to
=
orm
[
'credit.CreditProvider'
])),
(
'timestamp'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
auto_now_add
=
True
,
blank
=
True
)),
(
'parameters'
,
self
.
gf
(
'jsonfield.fields.JSONField'
)()),
(
'status'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
'pending'
,
max_length
=
255
)),
))
db
.
send_create_signal
(
'credit'
,
[
'CreditRequest'
])
# Adding unique constraint on 'CreditRequest', fields ['username', 'course', 'provider']
db
.
create_unique
(
'credit_creditrequest'
,
[
'username'
,
'course_id'
,
'provider_id'
])
# Adding model 'HistoricalCreditRequest'
db
.
create_table
(
'credit_historicalcreditrequest'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)(
db_index
=
True
,
blank
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'uuid'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
32
,
db_index
=
True
)),
(
'username'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'timestamp'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
blank
=
True
)),
(
'parameters'
,
self
.
gf
(
'jsonfield.fields.JSONField'
)()),
(
'status'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
'pending'
,
max_length
=
255
)),
(
'course'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
blank
=
True
,
related_name
=
u'+'
,
null
=
True
,
on_delete
=
models
.
DO_NOTHING
,
to
=
orm
[
'credit.CreditCourse'
])),
(
'provider'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
blank
=
True
,
related_name
=
u'+'
,
null
=
True
,
on_delete
=
models
.
DO_NOTHING
,
to
=
orm
[
'credit.CreditProvider'
])),
(
u'history_id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
u'history_date'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)()),
(
u'history_user'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
u'+'
,
null
=
True
,
on_delete
=
models
.
SET_NULL
,
to
=
orm
[
'auth.User'
])),
(
u'history_type'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
1
)),
))
db
.
send_create_signal
(
'credit'
,
[
'HistoricalCreditRequest'
])
# Adding M2M table for field providers on 'CreditCourse'
m2m_table_name
=
db
.
shorten_name
(
'credit_creditcourse_providers'
)
db
.
create_table
(
m2m_table_name
,
(
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
primary_key
=
True
,
auto_created
=
True
)),
(
'creditcourse'
,
models
.
ForeignKey
(
orm
[
'credit.creditcourse'
],
null
=
False
)),
(
'creditprovider'
,
models
.
ForeignKey
(
orm
[
'credit.creditprovider'
],
null
=
False
))
))
db
.
create_unique
(
m2m_table_name
,
[
'creditcourse_id'
,
'creditprovider_id'
])
def
backwards
(
self
,
orm
):
# Removing unique constraint on 'CreditRequest', fields ['username', 'course', 'provider']
db
.
delete_unique
(
'credit_creditrequest'
,
[
'username'
,
'course_id'
,
'provider_id'
])
# Deleting model 'CreditRequest'
db
.
delete_table
(
'credit_creditrequest'
)
# Deleting model 'HistoricalCreditRequest'
db
.
delete_table
(
'credit_historicalcreditrequest'
)
# Removing M2M table for field providers on 'CreditCourse'
db
.
delete_table
(
db
.
shorten_name
(
'credit_creditcourse_providers'
))
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'
})
},
'credit.creditcourse'
:
{
'Meta'
:
{
'object_name'
:
'CreditCourse'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'providers'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['credit.CreditProvider']"
,
'symmetrical'
:
'False'
})
},
'credit.crediteligibility'
:
{
'Meta'
:
{
'unique_together'
:
"(('username', 'course'),)"
,
'object_name'
:
'CreditEligibility'
},
'course'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'eligibilities'"
,
'to'
:
"orm['credit.CreditCourse']"
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'provider'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'eligibilities'"
,
'to'
:
"orm['credit.CreditProvider']"
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
})
},
'credit.creditprovider'
:
{
'Meta'
:
{
'object_name'
:
'CreditProvider'
},
'active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'display_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'eligibility_duration'
:
(
'django.db.models.fields.PositiveIntegerField'
,
[],
{
'default'
:
'31556970'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'provider_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'provider_url'
:
(
'django.db.models.fields.URLField'
,
[],
{
'default'
:
"''"
,
'unique'
:
'True'
,
'max_length'
:
'255'
})
},
'credit.creditrequest'
:
{
'Meta'
:
{
'unique_together'
:
"(('username', 'course', 'provider'),)"
,
'object_name'
:
'CreditRequest'
},
'course'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'credit_requests'"
,
'to'
:
"orm['credit.CreditCourse']"
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'parameters'
:
(
'jsonfield.fields.JSONField'
,
[],
{}),
'provider'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'credit_requests'"
,
'to'
:
"orm['credit.CreditProvider']"
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'pending'"
,
'max_length'
:
'255'
}),
'timestamp'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'32'
,
'db_index'
:
'True'
})
},
'credit.creditrequirement'
:
{
'Meta'
:
{
'unique_together'
:
"(('namespace', 'name', 'course'),)"
,
'object_name'
:
'CreditRequirement'
},
'active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'course'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'credit_requirements'"
,
'to'
:
"orm['credit.CreditCourse']"
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'criteria'
:
(
'jsonfield.fields.JSONField'
,
[],
{}),
'display_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'255'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'namespace'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
})
},
'credit.creditrequirementstatus'
:
{
'Meta'
:
{
'object_name'
:
'CreditRequirementStatus'
},
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'reason'
:
(
'jsonfield.fields.JSONField'
,
[],
{
'default'
:
'{}'
}),
'requirement'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'statuses'"
,
'to'
:
"orm['credit.CreditRequirement']"
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
})
},
'credit.historicalcreditrequest'
:
{
'Meta'
:
{
'ordering'
:
"(u'-history_date', u'-history_id')"
,
'object_name'
:
'HistoricalCreditRequest'
},
'course'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'blank'
:
'True'
,
'related_name'
:
"u'+'"
,
'null'
:
'True'
,
'on_delete'
:
'models.DO_NOTHING'
,
'to'
:
"orm['credit.CreditCourse']"
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
u'history_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
u'history_id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
u'history_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'1'
}),
u'history_user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"u'+'"
,
'null'
:
'True'
,
'on_delete'
:
'models.SET_NULL'
,
'to'
:
"orm['auth.User']"
}),
'id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'parameters'
:
(
'jsonfield.fields.JSONField'
,
[],
{}),
'provider'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'blank'
:
'True'
,
'related_name'
:
"u'+'"
,
'null'
:
'True'
,
'on_delete'
:
'models.DO_NOTHING'
,
'to'
:
"orm['credit.CreditProvider']"
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'pending'"
,
'max_length'
:
'255'
}),
'timestamp'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
,
'db_index'
:
'True'
})
}
}
complete_apps
=
[
'credit'
]
openedx/core/djangoapps/credit/models.py
View file @
36d1eb22
...
...
@@ -9,6 +9,8 @@ successful completion of a course on EdX
import
logging
from
django.db
import
models
from
simple_history.models
import
HistoricalRecords
from
jsonfield.fields
import
JSONField
from
model_utils.models
import
TimeStampedModel
...
...
@@ -19,6 +21,29 @@ from django.utils.translation import ugettext_lazy
log
=
logging
.
getLogger
(
__name__
)
class
CreditProvider
(
TimeStampedModel
):
"""This model represents an institution that can grant credit for a course.
Each provider is identified by unique ID (e.g., 'ASU'). CreditProvider also
includes a `url` where the student will be sent when he/she will try to
get credit for course. Eligibility duration will be use to set duration
for which credit eligible message appears on dashboard.
"""
provider_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
,
unique
=
True
)
display_name
=
models
.
CharField
(
max_length
=
255
)
provider_url
=
models
.
URLField
(
max_length
=
255
,
unique
=
True
,
default
=
""
)
# Default is one year
DEFAULT_ELIGIBILITY_DURATION
=
31556970
eligibility_duration
=
models
.
PositiveIntegerField
(
help_text
=
ugettext_lazy
(
u"Number of seconds to show eligibility message"
),
default
=
DEFAULT_ELIGIBILITY_DURATION
)
active
=
models
.
BooleanField
(
default
=
True
)
class
CreditCourse
(
models
.
Model
):
"""
Model for tracking a credit course.
...
...
@@ -26,6 +51,7 @@ class CreditCourse(models.Model):
course_key
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
,
unique
=
True
)
enabled
=
models
.
BooleanField
(
default
=
False
)
providers
=
models
.
ManyToManyField
(
CreditProvider
)
@classmethod
def
is_credit_course
(
cls
,
course_key
):
...
...
@@ -55,26 +81,9 @@ class CreditCourse(models.Model):
return
cls
.
objects
.
get
(
course_key
=
course_key
,
enabled
=
True
)
class
CreditProvider
(
TimeStampedModel
):
"""This model represents an institution that can grant credit for a course.
Each provider is identified by unique ID (e.g., 'ASU'). CreditProvider also
includes a `url` where the student will be sent when he/she will try to
get credit for course. Eligibility duration will be use to set duration
for which credit eligible message appears on dashboard.
"""
provider_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
,
unique
=
True
)
display_name
=
models
.
CharField
(
max_length
=
255
)
provider_url
=
models
.
URLField
(
max_length
=
255
,
unique
=
True
)
eligibility_duration
=
models
.
PositiveIntegerField
(
help_text
=
ugettext_lazy
(
u"Number of seconds to show eligibility message"
)
)
active
=
models
.
BooleanField
(
default
=
True
)
class
CreditRequirement
(
TimeStampedModel
):
"""This model represents a credit requirement.
"""
This model represents a credit requirement.
Each requirement is uniquely identified by its 'namespace' and
'name' fields.
...
...
@@ -89,7 +98,7 @@ class CreditRequirement(TimeStampedModel):
course
=
models
.
ForeignKey
(
CreditCourse
,
related_name
=
"credit_requirements"
)
namespace
=
models
.
CharField
(
max_length
=
255
)
name
=
models
.
CharField
(
max_length
=
255
)
display_name
=
models
.
CharField
(
max_length
=
255
)
display_name
=
models
.
CharField
(
max_length
=
255
,
default
=
""
)
criteria
=
JSONField
()
active
=
models
.
BooleanField
(
default
=
True
)
...
...
@@ -101,7 +110,8 @@ class CreditRequirement(TimeStampedModel):
@classmethod
def
add_or_update_course_requirement
(
cls
,
credit_course
,
requirement
):
"""Add requirement to a given course.
"""
Add requirement to a given course.
Args:
credit_course(CreditCourse): The identifier for credit course
...
...
@@ -127,7 +137,8 @@ class CreditRequirement(TimeStampedModel):
@classmethod
def
get_course_requirements
(
cls
,
course_key
,
namespace
=
None
):
"""Get credit requirements of a given course.
"""
Get credit requirements of a given course.
Args:
course_key(CourseKey): The identifier for a course
...
...
@@ -143,7 +154,8 @@ class CreditRequirement(TimeStampedModel):
@classmethod
def
disable_credit_requirements
(
cls
,
requirement_ids
):
"""Mark the given requirements inactive.
"""
Mark the given requirements inactive.
Args:
requirement_ids(list): List of ids
...
...
@@ -155,7 +167,8 @@ class CreditRequirement(TimeStampedModel):
class
CreditRequirementStatus
(
TimeStampedModel
):
"""This model represents the status of each requirement.
"""
This model represents the status of each requirement.
For a particular credit requirement, a user can either:
1) Have satisfied the requirement (example: approved in-course reverification)
...
...
@@ -176,7 +189,7 @@ class CreditRequirementStatus(TimeStampedModel):
username
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
requirement
=
models
.
ForeignKey
(
CreditRequirement
,
related_name
=
"statuses"
)
status
=
models
.
CharField
(
choices
=
REQUIREMENT_STATUS_CHOICES
,
max_length
=
32
)
status
=
models
.
CharField
(
max_length
=
32
,
choices
=
REQUIREMENT_STATUS_CHOICES
)
# Include additional information about why the user satisfied or failed
# the requirement. This is specific to the type of requirement.
...
...
@@ -185,9 +198,13 @@ class CreditRequirementStatus(TimeStampedModel):
# the grade to users later and to send the information to credit providers.
reason
=
JSONField
(
default
=
{})
class
Meta
(
object
):
# pylint: disable=missing-docstring
get_latest_by
=
"created"
class
CreditEligibility
(
TimeStampedModel
):
"""A record of a user's eligibility for credit from a specific credit
"""
A record of a user's eligibility for credit from a specific credit
provider for a specific course.
"""
...
...
@@ -195,8 +212,87 @@ class CreditEligibility(TimeStampedModel):
course
=
models
.
ForeignKey
(
CreditCourse
,
related_name
=
"eligibilities"
)
provider
=
models
.
ForeignKey
(
CreditProvider
,
related_name
=
"eligibilities"
)
class
Meta
(
object
):
class
Meta
(
object
):
# pylint: disable=missing-docstring
unique_together
=
(
'username'
,
'course'
)
class
CreditRequest
(
TimeStampedModel
):
"""
Model metadata.
A request for credit from a particular credit provider.
When a user initiates a request for credit, a CreditRequest record will be created.
Each CreditRequest is assigned a unique identifier so we can find it when the request
is approved by the provider. The CreditRequest record stores the parameters to be sent
at the time the request is made. If the user re-issues the request
(perhaps because the user did not finish filling in forms on the credit provider's site),
the request record will be updated, but the UUID will remain the same.
"""
unique_together
=
(
'username'
,
'course'
)
uuid
=
models
.
CharField
(
max_length
=
32
,
unique
=
True
,
db_index
=
True
)
username
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
course
=
models
.
ForeignKey
(
CreditCourse
,
related_name
=
"credit_requests"
)
provider
=
models
.
ForeignKey
(
CreditProvider
,
related_name
=
"credit_requests"
)
timestamp
=
models
.
DateTimeField
(
auto_now_add
=
True
)
parameters
=
JSONField
()
REQUEST_STATUS_PENDING
=
"pending"
REQUEST_STATUS_APPROVED
=
"approved"
REQUEST_STATUS_REJECTED
=
"rejected"
REQUEST_STATUS_CHOICES
=
(
(
REQUEST_STATUS_PENDING
,
"Pending"
),
(
REQUEST_STATUS_APPROVED
,
"Approved"
),
(
REQUEST_STATUS_REJECTED
,
"Rejected"
),
)
status
=
models
.
CharField
(
max_length
=
255
,
choices
=
REQUEST_STATUS_CHOICES
,
default
=
REQUEST_STATUS_PENDING
)
history
=
HistoricalRecords
()
@classmethod
def
credit_requests_for_user
(
cls
,
username
):
"""
Retrieve all credit requests for a user.
Arguments:
username (unicode): The username of the user.
Returns: list
Example Usage:
>>> CreditRequest.credit_requests_for_user("bob")
[
{
"uuid": "557168d0f7664fe59097106c67c3f847",
"timestamp": "2015-05-04T20:57:57.987119+00:00",
"course_key": "course-v1:HogwartsX+Potions101+1T2015",
"provider": {
"id": "HogwartsX",
"display_name": "Hogwarts School of Witchcraft and Wizardry",
},
"status": "pending" # or "approved" or "rejected"
}
]
"""
return
[
{
"uuid"
:
request
.
uuid
,
"timestamp"
:
request
.
modified
,
"course_key"
:
request
.
course
.
course_key
,
"provider"
:
{
"id"
:
request
.
provider
.
provider_id
,
"display_name"
:
request
.
provider
.
display_name
},
"status"
:
request
.
status
}
for
request
in
cls
.
objects
.
select_related
(
'course'
,
'provider'
)
.
filter
(
username
=
username
)
]
class
Meta
(
object
):
# pylint: disable=missing-docstring
# Enforce the constraint that each user can have exactly one outstanding
# request to a given provider. Multiple requests use the same UUID.
unique_together
=
(
'username'
,
'course'
,
'provider'
)
openedx/core/djangoapps/credit/tests/test_api.py
View file @
36d1eb22
"""
Tests for
credit course api
.
Tests for
the API functions in the credit app
.
"""
import
datetime
import
ddt
import
pytz
import
dateutil.parser
as
date_parser
from
django.test
import
TestCase
from
django.db
import
connection
,
transaction
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.credit.api
import
(
get_credit_requirements
,
set_credit_requirements
,
_get_requirements_to_disable
from
student.tests.factories
import
UserFactory
from
openedx.core.djangoapps.credit
import
api
from
openedx.core.djangoapps.credit.exceptions
import
(
InvalidCreditRequirements
,
InvalidCreditCourse
,
RequestAlreadyCompleted
,
UserIsNotEligible
,
InvalidCreditStatus
,
CreditRequestNotFound
,
)
from
openedx.core.djangoapps.credit.models
import
(
CreditCourse
,
CreditProvider
,
CreditRequirement
,
CreditRequirementStatus
,
CreditEligibility
,
)
from
openedx.core.djangoapps.credit.exceptions
import
InvalidCreditRequirements
,
InvalidCreditCourse
from
openedx.core.djangoapps.credit.models
import
CreditCourse
,
CreditRequirement
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
@ddt.ddt
class
ApiTestCases
(
ModuleStoreTestCase
):
class
CreditApiTestBase
(
TestCase
):
"""
Tests for credit course api
.
Base class for test cases of the credit API
.
"""
PROVIDER_ID
=
"hogwarts"
PROVIDER_NAME
=
"Hogwarts School of Witchcraft and Wizardry"
def
setUp
(
self
,
**
kwargs
):
super
(
ApiTestCases
,
self
)
.
setUp
()
super
(
CreditApiTestBase
,
self
)
.
setUp
()
self
.
course_key
=
CourseKey
.
from_string
(
"edX/DemoX/Demo_Course"
)
def
add_credit_course
(
self
,
enabled
=
True
):
"""Mark the course as a credit """
credit_course
=
CreditCourse
.
objects
.
create
(
course_key
=
self
.
course_key
,
enabled
=
enabled
)
# Associate a credit provider with the course.
credit_provider
=
CreditProvider
.
objects
.
create
(
provider_id
=
self
.
PROVIDER_ID
,
display_name
=
self
.
PROVIDER_NAME
)
credit_course
.
providers
.
add
(
credit_provider
)
return
credit_course
@ddt.ddt
class
CreditRequirementApiTests
(
CreditApiTestBase
):
"""
Test Python API for credit requirements and eligibility.
"""
@ddt.data
(
[
{
...
...
@@ -52,12 +87,11 @@ class ApiTestCases(ModuleStoreTestCase):
def
test_set_credit_requirements_invalid_requirements
(
self
,
requirements
):
self
.
add_credit_course
()
with
self
.
assertRaises
(
InvalidCreditRequirements
):
set_credit_requirements
(
self
.
course_key
,
requirements
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
def
test_set_credit_requirements_invalid_course
(
self
):
"""Test that 'InvalidCreditCourse' exception is raise if we try to
set credit requirements for a non credit course.
"""
# Test that 'InvalidCreditCourse' exception is raise if we try to
# set credit requirements for a non credit course.
requirements
=
[
{
"namespace"
:
"grade"
,
...
...
@@ -67,16 +101,14 @@ class ApiTestCases(ModuleStoreTestCase):
}
]
with
self
.
assertRaises
(
InvalidCreditCourse
):
set_credit_requirements
(
self
.
course_key
,
requirements
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
add_credit_course
(
enabled
=
False
)
with
self
.
assertRaises
(
InvalidCreditCourse
):
set_credit_requirements
(
self
.
course_key
,
requirements
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
def
test_set_get_credit_requirements
(
self
):
"""Test that if same requirement is added multiple times
then it is added only one time and update for next all iterations.
"""
# Test that if same requirement is added multiple times
self
.
add_credit_course
()
requirements
=
[
{
...
...
@@ -96,17 +128,21 @@ class ApiTestCases(ModuleStoreTestCase):
}
}
]
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
get_credit_requirements
(
self
.
course_key
)),
1
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
api
.
get_credit_requirements
(
self
.
course_key
)),
1
)
# now verify that the saved requirement has values of last requirement
# from all same requirements
self
.
assertEqual
(
get_credit_requirements
(
self
.
course_key
)[
0
],
requirements
[
1
])
def
test_disable_credit_requirements
(
self
):
def
test_disable_existing_requirement
(
self
):
self
.
add_credit_course
()
# Set initial requirements
requirements
=
[
{
"namespace"
:
"reverification"
,
"name"
:
"midterm"
,
"display_name"
:
"Midterm"
,
"criteria"
:
{}
},
{
"namespace"
:
"grade"
,
"name"
:
"grade"
,
"display_name"
:
"Grade"
,
...
...
@@ -115,25 +151,17 @@ class ApiTestCases(ModuleStoreTestCase):
}
}
]
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
get_credit_requirements
(
self
.
course_key
)),
1
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
requirements
=
[
{
"namespace"
:
"reverification"
,
"name"
:
"i4x://edX/DemoX/edx-reverification-block/assessment_uuid"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{}
}
]
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
get_credit_requirements
(
self
.
course_key
)),
1
)
# Update the requirements, removing an existing requirement
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
[
1
:])
grade_req
=
CreditRequirement
.
objects
.
filter
(
namespace
=
"grade"
,
name
=
"grade"
)
self
.
assertEqual
(
len
(
grade_req
),
1
)
self
.
assertEqual
(
grade_req
[
0
]
.
active
,
False
)
# Expect that now only the grade requirement is returned
visible_reqs
=
api
.
get_credit_requirements
(
self
.
course_key
)
self
.
assertEqual
(
len
(
visible_reqs
),
1
)
self
.
assertEqual
(
visible_reqs
[
0
][
"namespace"
],
"grade"
)
def
test_
requirements_to_disable
(
self
):
def
test_
disable_credit_requirements
(
self
):
self
.
add_credit_course
()
requirements
=
[
{
...
...
@@ -145,10 +173,8 @@ class ApiTestCases(ModuleStoreTestCase):
}
}
]
set_credit_requirements
(
self
.
course_key
,
requirements
)
old_requirements
=
CreditRequirement
.
get_course_requirements
(
self
.
course_key
)
self
.
assertEqual
(
len
(
old_requirements
),
1
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
api
.
get_credit_requirements
(
self
.
course_key
)),
1
)
requirements
=
[
{
...
...
@@ -158,33 +184,225 @@ class ApiTestCases(ModuleStoreTestCase):
"criteria"
:
{}
}
]
requirements_to_disabled
=
_get_requirements_to_disable
(
old_requirements
,
requirements
)
self
.
assertEqual
(
len
(
requirements_to_disabled
),
1
)
self
.
assertEqual
(
requirements_to_disabled
[
0
],
old_requirements
[
0
]
.
id
)
api
.
set_credit_requirements
(
self
.
course_key
,
requirements
)
self
.
assertEqual
(
len
(
api
.
get_credit_requirements
(
self
.
course_key
)),
1
)
requirements
=
[
{
"namespace"
:
"grade"
,
"name"
:
"grade"
,
"display_name"
:
"Grade"
,
"criteria"
:
{
"min_grade"
:
0.8
}
},
{
"namespace"
:
"reverification"
,
"name"
:
"i4x://edX/DemoX/edx-reverification-block/assessment_uuid"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{}
grade_req
=
CreditRequirement
.
objects
.
filter
(
namespace
=
"grade"
,
name
=
"grade"
)
self
.
assertEqual
(
len
(
grade_req
),
1
)
self
.
assertEqual
(
grade_req
[
0
]
.
active
,
False
)
@ddt.ddt
class
CreditProviderIntegrationApiTests
(
CreditApiTestBase
):
"""
Test Python API for credit provider integration.
"""
USER_INFO
=
{
"username"
:
"bob"
,
"email"
:
"bob@example.com"
,
"full_name"
:
"Bob"
,
"mailing_address"
:
"123 Fake Street, Cambridge MA"
,
"country"
:
"US"
,
}
]
requirements_to_disabled
=
_get_requirements_to_disable
(
old_requirements
,
requirements
)
self
.
assertEqual
(
len
(
requirements_to_disabled
),
0
)
def
add_credit_course
(
self
,
enabled
=
True
):
FINAL_GRADE
=
0.95
def
setUp
(
self
):
super
(
CreditProviderIntegrationApiTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
(
username
=
self
.
USER_INFO
[
'username'
],
email
=
self
.
USER_INFO
[
'email'
],
)
self
.
user
.
profile
.
name
=
self
.
USER_INFO
[
'full_name'
]
self
.
user
.
profile
.
mailing_address
=
self
.
USER_INFO
[
'mailing_address'
]
self
.
user
.
profile
.
country
=
self
.
USER_INFO
[
'country'
]
self
.
user
.
profile
.
save
()
# By default, configure the database so that there is a single
# credit requirement that the user has satisfied (minimum grade)
self
.
_configure_credit
()
def
test_credit_request
(
self
):
# Initiate a credit request
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# Validate the UUID
self
.
assertIn
(
'uuid'
,
request
)
self
.
assertEqual
(
len
(
request
[
'uuid'
]),
32
)
# Validate the timestamp
self
.
assertIn
(
'timestamp'
,
request
)
parsed_date
=
date_parser
.
parse
(
request
[
'timestamp'
])
self
.
assertTrue
(
parsed_date
<
datetime
.
datetime
.
now
(
pytz
.
UTC
))
# Validate course information
self
.
assertIn
(
'course_org'
,
request
)
self
.
assertEqual
(
request
[
'course_org'
],
self
.
course_key
.
org
)
self
.
assertIn
(
'course_num'
,
request
)
self
.
assertEqual
(
request
[
'course_num'
],
self
.
course_key
.
course
)
self
.
assertIn
(
'course_run'
,
request
)
self
.
assertEqual
(
request
[
'course_run'
],
self
.
course_key
.
run
)
self
.
assertIn
(
'final_grade'
,
request
)
self
.
assertEqual
(
request
[
'final_grade'
],
self
.
FINAL_GRADE
)
# Validate user information
for
key
in
self
.
USER_INFO
.
keys
():
request_key
=
'user_{key}'
.
format
(
key
=
key
)
self
.
assertIn
(
request_key
,
request
)
self
.
assertEqual
(
request
[
request_key
],
self
.
USER_INFO
[
key
])
@ddt.data
(
"approved"
,
"rejected"
)
def
test_credit_request_status
(
self
,
status
):
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# Initial status should be "pending"
self
.
_assert_credit_status
(
"pending"
)
# Update the status
api
.
update_credit_request_status
(
request
[
'uuid'
],
status
)
self
.
_assert_credit_status
(
status
)
def
test_query_counts
(
self
):
# Yes, this is a lot of queries, but this API call is also doing a lot of work :)
# - 1 query: Check the user's eligibility and retrieve the credit course and provider.
# - 2 queries: Get-or-create the credit request.
# - 1 query: Retrieve user account and profile information from the user API.
# - 1 query: Look up the user's final grade from the credit requirements table.
# - 2 queries: Update the request.
# - 2 queries: Update the history table for the request.
with
self
.
assertNumQueries
(
9
):
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# - 3 queries: Retrieve and update the request
# - 1 query: Update the history table for the request.
with
self
.
assertNumQueries
(
4
):
api
.
update_credit_request_status
(
request
[
'uuid'
],
"approved"
)
with
self
.
assertNumQueries
(
1
):
api
.
get_credit_requests_for_user
(
self
.
USER_INFO
[
'username'
])
def
test_reuse_credit_request
(
self
):
# Create the first request
first_request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# Update the user's profile information, then attempt a second request
self
.
user
.
profile
.
name
=
"Bobby"
self
.
user
.
profile
.
save
()
second_request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# Request UUID should be the same
self
.
assertEqual
(
first_request
[
'uuid'
],
second_request
[
'uuid'
])
# Request should use the updated information
self
.
assertEqual
(
second_request
[
'user_full_name'
],
"Bobby"
)
@ddt.data
(
"approved"
,
"rejected"
)
def
test_cannot_make_credit_request_after_response
(
self
,
status
):
# Create the first request
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
# Provider updates the status
api
.
update_credit_request_status
(
request
[
'uuid'
],
status
)
# Attempting a second request raises an exception
with
self
.
assertRaises
(
RequestAlreadyCompleted
):
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
@ddt.data
(
"pending"
,
"failed"
)
def
test_user_is_not_eligible
(
self
,
status
):
# Simulate a user who is not eligible for credit
CreditEligibility
.
objects
.
all
()
.
delete
()
status
=
CreditRequirementStatus
.
objects
.
get
(
username
=
self
.
USER_INFO
[
'username'
])
status
.
status
=
status
status
.
reason
=
{}
status
.
save
()
with
self
.
assertRaises
(
UserIsNotEligible
):
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
def
test_create_request_null_mailing_address
(
self
):
# User did not specify a mailing address
self
.
user
.
profile
.
mailing_address
=
None
self
.
user
.
profile
.
save
()
# Request should include an empty mailing address field
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
self
.
assertEqual
(
request
[
"user_mailing_address"
],
""
)
def
test_create_request_null_country
(
self
):
# Simulate users who registered accounts before the country field was introduced.
# We need to manipulate the database directly because the country Django field
# coerces None values to empty strings.
query
=
"UPDATE auth_userprofile SET country = NULL WHERE id =
%
s"
connection
.
cursor
()
.
execute
(
query
,
[
str
(
self
.
user
.
profile
.
id
)])
transaction
.
commit_unless_managed
()
# Request should include an empty country field
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
self
.
assertEqual
(
request
[
"user_country"
],
""
)
def
test_user_has_no_final_grade
(
self
):
# Simulate an error condition that should never happen:
# a user is eligible for credit, but doesn't have a final
# grade recorded in the eligibility requirement.
grade_status
=
CreditRequirementStatus
.
objects
.
get
(
username
=
self
.
USER_INFO
[
'username'
],
requirement__namespace
=
"grade"
,
requirement__name
=
"grade"
)
grade_status
.
reason
=
{}
grade_status
.
save
()
with
self
.
assertRaises
(
UserIsNotEligible
):
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
def
test_update_invalid_credit_status
(
self
):
# The request status must be either "approved" or "rejected"
request
=
api
.
create_credit_request
(
self
.
course_key
,
self
.
PROVIDER_ID
,
self
.
USER_INFO
[
'username'
])
with
self
.
assertRaises
(
InvalidCreditStatus
):
api
.
update_credit_request_status
(
request
[
'uuid'
],
"invalid"
)
def
test_update_credit_request_not_found
(
self
):
# The request UUID must exist
with
self
.
assertRaises
(
CreditRequestNotFound
):
api
.
update_credit_request_status
(
"invalid_uuid"
,
"approved"
)
def
test_get_credit_requests_no_requests
(
self
):
requests
=
api
.
get_credit_requests_for_user
(
self
.
USER_INFO
[
'username'
])
self
.
assertEqual
(
requests
,
[])
def
_configure_credit
(
self
):
"""
Mark the course as a credit.
Configure a credit course and its requirements.
By default, add a single requirement (minimum grade)
that the user has satisfied.
"""
credit_course
=
CreditCourse
(
course_key
=
self
.
course_key
,
enabled
=
enabled
)
credit_course
.
save
()
return
credit_course
credit_course
=
self
.
add_credit_course
()
requirement
=
CreditRequirement
.
objects
.
create
(
course
=
credit_course
,
namespace
=
"grade"
,
name
=
"grade"
,
active
=
True
)
status
=
CreditRequirementStatus
.
objects
.
create
(
username
=
self
.
USER_INFO
[
'username'
],
requirement
=
requirement
,
)
status
.
status
=
"satisfied"
status
.
reason
=
{
"final_grade"
:
self
.
FINAL_GRADE
}
status
.
save
()
CreditEligibility
.
objects
.
create
(
username
=
self
.
USER_INFO
[
'username'
],
course
=
CreditCourse
.
objects
.
get
(
course_key
=
self
.
course_key
),
provider
=
CreditProvider
.
objects
.
get
(
provider_id
=
self
.
PROVIDER_ID
)
)
def
_assert_credit_status
(
self
,
expected_status
):
"""Check the user's credit status. """
statuses
=
api
.
get_credit_requests_for_user
(
self
.
USER_INFO
[
'username'
])
self
.
assertEqual
(
statuses
[
0
][
"status"
],
expected_status
)
openedx/core/djangoapps/credit/tests/test_models.py
View file @
36d1eb22
# -*- coding: utf-8 -*-
"""
Tests for credit course models.
"""
import
ddt
from
django.test
import
TestCase
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.credit.models
import
CreditCourse
,
CreditRequirement
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
@ddt.ddt
class
ModelTestCases
(
ModuleStore
TestCase
):
class
CreditEligibilityModelTests
(
TestCase
):
"""
Tests for credit
course models
.
Tests for credit
models used to track credit eligibility
.
"""
def
setUp
(
self
,
**
kwargs
):
super
(
ModelTestCase
s
,
self
)
.
setUp
()
super
(
CreditEligibilityModelTest
s
,
self
)
.
setUp
()
self
.
course_key
=
CourseKey
.
from_string
(
"edX/DemoX/Demo_Course"
)
@ddt.data
(
False
,
True
)
...
...
requirements/edx/base.txt
View file @
36d1eb22
...
...
@@ -30,6 +30,7 @@ django-openid-auth==0.4
django-robots==0.9.1
django-sekizai==0.6.1
django-ses==0.4.1
django-simple-history==1.6.1
django-storages==1.1.5
django-threaded-multihost==1.4-1
django-method-override==0.1.0
...
...
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