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
ab713181
Commit
ab713181
authored
Sep 11, 2017
by
Alex Dusenbery
Committed by
Alex Dusenbery
Sep 19, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
EDUCATOR-1316 | Refactor courseware.views.views._get_cert_data and related functions.
parent
fd07dea0
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
444 additions
and
378 deletions
+444
-378
common/djangoapps/student/views.py
+1
-1
lms/djangoapps/certificates/api.py
+61
-82
lms/djangoapps/certificates/tests/test_webview_views.py
+1
-1
lms/djangoapps/certificates/views/webview.py
+16
-14
lms/djangoapps/courseware/tests/test_views.py
+105
-145
lms/djangoapps/courseware/views/views.py
+116
-125
lms/djangoapps/instructor/views/instructor_dashboard.py
+1
-1
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+1
-1
openedx/core/djangoapps/certificates/api.py
+46
-0
openedx/core/djangoapps/certificates/apps.py
+13
-0
openedx/core/djangoapps/certificates/tests/test_api.py
+83
-8
No files found.
common/djangoapps/student/views.py
View file @
ab713181
...
...
@@ -376,7 +376,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
if
status
==
'ready'
:
# showing the certificate web view button if certificate is ready state and feature flags are enabled.
if
has_html_certificates_enabled
(
course_overview
.
id
,
course_overview
):
if
has_html_certificates_enabled
(
course_overview
):
if
course_overview
.
has_any_active_web_certificate
:
status_dict
.
update
({
'show_cert_web_view'
:
True
,
...
...
lms/djangoapps/certificates/api.py
View file @
ab713181
...
...
@@ -150,7 +150,12 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
xqueue
=
XQueueCertInterface
()
if
insecure
:
xqueue
.
use_https
=
False
generate_pdf
=
not
has_html_certificates_enabled
(
course_key
,
course
)
if
not
course
:
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
generate_pdf
=
not
has_html_certificates_enabled
(
course
)
cert
=
xqueue
.
add_cert
(
student
,
course_key
,
...
...
@@ -198,7 +203,11 @@ def regenerate_user_certificates(student, course_key, course=None,
if
insecure
:
xqueue
.
use_https
=
False
generate_pdf
=
not
has_html_certificates_enabled
(
course_key
,
course
)
if
not
course
:
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
generate_pdf
=
not
has_html_certificates_enabled
(
course
)
return
xqueue
.
regen_cert
(
student
,
course_key
,
...
...
@@ -353,44 +362,6 @@ def generate_example_certificates(course_key):
xqueue
.
add_example_cert
(
cert
)
def
has_html_certificates_enabled
(
course_key
,
course
=
None
):
"""
Determine if a course has html certificates enabled.
Arguments:
course_key (CourseKey|str): A course key or a string representation
of one.
course (CourseDescriptor|CourseOverview): A course.
"""
# If the feature is disabled, then immediately return a False
if
not
settings
.
FEATURES
.
get
(
'CERTIFICATES_HTML_VIEW'
,
False
):
return
False
# If we don't have a course object, we'll need to assemble one
if
not
course
:
# Initialize a course key if necessary
if
not
isinstance
(
course_key
,
CourseKey
):
try
:
course_key
=
CourseKey
.
from_string
(
course_key
)
except
InvalidKeyError
:
log
.
warning
(
(
'Unable to parse course_key "
%
s"'
,
course_key
),
exc_info
=
True
)
return
False
# Pull the course data from the cache
try
:
course
=
CourseOverview
.
get_from_id
(
course_key
)
except
:
# pylint: disable=bare-except
log
.
warning
(
(
'Unable to load CourseOverview object for course_key "
%
s"'
,
unicode
(
course_key
)),
exc_info
=
True
)
# Return the flag on the course object
return
course
.
cert_html_view_enabled
if
course
else
False
def
example_certificates_status
(
course_key
):
"""Check the status of example certificates for a course.
...
...
@@ -425,50 +396,58 @@ def example_certificates_status(course_key):
return
ExampleCertificateSet
.
latest_status
(
course_key
)
def
_safe_course_key
(
course_key
):
if
not
isinstance
(
course_key
,
CourseKey
):
return
CourseKey
.
from_string
(
course_key
)
return
course_key
def
_course_from_key
(
course_key
):
return
CourseOverview
.
get_from_id
(
_safe_course_key
(
course_key
))
def
_certificate_html_url
(
user_id
,
course_id
,
uuid
):
if
uuid
:
return
reverse
(
'certificates:render_cert_by_uuid'
,
kwargs
=
{
'certificate_uuid'
:
uuid
})
elif
user_id
and
course_id
:
kwargs
=
{
"user_id"
:
str
(
user_id
),
"course_id"
:
unicode
(
course_id
)}
return
reverse
(
'certificates:html_view'
,
kwargs
=
kwargs
)
return
''
def
_certificate_download_url
(
user_id
,
course_id
):
try
:
user_certificate
=
GeneratedCertificate
.
eligible_certificates
.
get
(
user
=
user_id
,
course_id
=
_safe_course_key
(
course_id
)
)
return
user_certificate
.
download_url
except
GeneratedCertificate
.
DoesNotExist
:
log
.
critical
(
'Unable to lookup certificate
\n
'
'user id:
%
d
\n
'
'course:
%
s'
,
user_id
,
unicode
(
course_id
)
)
return
''
def
has_html_certificates_enabled
(
course
):
if
not
settings
.
FEATURES
.
get
(
'CERTIFICATES_HTML_VIEW'
,
False
):
return
False
return
course
.
cert_html_view_enabled
def
get_certificate_url
(
user_id
=
None
,
course_id
=
None
,
uuid
=
None
):
"""
:return certificate url for web or pdf certs. In case of web certs returns either old
or new cert url based on given parameters. For web certs if `uuid` is it would return
new uuid based cert url url otherwise old url.
"""
url
=
""
if
has_html_certificates_enabled
(
course_id
):
if
uuid
:
url
=
reverse
(
'certificates:render_cert_by_uuid'
,
kwargs
=
dict
(
certificate_uuid
=
uuid
)
)
elif
user_id
and
course_id
:
url
=
reverse
(
'certificates:html_view'
,
kwargs
=
{
"user_id"
:
str
(
user_id
),
"course_id"
:
unicode
(
course_id
),
}
)
else
:
if
isinstance
(
course_id
,
basestring
):
try
:
course_id
=
CourseKey
.
from_string
(
course_id
)
except
InvalidKeyError
:
log
.
warning
(
(
'Unable to parse course_id "
%
s"'
,
course_id
),
exc_info
=
True
)
return
url
try
:
user_certificate
=
GeneratedCertificate
.
eligible_certificates
.
get
(
user
=
user_id
,
course_id
=
course_id
)
url
=
user_certificate
.
download_url
except
GeneratedCertificate
.
DoesNotExist
:
log
.
critical
(
'Unable to lookup certificate
\n
'
'user id:
%
d
\n
'
'course:
%
s'
,
user_id
,
unicode
(
course_id
)
)
url
=
''
course
=
_course_from_key
(
course_id
)
if
not
course
:
return
url
if
has_html_certificates_enabled
(
course
):
url
=
_certificate_html_url
(
user_id
,
course_id
,
uuid
)
else
:
url
=
_certificate_download_url
(
user_id
,
course_id
)
return
url
...
...
lms/djangoapps/certificates/tests/test_webview_views.py
View file @
ab713181
...
...
@@ -674,7 +674,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
self
.
_add_course_certificates
(
count
=
1
,
signatory_count
=
0
)
test_url
=
get_certificate_url
(
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
)
course_id
=
unicode
(
self
.
course
.
id
)
)
response
=
self
.
client
.
get
(
test_url
)
self
.
assertNotIn
(
'Signatory_Name 0'
,
response
.
content
)
...
...
lms/djangoapps/certificates/views/webview.py
View file @
ab713181
...
...
@@ -26,8 +26,7 @@ from certificates.api import (
get_certificate_footer_context
,
get_certificate_header_context
,
get_certificate_template
,
get_certificate_url
,
has_html_certificates_enabled
get_certificate_url
)
from
certificates.models
import
(
CertificateGenerationCourseSetting
,
...
...
@@ -48,8 +47,7 @@ from student.models import LinkedInAddToProfileConfiguration
from
util
import
organizations_helpers
as
organization_api
from
util.date_utils
import
strftime_localized
from
util.views
import
handle_500
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -523,23 +521,18 @@ def render_html_view(request, user_id, course_id):
_update_context_with_basic_info
(
context
,
course_id
,
platform_name
,
configuration
)
invalid_template_path
=
'certificates/invalid.html'
# Kick the user back to the "Invalid" screen if the feature is disabled
if
not
has_html_certificates_enabled
(
course_id
):
log
.
info
(
"Invalid cert: HTML certificates disabled for
%
s. User id:
%
d"
,
course_id
,
user_id
,
)
# Kick the user back to the "Invalid" screen if the feature is disabled globally
if
not
settings
.
FEATURES
.
get
(
'CERTIFICATES_HTML_VIEW'
,
False
):
return
render_to_response
(
invalid_template_path
,
context
)
# Load the course and user objects
try
:
course_key
=
CourseKey
.
from_string
(
course_id
)
user
=
User
.
objects
.
get
(
id
=
user_id
)
course
=
modulestore
()
.
get_course
(
course_key
)
course
=
get_course_by_id
(
course_key
)
# For any
other expected
exceptions, kick the user back to the "Invalid" screen
except
(
InvalidKeyError
,
ItemNotFoundError
,
User
.
DoesNotExist
)
as
exception
:
# For any
course or user
exceptions, kick the user back to the "Invalid" screen
except
(
InvalidKeyError
,
User
.
DoesNotExist
,
Http404
)
as
exception
:
error_str
=
(
"Invalid cert: error finding course
%
s or user with id "
"
%
d. Specific error:
%
s"
...
...
@@ -547,6 +540,15 @@ def render_html_view(request, user_id, course_id):
log
.
info
(
error_str
,
course_id
,
user_id
,
str
(
exception
))
return
render_to_response
(
invalid_template_path
,
context
)
# Kick the user back to the "Invalid" screen if the feature is disabled for the course
if
not
course
.
cert_html_view_enabled
:
log
.
info
(
"Invalid cert: HTML certificates disabled for
%
s. User id:
%
d"
,
course_id
,
user_id
,
)
return
render_to_response
(
invalid_template_path
,
context
)
# Load user's certificate
user_certificate
=
_get_user_certificate
(
request
,
user
,
course_key
,
course
,
preview_mode
)
if
not
user_certificate
:
...
...
lms/djangoapps/courseware/tests/test_views.py
View file @
ab713181
...
...
@@ -10,8 +10,13 @@ from HTMLParser import HTMLParser
from
urllib
import
quote
,
urlencode
from
uuid
import
uuid4
import
courseware.views.views
as
views
import
ddt
from
freezegun
import
freeze_time
from
mock
import
MagicMock
,
PropertyMock
,
create_autospec
,
patch
from
nose.plugins.attrib
import
attr
from
pytz
import
UTC
import
courseware.views.views
as
views
import
shoppingcart
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
certificates
import
api
as
certs_api
...
...
@@ -34,13 +39,12 @@ from django.http import Http404, HttpResponseBadRequest
from
django.test
import
TestCase
from
django.test.client
import
Client
,
RequestFactory
from
django.test.utils
import
override_settings
from
freezegun
import
freeze_time
from
lms.djangoapps.commerce.utils
import
EcommerceService
# pylint: disable=import-error
from
lms.djangoapps.grades.config.waffle
import
waffle
as
grades_waffle
from
lms.djangoapps.grades.config.waffle
import
ASSUME_ZERO_GRADE_IF_ABSENT
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.grades.tests.utils
import
mock_get_score
from
milestones.tests.utils
import
MilestonesTestCaseMixin
from
mock
import
MagicMock
,
PropertyMock
,
create_autospec
,
patch
from
nose.plugins.attrib
import
attr
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locations
import
Location
from
openedx.core.djangoapps.catalog.tests.factories
import
CourseFactory
as
CatalogCourseFactory
...
...
@@ -55,7 +59,6 @@ from openedx.core.djangolib.testing.utils import get_mock_request
from
openedx.core.lib.gating
import
api
as
gating_api
from
openedx.features.course_experience
import
COURSE_OUTLINE_PAGE_FLAG
from
openedx.features.enterprise_support.tests.mixins.enterprise
import
EnterpriseTestConsentRequired
from
pytz
import
UTC
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
AdminFactory
,
CourseEnrollmentFactory
,
UserFactory
from
util.tests.test_date_utils
import
fake_pgettext
,
fake_ugettext
...
...
@@ -1222,7 +1225,7 @@ class ProgressPageBaseTests(ModuleStoreTestCase):
start
=
datetime
(
2013
,
9
,
16
,
7
,
17
,
28
),
grade_cutoffs
=
{
u'çü†øƒƒ'
:
0.75
,
'Pass'
:
0.5
},
end
=
datetime
.
now
(),
certificate_available_date
=
datetime
.
now
(),
certificate_available_date
=
datetime
.
now
(
UTC
),
**
options
)
...
...
@@ -1359,10 +1362,6 @@ class ProgressPageTests(ProgressPageBaseTests):
self
.
assertNotContains
(
resp
,
'Request Certificate'
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'CERTIFICATES_HTML_VIEW'
:
True
})
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}}),
)
def
test_view_certificate_link
(
self
):
"""
If certificate web view is enabled then certificate web view button should appear for user who certificate is
...
...
@@ -1400,28 +1399,30 @@ class ProgressPageTests(ProgressPageBaseTests):
self
.
course
.
save
()
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u"View Certificate"
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}}
self
.
assertContains
(
resp
,
u"earned a certificate for this course"
)
cert_url
=
certs_api
.
get_certificate_url
(
course_id
=
self
.
course
.
id
,
uuid
=
certificate
.
verify_uuid
)
self
.
assertContains
(
resp
,
cert_url
)
resp
=
self
.
_get_progress_page
()
# when course certificate is not active
certificates
[
0
][
'is_active'
]
=
False
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
self
.
assertContains
(
resp
,
u"View Certificate"
)
resp
=
self
.
_get_progress_page
()
self
.
assertNotContains
(
resp
,
u"View Your Certificate"
)
self
.
assertNotContains
(
resp
,
u"You can now view your certificate"
)
self
.
assertContains
(
resp
,
"working on it..."
)
self
.
assertContains
(
resp
,
"creating your certificate"
)
self
.
assertContains
(
resp
,
u"earned a certificate for this course"
)
cert_url
=
certs_api
.
get_certificate_url
(
course_id
=
self
.
course
.
id
,
uuid
=
certificate
.
verify_uuid
)
self
.
assertContains
(
resp
,
cert_url
)
# when course certificate is not active
certificates
[
0
][
'is_active'
]
=
False
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
resp
=
self
.
_get_progress_page
()
self
.
assertNotContains
(
resp
,
u"View Your Certificate"
)
self
.
assertNotContains
(
resp
,
u"You can now view your certificate"
)
self
.
assertContains
(
resp
,
"working on it..."
)
self
.
assertContains
(
resp
,
"creating your certificate"
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'CERTIFICATES_HTML_VIEW'
:
False
})
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}})
)
def
test_view_certificate_link_hidden
(
self
):
"""
If certificate web view is disabled then certificate web view button should not appear for user who certificate
...
...
@@ -1441,8 +1442,13 @@ class ProgressPageTests(ProgressPageBaseTests):
# Enable certificate generation for this course
certs_api
.
set_cert_generation_enabled
(
self
.
course
.
id
,
True
)
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u"Download Your Certificate"
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}}
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u"Download Your Certificate"
)
@ddt.data
(
*
itertools
.
product
((
True
,
False
),
(
True
,
False
))
...
...
@@ -1452,7 +1458,7 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
self
.
setup_course
(
self_paced
=
self_paced
)
with
self
.
assertNumQueries
(
43
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
2
):
with
self
.
assertNumQueries
(
43
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
1
):
self
.
_get_progress_page
()
@ddt.data
(
...
...
@@ -1465,20 +1471,16 @@ class ProgressPageTests(ProgressPageBaseTests):
with
grades_waffle
()
.
override
(
ASSUME_ZERO_GRADE_IF_ABSENT
,
active
=
enable_waffle
):
with
self
.
assertNumQueries
(
initial
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
2
):
),
check_mongo_calls
(
1
):
self
.
_get_progress_page
()
# subsequent accesses to the progress page require fewer queries.
for
_
in
range
(
2
):
with
self
.
assertNumQueries
(
subsequent
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
2
):
),
check_mongo_calls
(
1
):
self
.
_get_progress_page
()
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}})
)
@ddt.data
(
*
itertools
.
product
(
(
...
...
@@ -1502,23 +1504,24 @@ class ProgressPageTests(ProgressPageBaseTests):
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
)
as
user_verify
:
user_verify
.
return_value
=
user_verified
resp
=
self
.
client
.
get
(
reverse
(
'progress'
,
args
=
[
unicode
(
self
.
course
.
id
)])
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}
}
cert_button_hidden
=
course_mode
is
CourseMode
.
AUDIT
or
\
course_mode
in
CourseMode
.
VERIFIED_MODES
and
not
user_verified
resp
=
self
.
_get_progress_page
()
self
.
assertEqual
(
cert_button_hidden
,
'Request Certificate'
not
in
resp
.
content
)
cert_button_hidden
=
course_mode
is
CourseMode
.
AUDIT
or
\
course_mode
in
CourseMode
.
VERIFIED_MODES
and
not
user_verified
self
.
assertEqual
(
cert_button_hidden
,
'Request Certificate'
not
in
resp
.
content
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'CERTIFICATES_HTML_VIEW'
:
True
})
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}})
)
def
test_page_with_invalidated_certificate_with_html_view
(
self
):
"""
Verify that for html certs if certificate is marked as invalidated than
...
...
@@ -1545,14 +1548,17 @@ class ProgressPageTests(ProgressPageBaseTests):
self
.
course
.
save
()
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u"View Certificate"
)
self
.
assert_invalidate_certificate
(
generated_certificate
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}
}
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u"View Certificate"
)
self
.
assert_invalidate_certificate
(
generated_certificate
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}})
)
def
test_page_with_invalidated_certificate_with_pdf
(
self
):
"""
Verify that for pdf certs if certificate is marked as invalidated than
...
...
@@ -1562,14 +1568,15 @@ class ProgressPageTests(ProgressPageBaseTests):
"http://www.example.com/certificate.pdf"
,
"honor"
)
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u'Download Your Certificate'
)
self
.
assert_invalidate_certificate
(
generated_certificate
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}}
resp
=
self
.
_get_progress_page
()
self
.
assertContains
(
resp
,
u'Download Your Certificate'
)
self
.
assert_invalidate_certificate
(
generated_certificate
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}})
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_message_for_audit_mode
(
self
):
""" Verify that message appears on progress page, if learner is enrolled
...
...
@@ -1578,14 +1585,19 @@ class ProgressPageTests(ProgressPageBaseTests):
user
=
UserFactory
.
create
()
self
.
assertTrue
(
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
))
CourseEnrollmentFactory
(
user
=
user
,
course_id
=
self
.
course
.
id
,
mode
=
CourseMode
.
AUDIT
)
response
=
self
.
_get_progress_page
()
self
.
assertContains
(
response
,
u'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
,
'section_breakdown'
:
[],
'grade_breakdown'
:
{}}
response
=
self
.
_get_progress_page
()
self
.
assertContains
(
response
,
u'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_invalidated_cert_data
(
self
):
"""
Verify that invalidated cert data is returned if cert is invalidated.
...
...
@@ -1600,11 +1612,10 @@ class ProgressPageTests(ProgressPageBaseTests):
)
# Invalidate user certificate
generated_certificate
.
invalidate
()
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
self
.
course
.
id
,
True
,
CourseMode
.
HONOR
)
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
CourseMode
.
HONOR
,
MagicMock
(
passed
=
True
)
)
self
.
assertEqual
(
response
.
cert_status
,
'invalidated'
)
self
.
assertEqual
(
response
.
title
,
'Your certificate has been invalidated'
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_downloadable_get_cert_data
(
self
):
"""
Verify that downloadable cert data is returned if cert is downloadable.
...
...
@@ -1614,12 +1625,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with
patch
(
'certificates.api.certificate_downloadable_status'
,
return_value
=
self
.
mock_certificate_downloadable_status
(
is_downloadable
=
True
)):
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
self
.
course
.
id
,
True
,
CourseMode
.
HONOR
)
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
CourseMode
.
HONOR
,
MagicMock
(
passed
=
True
)
)
self
.
assertEqual
(
response
.
cert_status
,
'downloadable'
)
self
.
assertEqual
(
response
.
title
,
'Your certificate is available'
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_generating_get_cert_data
(
self
):
"""
Verify that generating cert data is returned if cert is generating.
...
...
@@ -1629,12 +1639,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with
patch
(
'certificates.api.certificate_downloadable_status'
,
return_value
=
self
.
mock_certificate_downloadable_status
(
is_generating
=
True
)):
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
self
.
course
.
id
,
True
,
CourseMode
.
HONOR
)
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
CourseMode
.
HONOR
,
MagicMock
(
passed
=
True
)
)
self
.
assertEqual
(
response
.
cert_status
,
'generating'
)
self
.
assertEqual
(
response
.
title
,
"We're working on it..."
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_unverified_get_cert_data
(
self
):
"""
Verify that unverified cert data is returned if cert is unverified.
...
...
@@ -1644,12 +1653,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with
patch
(
'certificates.api.certificate_downloadable_status'
,
return_value
=
self
.
mock_certificate_downloadable_status
(
is_unverified
=
True
)):
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
self
.
course
.
id
,
True
,
CourseMode
.
HONOR
)
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
CourseMode
.
HONOR
,
MagicMock
(
passed
=
True
)
)
self
.
assertEqual
(
response
.
cert_status
,
'unverified'
)
self
.
assertEqual
(
response
.
title
,
"Certificate unavailable"
)
@patch
(
'courseware.views.views.is_course_passed'
,
PropertyMock
(
return_value
=
True
))
def
test_request_get_cert_data
(
self
):
"""
Verify that requested cert data is returned if cert is to be requested.
...
...
@@ -1659,7 +1667,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with
patch
(
'certificates.api.certificate_downloadable_status'
,
return_value
=
self
.
mock_certificate_downloadable_status
()):
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
self
.
course
.
id
,
True
,
CourseMode
.
HONOR
)
response
=
views
.
_get_cert_data
(
self
.
user
,
self
.
course
,
CourseMode
.
HONOR
,
MagicMock
(
passed
=
True
)
)
self
.
assertEqual
(
response
.
cert_status
,
'requesting'
)
self
.
assertEqual
(
response
.
title
,
"Congratulations, you qualified for a certificate!"
)
...
...
@@ -2013,54 +2021,6 @@ class VerifyCourseKeyDecoratorTests(TestCase):
@attr
(
shard
=
1
)
class
IsCoursePassedTests
(
ModuleStoreTestCase
):
"""
Tests for the is_course_passed helper function
"""
SUCCESS_CUTOFF
=
0.5
def
setUp
(
self
):
super
(
IsCoursePassedTests
,
self
)
.
setUp
()
self
.
student
=
UserFactory
()
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
number
=
'verified'
,
display_name
=
'Verified Course'
,
grade_cutoffs
=
{
'cutoff'
:
0.75
,
'Pass'
:
self
.
SUCCESS_CUTOFF
}
)
self
.
request
=
RequestFactory
()
self
.
request
.
user
=
self
.
student
def
test_user_fails_if_not_clear_exam
(
self
):
# If user has not grade then false will return
self
.
assertFalse
(
views
.
is_course_passed
(
self
.
course
,
None
,
self
.
student
,
self
.
request
))
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'percent'
:
0.9
}))
def
test_user_pass_if_percent_appears_above_passing_point
(
self
):
# Mocking the grades.grade
# If user has above passing marks then True will return
self
.
assertTrue
(
views
.
is_course_passed
(
self
.
course
,
None
,
self
.
student
,
self
.
request
))
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'percent'
:
0.2
}))
def
test_user_fail_if_percent_appears_below_passing_point
(
self
):
# Mocking the grades.grade
# If user has below passing marks then False will return
self
.
assertFalse
(
views
.
is_course_passed
(
self
.
course
,
None
,
self
.
student
,
self
.
request
))
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'percent'
:
SUCCESS_CUTOFF
})
)
def
test_user_with_passing_marks_and_achieved_marks_equal
(
self
):
# Mocking the grades.grade
# If user's achieved passing marks are equal to the required passing
# marks then it will return True
self
.
assertTrue
(
views
.
is_course_passed
(
self
.
course
,
None
,
self
.
student
,
self
.
request
))
@attr
(
shard
=
1
)
class
GenerateUserCertTests
(
ModuleStoreTestCase
):
"""
Tests for the view function Generated User Certs
...
...
@@ -2088,12 +2048,9 @@ class GenerateUserCertTests(ModuleStoreTestCase):
self
.
assertEqual
(
resp
.
status_code
,
HttpResponseBadRequest
.
status_code
)
self
.
assertIn
(
"Your certificate will be available when you pass the course."
,
resp
.
content
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
})
)
@patch
(
'courseware.views.views.is_course_passed'
,
return_value
=
True
)
@override_settings
(
CERT_QUEUE
=
'certificates'
,
LMS_SEGMENT_KEY
=
"foobar"
)
def
test_user_with_passing_grade
(
self
):
def
test_user_with_passing_grade
(
self
,
mock_is_course_passed
):
# If user has above passing grading then json will return cert generating message and
# status valid code
# mocking xqueue and analytics
...
...
@@ -2104,6 +2061,7 @@ class GenerateUserCertTests(ModuleStoreTestCase):
with
patch
(
'capa.xqueue_interface.XQueueInterface.send_to_queue'
)
as
mock_send_to_queue
:
mock_send_to_queue
.
return_value
=
(
0
,
"Successfully queued"
)
resp
=
self
.
client
.
post
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
...
...
@@ -2123,10 +2081,6 @@ class GenerateUserCertTests(ModuleStoreTestCase):
)
mock_tracker
.
reset_mock
()
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
})
)
def
test_user_with_passing_existing_generating_cert
(
self
):
# If user has passing grade but also has existing generating cert
# then json will return cert generating message with bad request code
...
...
@@ -2136,14 +2090,15 @@ class GenerateUserCertTests(ModuleStoreTestCase):
status
=
CertificateStatuses
.
generating
,
mode
=
'verified'
)
resp
=
self
.
client
.
post
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
HttpResponseBadRequest
.
status_code
)
self
.
assertIn
(
"Certificate is being created."
,
resp
.
content
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summary
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
}
resp
=
self
.
client
.
post
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
HttpResponseBadRequest
.
status_code
)
self
.
assertIn
(
"Certificate is being created."
,
resp
.
content
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary'
,
PropertyMock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
})
)
@override_settings
(
CERT_QUEUE
=
'certificates'
,
LMS_SEGMENT_KEY
=
"foobar"
)
def
test_user_with_passing_existing_downloadable_cert
(
self
):
# If user has already downloadable certificate
...
...
@@ -2156,9 +2111,14 @@ class GenerateUserCertTests(ModuleStoreTestCase):
mode
=
'verified'
)
resp
=
self
.
client
.
post
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
HttpResponseBadRequest
.
status_code
)
self
.
assertIn
(
"Certificate has already been created."
,
resp
.
content
)
with
patch
(
'lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create'
)
as
mock_create
:
course_grade
=
mock_create
.
return_value
course_grade
.
passed
=
True
course_grade
.
summay
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
}
resp
=
self
.
client
.
post
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
HttpResponseBadRequest
.
status_code
)
self
.
assertIn
(
"Certificate has already been created."
,
resp
.
content
)
def
test_user_with_non_existing_course
(
self
):
# If try to access a course with valid key pattern then it will return
...
...
lms/djangoapps/courseware/views/views.py
View file @
ab713181
...
...
@@ -71,13 +71,14 @@ from markupsafe import escape
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
openedx.core.djangoapps.catalog.utils
import
get_programs
,
get_programs_with_type
from
openedx.core.djangoapps.certificates
import
api
as
auto_certs_api
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.credit.api
import
(
get_credit_requirement_status
,
is_credit_course
,
is_user_eligible_for_credit
)
from
openedx.core.djangoapps.certificates
.config
import
waffle
as
certificates_waffle
from
openedx.core.djangoapps.certificates
import
api
as
auto_certs_api
from
openedx.core.djangoapps.models.course_details
import
CourseDetails
from
openedx.core.djangoapps.monitoring_utils
import
set_custom_metrics_for_course_key
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
...
...
@@ -117,6 +118,62 @@ CertData = namedtuple(
"CertData"
,
[
"cert_status"
,
"title"
,
"msg"
,
"download_url"
,
"cert_web_view_url"
]
)
AUDIT_PASSING_CERT_DATA
=
CertData
(
CertificateStatuses
.
audit_passing
,
_
(
'Your enrollment: Audit track'
),
_
(
'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
),
download_url
=
None
,
cert_web_view_url
=
None
)
GENERATING_CERT_DATA
=
CertData
(
CertificateStatuses
.
generating
,
_
(
"We're working on it..."
),
_
(
"We're creating your certificate. You can keep working in your courses and a link "
"to it will appear here and on your Dashboard when it is ready."
),
download_url
=
None
,
cert_web_view_url
=
None
)
INVALID_CERT_DATA
=
CertData
(
CertificateStatuses
.
invalidated
,
_
(
'Your certificate has been invalidated'
),
_
(
'Please contact your course team if you have any questions.'
),
download_url
=
None
,
cert_web_view_url
=
None
)
REQUESTING_CERT_DATA
=
CertData
(
CertificateStatuses
.
requesting
,
_
(
'Congratulations, you qualified for a certificate!'
),
_
(
"You've earned a certificate for this course."
),
download_url
=
None
,
cert_web_view_url
=
None
)
UNVERIFIED_CERT_DATA
=
CertData
(
CertificateStatuses
.
unverified
,
_
(
'Certificate unavailable'
),
_
(
'You have not received a certificate because you do not have a current {platform_name} '
'verified identity.'
)
.
format
(
platform_name
=
configuration_helpers
.
get_value
(
'PLATFORM_NAME'
,
settings
.
PLATFORM_NAME
)),
download_url
=
None
,
cert_web_view_url
=
None
)
def
_downloadable_cert_data
(
download_url
=
None
,
cert_web_view_url
=
None
):
return
CertData
(
CertificateStatuses
.
downloadable
,
_
(
'Your certificate is available'
),
_
(
"You've earned a certificate for this course."
),
download_url
=
download_url
,
cert_web_view_url
=
cert_web_view_url
)
def
user_groups
(
user
):
"""
...
...
@@ -872,23 +929,22 @@ def _progress(request, course_key, student_id):
course_grade
=
CourseGradeFactory
()
.
create
(
student
,
course
)
courseware_summary
=
course_grade
.
chapter_grades
.
values
()
grade_summary
=
course_grade
.
summary
studio_url
=
get_studio_url
(
course
,
'settings/grading'
)
# checking certificate generation configuration
enrollment_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
student
,
course_key
)
enrollment_mode
,
_
=
CourseEnrollment
.
enrollment_mode_for_user
(
student
,
course_key
)
context
=
{
'course'
:
course
,
'courseware_summary'
:
courseware_summary
,
'studio_url'
:
studio_url
,
'grade_summary'
:
grade_
summary
,
'grade_summary'
:
course_grade
.
summary
,
'staff_access'
:
staff_access
,
'masquerade'
:
masquerade
,
'supports_preview_menu'
:
True
,
'student'
:
student
,
'credit_course_requirements'
:
_credit_course_requirements
(
course_key
,
student
),
'certificate_data'
:
_get_cert_data
(
student
,
course
,
course_key
,
is_active
,
enrollment_mode
,
grade_summary
),
'certificate_data'
:
_get_cert_data
(
student
,
course
,
enrollment_mode
,
course_grade
),
}
context
.
update
(
get_experiment_user_metadata_context
(
...
...
@@ -903,128 +959,68 @@ def _progress(request, course_key, student_id):
return
response
def
_get_cert_data
(
student
,
course
,
course_key
,
is_active
,
enrollment_mode
,
grade_summary
=
None
):
def
_downloadable_certificate_message
(
course
,
cert_downloadable_status
):
if
certs_api
.
has_html_certificates_enabled
(
course
):
if
certs_api
.
get_active_web_certificate
(
course
)
is
not
None
:
return
_downloadable_cert_data
(
download_url
=
None
,
cert_web_view_url
=
certs_api
.
get_certificate_url
(
course_id
=
course
.
id
,
uuid
=
cert_downloadable_status
[
'uuid'
]
)
)
else
:
return
GENERATING_CERT_DATA
return
_downloadable_cert_data
(
download_url
=
cert_downloadable_status
[
'download_url'
])
def
_missing_required_verification
(
student
,
enrollment_mode
):
return
(
enrollment_mode
in
CourseMode
.
VERIFIED_MODES
and
not
SoftwareSecurePhotoVerification
.
user_is_verified
(
student
)
)
def
_certificate_message
(
student
,
course
,
enrollment_mode
):
if
certs_api
.
is_certificate_invalid
(
student
,
course
.
id
):
return
INVALID_CERT_DATA
cert_downloadable_status
=
certs_api
.
certificate_downloadable_status
(
student
,
course
.
id
)
if
cert_downloadable_status
[
'is_generating'
]:
return
GENERATING_CERT_DATA
if
cert_downloadable_status
[
'is_unverified'
]
or
_missing_required_verification
(
student
,
enrollment_mode
):
return
UNVERIFIED_CERT_DATA
if
cert_downloadable_status
[
'is_downloadable'
]:
return
_downloadable_certificate_message
(
course
,
cert_downloadable_status
)
return
REQUESTING_CERT_DATA
def
_get_cert_data
(
student
,
course
,
enrollment_mode
,
course_grade
=
None
):
"""Returns students course certificate related data.
Arguments:
student (User): Student for whom certificate to retrieve.
course (Course): Course object for which certificate data to retrieve.
course_key (CourseKey): Course identifier for course.
is_active (Bool): Boolean value to check if course is active.
enrollment_mode (String): Course mode in which student is enrolled.
grade_summary (dict): Student grade details
.
course_grade (CourseGrade): Student's course grade record
.
Returns:
returns dict if course certificate is available else None.
"""
from
lms.djangoapps.courseware.courses
import
get_course_by_id
if
not
CourseMode
.
is_eligible_for_certificate
(
enrollment_mode
):
return
CertData
(
CertificateStatuses
.
audit_passing
,
_
(
'Your enrollment: Audit track'
),
_
(
'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
),
download_url
=
None
,
cert_web_view_url
=
None
)
may_view_certificate
=
False
if
course_key
:
may_view_certificate
=
get_course_by_id
(
course_key
)
.
may_certify
()
switches
=
certificates_waffle
.
waffle
()
switch_enabled
=
switches
.
is_enabled
(
certificates_waffle
.
AUTO_CERTIFICATE_GENERATION
)
student_cert_generation_enabled
=
switch_enabled
or
certs_api
.
cert_generation_enabled
(
course_key
)
# Don't show certificate information if:
# 1) the learner has not passed the course
# 2) the course is not active
# 3) auto-generated certs flags are not enabled, but student cert generation is not enabled either
# 4) the learner may not view the certificate, based on the course's advanced course settings.
if
not
all
([
is_course_passed
(
course
,
grade_summary
),
is_active
,
student_cert_generation_enabled
,
may_view_certificate
]):
return
None
return
AUDIT_PASSING_CERT_DATA
if
certs_api
.
is_certificate_invalid
(
student
,
course_key
):
return
CertData
(
CertificateStatuses
.
invalidated
,
_
(
'Your certificate has been invalidated'
),
_
(
'Please contact your course team if you have any questions.'
),
download_url
=
None
,
cert_web_view_url
=
None
)
certificates_enabled_for_course
=
certs_api
.
cert_generation_enabled
(
course
.
id
)
if
course_grade
is
None
:
course_grade
=
CourseGradeFactory
()
.
create
(
student
,
course
)
cert_downloadable_status
=
certs_api
.
certificate_downloadable_status
(
student
,
course_key
)
if
not
auto_certs_api
.
can_show_certificate_message
(
course
,
student
,
course_grade
,
certificates_enabled_for_course
):
return
generating_certificate_message
=
CertData
(
CertificateStatuses
.
generating
,
_
(
"We're working on it..."
),
_
(
"We're creating your certificate. You can keep working in your courses and a link "
"to it will appear here and on your Dashboard when it is ready."
),
download_url
=
None
,
cert_web_view_url
=
None
)
if
cert_downloadable_status
[
'is_downloadable'
]:
if
certs_api
.
has_html_certificates_enabled
(
course_key
,
course
):
if
certs_api
.
get_active_web_certificate
(
course
)
is
not
None
:
return
CertData
(
CertificateStatuses
.
downloadable
,
_
(
'Your certificate is available'
),
_
(
"You've earned a certificate for this course."
),
download_url
=
None
,
cert_web_view_url
=
certs_api
.
get_certificate_url
(
course_id
=
course_key
,
uuid
=
cert_downloadable_status
[
'uuid'
]
)
)
else
:
# If there is an error, the user should see the generating certificate message
# until a new certificate is generated.
return
generating_certificate_message
return
CertData
(
CertificateStatuses
.
downloadable
,
_
(
'Your certificate is available'
),
_
(
"You've earned a certificate for this course."
),
download_url
=
cert_downloadable_status
[
'download_url'
],
cert_web_view_url
=
None
)
if
cert_downloadable_status
[
'is_generating'
]:
return
generating_certificate_message
# If the learner is in verified modes and the student did not have
# their ID verified, we need to show message to ask learner to verify their ID first
missing_required_verification
=
(
enrollment_mode
in
CourseMode
.
VERIFIED_MODES
and
not
SoftwareSecurePhotoVerification
.
user_is_verified
(
student
)
)
if
missing_required_verification
or
cert_downloadable_status
[
'is_unverified'
]:
platform_name
=
configuration_helpers
.
get_value
(
'PLATFORM_NAME'
,
settings
.
PLATFORM_NAME
)
return
CertData
(
CertificateStatuses
.
unverified
,
_
(
'Certificate unavailable'
),
_
(
'You have not received a certificate because you do not have a current {platform_name} '
'verified identity.'
)
.
format
(
platform_name
=
platform_name
),
download_url
=
None
,
cert_web_view_url
=
None
)
return
CertData
(
CertificateStatuses
.
requesting
,
_
(
'Congratulations, you qualified for a certificate!'
),
_
(
"You've earned a certificate for this course."
),
download_url
=
None
,
cert_web_view_url
=
None
)
return
_certificate_message
(
student
,
course
,
enrollment_mode
)
def
_credit_course_requirements
(
course_key
,
student
):
...
...
@@ -1281,26 +1277,21 @@ def course_survey(request, course_id):
)
def
is_course_passed
(
course
,
grade_summary
=
None
,
student
=
None
,
request
=
None
):
def
is_course_passed
(
student
,
course
,
course_grade
=
None
):
"""
check user's course passing status. return True if passed
Arguments:
course : course object
grade_summary (dict) : contains student grade details.
student : user object
request (HttpRequest)
course : course object
course_grade (CourseGrade) : contains student grade details.
Returns:
returns bool value
"""
nonzero_cutoffs
=
[
cutoff
for
cutoff
in
course
.
grade_cutoffs
.
values
()
if
cutoff
>
0
]
success_cutoff
=
min
(
nonzero_cutoffs
)
if
nonzero_cutoffs
else
None
if
grade_summary
is
None
:
grade_summary
=
CourseGradeFactory
()
.
create
(
student
,
course
)
.
summary
return
success_cutoff
and
grade_summary
[
'percent'
]
>=
success_cutoff
if
course_grade
is
None
:
course_grade
=
CourseGradeFactory
()
.
create
(
student
,
course
)
return
course_grade
.
passed
# Grades can potentially be written - if so, let grading manage the transaction.
...
...
@@ -1343,7 +1334,7 @@ def generate_user_cert(request, course_id):
if
not
course
:
return
HttpResponseBadRequest
(
_
(
"Course is not valid"
))
if
not
is_course_passed
(
course
,
None
,
student
,
request
):
if
not
is_course_passed
(
student
,
course
):
return
HttpResponseBadRequest
(
_
(
"Your certificate will be available when you pass the course."
))
certificate_status
=
certs_api
.
certificate_downloadable_status
(
student
,
course
.
id
)
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
ab713181
...
...
@@ -331,7 +331,7 @@ def _section_certificates(course):
"""
example_cert_status
=
None
html_cert_enabled
=
certs_api
.
has_html_certificates_enabled
(
course
.
id
,
course
)
html_cert_enabled
=
certs_api
.
has_html_certificates_enabled
(
course
)
if
html_cert_enabled
:
can_enable_for_course
=
True
else
:
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
ab713181
...
...
@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed'
:
3
,
'skipped'
:
2
}
with
self
.
assertNumQueries
(
17
1
):
with
self
.
assertNumQueries
(
17
6
):
self
.
assertCertificatesGenerated
(
task_input
,
expected_results
)
expected_results
=
{
...
...
openedx/core/djangoapps/certificates/api.py
View file @
ab713181
...
...
@@ -2,8 +2,12 @@
The public API for certificates.
"""
from
datetime
import
datetime
from
pytz
import
UTC
from
course_modes.models
import
CourseMode
from
openedx.core.djangoapps.certificates.config
import
waffle
from
student.models
import
CourseEnrollment
SWITCHES
=
waffle
.
waffle
()
...
...
@@ -19,6 +23,48 @@ def _enabled_and_instructor_paced(course):
return
False
def
certificates_viewable_for_course
(
course
):
"""
Returns True if certificates are viewable for any student enrolled in the course, False otherwise.
"""
if
course
.
self_paced
:
return
True
if
(
course
.
certificates_display_behavior
in
(
'early_with_info'
,
'early_no_info'
)
or
course
.
certificates_show_before_end
):
return
True
if
(
course
.
certificate_available_date
and
course
.
certificate_available_date
<=
datetime
.
now
(
UTC
)
):
return
True
if
(
course
.
certificate_available_date
is
None
and
course
.
has_ended
()
):
return
True
return
False
def
is_certificate_valid
(
certificate
):
"""
Returns True if the student has a valid, verified certificate for this course, False otherwise.
"""
return
CourseEnrollment
.
is_enrolled_as_verified
(
certificate
.
user
,
certificate
.
course_id
)
and
certificate
.
is_valid
()
def
can_show_certificate_message
(
course
,
student
,
course_grade
,
certificates_enabled_for_course
):
if
not
(
(
auto_certificate_generation_enabled
()
or
certificates_enabled_for_course
)
and
CourseEnrollment
.
is_enrolled
(
student
,
course
.
id
)
and
certificates_viewable_for_course
(
course
)
and
course_grade
.
passed
):
return
False
return
True
def
can_show_certificate_available_date_field
(
course
):
return
_enabled_and_instructor_paced
(
course
)
...
...
openedx/core/djangoapps/certificates/apps.py
0 → 100644
View file @
ab713181
"""
Openedx Certificates Application Configuration
"""
from
django.apps
import
AppConfig
class
OpenedxCertificatesConfig
(
AppConfig
):
"""
Application Configuration for Openedx Certificates.
"""
name
=
'openedx.core.djangoapps.certificates'
label
=
'openedx_certificates'
openedx/core/djangoapps/certificates/tests/test_api.py
View file @
ab713181
from
contextlib
import
contextmanager
from
datetime
import
datetime
,
timedelta
import
itertools
from
unittest
import
TestCase
import
ddt
import
pytz
import
waffle
from
course_modes.models
import
CourseMode
from
openedx.core.djangoapps.certificates
import
api
from
openedx.core.djangoapps.certificates.config
import
waffle
as
certs_waffle
from
openedx.core.djangoapps.content.course_overviews.tests.factories
import
CourseOverviewFactory
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
# TODO: Copied from lms.djangoapps.certificates.models,
# to be resolved per https://openedx.atlassian.net/browse/EDUCATOR-1318
class
CertificateStatuses
(
object
):
"""
Enum for certificate statuses
"""
deleted
=
'deleted'
deleting
=
'deleting'
downloadable
=
'downloadable'
error
=
'error'
generating
=
'generating'
notpassing
=
'notpassing'
restricted
=
'restricted'
unavailable
=
'unavailable'
auditing
=
'auditing'
audit_passing
=
'audit_passing'
audit_notpassing
=
'audit_notpassing'
unverified
=
'unverified'
invalidated
=
'invalidated'
requesting
=
'requesting'
ALL_STATUSES
=
(
deleted
,
deleting
,
downloadable
,
error
,
generating
,
notpassing
,
restricted
,
unavailable
,
auditing
,
audit_passing
,
audit_notpassing
,
unverified
,
invalidated
,
requesting
)
class
MockGeneratedCertificate
(
object
):
"""
We can't import GeneratedCertificate from LMS here, so we roll
our own minimal Certificate model for testing.
"""
def
__init__
(
self
,
user
=
None
,
course_id
=
None
,
mode
=
None
,
status
=
None
):
self
.
user
=
user
self
.
course_id
=
course_id
self
.
mode
=
mode
self
.
status
=
status
def
is_valid
(
self
):
"""
Return True if certificate is valid else return False.
"""
return
self
.
status
==
CertificateStatuses
.
downloadable
@contextmanager
...
...
@@ -15,18 +64,29 @@ def configure_waffle_namespace(feature_enabled):
namespace
=
certs_waffle
.
waffle
()
with
namespace
.
override
(
certs_waffle
.
AUTO_CERTIFICATE_GENERATION
,
active
=
feature_enabled
):
yield
yield
@ddt.ddt
class
FeatureEnabled
TestCase
(
TestCase
):
class
CertificatesApi
TestCase
(
TestCase
):
def
setUp
(
self
):
super
(
FeatureEnabledTestCase
,
self
)
.
setUp
()
self
.
course
=
CourseOverviewFactory
.
create
()
def
tearDown
(
self
):
super
(
FeatureEnabledTestCase
,
self
)
.
tearDown
()
self
.
course
.
self_paced
=
False
super
(
CertificatesApiTestCase
,
self
)
.
setUp
()
self
.
course
=
CourseOverviewFactory
.
create
(
start
=
datetime
(
2017
,
1
,
1
,
tzinfo
=
pytz
.
UTC
),
end
=
datetime
(
2017
,
1
,
31
,
tzinfo
=
pytz
.
UTC
),
certificate_available_date
=
None
)
self
.
user
=
UserFactory
.
create
()
self
.
enrollment
=
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
is_active
=
True
,
mode
=
'audit'
,
)
self
.
certificate
=
MockGeneratedCertificate
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
@ddt.data
(
True
,
False
)
def
test_auto_certificate_generation_enabled
(
self
,
feature_enabled
):
...
...
@@ -46,3 +106,18 @@ class FeatureEnabledTestCase(TestCase):
self
.
course
.
self_paced
=
is_self_paced
with
configure_waffle_namespace
(
feature_enabled
):
self
.
assertEqual
(
expected_value
,
api
.
can_show_certificate_available_date_field
(
self
.
course
))
@ddt.data
(
(
CourseMode
.
VERIFIED
,
CertificateStatuses
.
downloadable
,
True
),
(
CourseMode
.
VERIFIED
,
CertificateStatuses
.
notpassing
,
False
),
(
CourseMode
.
AUDIT
,
CertificateStatuses
.
downloadable
,
False
)
)
@ddt.unpack
def
test_is_certificate_valid
(
self
,
enrollment_mode
,
certificate_status
,
expected_value
):
self
.
enrollment
.
mode
=
enrollment_mode
self
.
enrollment
.
save
()
self
.
certificate
.
mode
=
CourseMode
.
VERIFIED
self
.
certificate
.
status
=
certificate_status
self
.
assertEqual
(
expected_value
,
api
.
is_certificate_valid
(
self
.
certificate
))
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