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
d62e2498
Commit
d62e2498
authored
Sep 18, 2017
by
Sofiya Semenova
Committed by
GitHub
Sep 18, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #15996 from edx/ssemenova/ed-1319
ED-1319 Cert not accessible by learner
parents
84c8fa30
3b34db29
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
122 additions
and
40 deletions
+122
-40
lms/djangoapps/certificates/signals.py
+19
-8
lms/djangoapps/certificates/tasks.py
+19
-2
lms/djangoapps/certificates/tests/test_signals.py
+45
-24
lms/djangoapps/certificates/tests/test_tasks.py
+39
-6
No files found.
lms/djangoapps/certificates/signals.py
View file @
d62e2498
...
...
@@ -14,6 +14,7 @@ from certificates.models import (
)
from
certificates.tasks
import
generate_certificate
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
openedx.core.djangoapps.certificates.api
import
auto_certificate_generation_enabled
from
openedx.core.djangoapps.certificates.config
import
waffle
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
...
...
@@ -22,6 +23,7 @@ from student.models import CourseEnrollment
log
=
logging
.
getLogger
(
__name__
)
CERTIFICATE_DELAY_SECONDS
=
2
@receiver
(
post_save
,
sender
=
CertificateWhitelist
,
dispatch_uid
=
"append_certificate_whitelist"
)
...
...
@@ -55,7 +57,7 @@ def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: dis
@receiver
(
LEARNER_NOW_VERIFIED
,
dispatch_uid
=
"learner_track_changed"
)
def
_listen_for_
track_change
(
sender
,
user
,
**
kwargs
):
# pylint: disable=unused-argument
def
_listen_for_
id_verification_status_changed
(
sender
,
user
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Catches a track change signal, determines user status,
calls fire_ungenerated_certificate_task for passing grades
...
...
@@ -65,16 +67,22 @@ def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused-
user_enrollments
=
CourseEnrollment
.
enrollments_for_user
(
user
=
user
)
grade_factory
=
CourseGradeFactory
()
expected_verification_status
,
_
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
for
enrollment
in
user_enrollments
:
if
grade_factory
.
read
(
user
=
user
,
course
=
enrollment
.
course_overview
)
.
passed
:
if
fire_ungenerated_certificate_task
(
user
,
enrollment
.
course_id
):
log
.
info
(
u'Certificate generation task initiated for {user} : {course} via track change'
.
format
(
if
fire_ungenerated_certificate_task
(
user
,
enrollment
.
course_id
,
expected_verification_status
):
message
=
(
u'Certificate generation task initiated for {user} : {course} via track change '
+
u'with verification status of {status}'
)
log
.
info
(
message
.
format
(
user
=
user
.
id
,
course
=
enrollment
.
course_id
course
=
enrollment
.
course_id
,
status
=
expected_verification_status
))
def
fire_ungenerated_certificate_task
(
user
,
course_key
):
def
fire_ungenerated_certificate_task
(
user
,
course_key
,
expected_verification_status
=
None
):
"""
Helper function to fire un-generated certificate tasks
...
...
@@ -87,8 +95,11 @@ def fire_ungenerated_certificate_task(user, course_key):
mode_is_verified
=
enrollment_mode
in
GeneratedCertificate
.
VERIFIED_CERTS_MODES
cert
=
GeneratedCertificate
.
certificate_for_student
(
user
,
course_key
)
if
mode_is_verified
and
(
cert
is
None
or
cert
.
status
==
'unverified'
):
generate_certificate
.
apply_async
(
kwargs
=
{
kwargs
=
{
'student'
:
unicode
(
user
.
id
),
'course_key'
:
unicode
(
course_key
),
})
'course_key'
:
unicode
(
course_key
)
}
if
expected_verification_status
:
kwargs
[
'expected_verification_status'
]
=
unicode
(
expected_verification_status
)
generate_certificate
.
apply_async
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
kwargs
)
return
True
lms/djangoapps/certificates/tasks.py
View file @
d62e2498
...
...
@@ -4,6 +4,7 @@ from logging import getLogger
from
celery_utils.logged_task
import
LoggedTask
from
celery_utils.persist_on_failure
import
PersistOnFailureTask
from
django.contrib.auth.models
import
User
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
opaque_keys.edx.keys
import
CourseKey
from
.api
import
generate_user_certificates
...
...
@@ -18,11 +19,27 @@ class _BaseCertificateTask(PersistOnFailureTask, LoggedTask): # pylint: disable
abstract
=
True
@task
(
base
=
_BaseCertificateTask
)
def
generate_certificate
(
**
kwargs
):
@task
(
base
=
_BaseCertificateTask
,
bind
=
True
,
default_retry_delay
=
30
,
max_retries
=
2
)
def
generate_certificate
(
self
,
**
kwargs
):
"""
Generates a certificate for a single user.
kwargs:
- student: The student for whom to generate a certificate.
- course_key: The course key for the course that the student is
receiving a certificate in.
- expected_verification_status: The expected verification status
for the user. When the status has changed, we double check
that the actual verification status is as expected before
generating a certificate, in the off chance that the database
has not yet updated with the user's new verification status.
"""
original_kwargs
=
kwargs
.
copy
()
student
=
User
.
objects
.
get
(
id
=
kwargs
.
pop
(
'student'
))
course_key
=
CourseKey
.
from_string
(
kwargs
.
pop
(
'course_key'
))
expected_verification_status
=
kwargs
.
pop
(
'expected_verification_status'
,
None
)
if
expected_verification_status
:
actual_verification_status
,
_
=
SoftwareSecurePhotoVerification
.
user_status
(
student
)
if
expected_verification_status
!=
actual_verification_status
:
raise
self
.
retry
(
kwargs
=
original_kwargs
)
generate_user_certificates
(
student
=
student
,
course_key
=
course_key
,
**
kwargs
)
lms/djangoapps/certificates/tests/test_signals.py
View file @
d62e2498
...
...
@@ -15,6 +15,7 @@ from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from
lms.djangoapps.grades.tests.utils
import
mock_passing_grade
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
openedx.core.djangoapps.certificates.config
import
waffle
from
lms.djangoapps.certificates.signals
import
CERTIFICATE_DELAY_SECONDS
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
...
@@ -91,10 +92,13 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
course
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
course
.
id
),
}
)
def
test_cert_generation_on_whitelist_append_instructor_paced
(
self
):
"""
...
...
@@ -116,10 +120,13 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user
=
self
.
user
,
course_id
=
self
.
ip_course
.
id
)
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
ip_course
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
ip_course
.
id
),
}
)
class
PassingGradeCertsTest
(
ModuleStoreTestCase
):
...
...
@@ -164,10 +171,13 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
# Certs fired after passing
with
mock_passing_grade
():
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
course
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
course
.
id
),
}
)
def
test_cert_generation_on_passing_instructor_paced
(
self
):
with
mock
.
patch
(
...
...
@@ -182,10 +192,13 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
# Certs fired after passing
with
mock_passing_grade
():
grade_factory
.
update
(
self
.
user
,
self
.
ip_course
)
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
ip_course
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user
.
id
),
'course_key'
:
unicode
(
self
.
ip_course
.
id
),
}
)
def
test_cert_already_generated
(
self
):
with
mock
.
patch
(
...
...
@@ -244,10 +257,14 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase):
status
=
'submitted'
)
attempt
.
approve
()
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user_one
.
id
),
'course_key'
:
unicode
(
self
.
course_one
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user_one
.
id
),
'course_key'
:
unicode
(
self
.
course_one
.
id
),
'expected_verification_status'
:
SoftwareSecurePhotoVerification
.
STATUS
.
approved
}
)
def
test_cert_generation_on_photo_verification_instructor_paced
(
self
):
with
mock
.
patch
(
...
...
@@ -261,7 +278,11 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase):
status
=
'submitted'
)
attempt
.
approve
()
mock_generate_certificate_apply_async
.
assert_called_with
(
kwargs
=
{
'student'
:
unicode
(
self
.
user_two
.
id
),
'course_key'
:
unicode
(
self
.
course_two
.
id
),
})
mock_generate_certificate_apply_async
.
assert_called_with
(
countdown
=
CERTIFICATE_DELAY_SECONDS
,
kwargs
=
{
'student'
:
unicode
(
self
.
user_two
.
id
),
'course_key'
:
unicode
(
self
.
course_two
.
id
),
'expected_verification_status'
:
SoftwareSecurePhotoVerification
.
STATUS
.
approved
}
)
lms/djangoapps/certificates/tests/test_tasks.py
View file @
d62e2498
from
unittest
import
TestCase
import
ddt
from
mock
import
patch
from
mock
import
call
,
patch
from
opaque_keys.edx.keys
import
CourseKey
from
nose.tools
import
assert_true
from
lms.djangoapps.certificates.tasks
import
generate_certificate
from
student.tests.factories
import
UserFactory
@ddt.ddt
class
GenerateUserCertificateTest
(
TestCase
):
@patch
(
'lms.djangoapps.certificates.tasks.generate_user_certificates'
)
@patch
(
'lms.djangoapps.certificates.tasks.User.objects.get'
)
def
test_
cert_task
(
self
,
user_get_mock
,
generate_user_certs_mock
):
def
test_
generate_user_certs
(
self
,
user_get_mock
,
generate_user_certs_mock
):
course_key
=
'course-v1:edX+CS101+2017_T2'
generate_certificate
(
student
=
'student-id'
,
course_key
=
course_key
,
otherarg
=
'c'
,
otherotherarg
=
'd'
)
kwargs
=
{
'student'
:
'student-id'
,
'course_key'
:
course_key
,
'otherarg'
:
'c'
,
'otherotherarg'
:
'd'
}
generate_certificate
.
apply_async
(
kwargs
=
kwargs
)
.
get
()
expected_student
=
user_get_mock
.
return_value
generate_user_certs_mock
.
assert_called_with
(
...
...
@@ -26,10 +33,36 @@ class GenerateUserCertificateTest(TestCase):
user_get_mock
.
assert_called_once_with
(
id
=
'student-id'
)
@ddt.data
(
'student'
,
'course_key'
)
def
test_
cert_task_
missing_args
(
self
,
missing_arg
):
def
test_missing_args
(
self
,
missing_arg
):
kwargs
=
{
'student'
:
'a'
,
'course_key'
:
'b'
,
'otherarg'
:
'c'
}
del
kwargs
[
missing_arg
]
with
patch
(
'lms.djangoapps.certificates.tasks.User.objects.get'
):
with
self
.
assertRaisesRegexp
(
KeyError
,
missing_arg
):
generate_certificate
(
**
kwargs
)
generate_certificate
.
apply_async
(
kwargs
=
kwargs
)
.
get
()
@patch
(
'lms.djangoapps.certificates.tasks.generate_user_certificates'
)
@patch
(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_status'
)
def
test_retry_until_verification_status_updates
(
self
,
user_status_mock
,
generate_user_certs_mock
):
course_key
=
'course-v1:edX+CS101+2017_T2'
student
=
UserFactory
()
kwargs
=
{
'student'
:
student
.
id
,
'course_key'
:
course_key
,
'expected_verification_status'
:
'approved'
}
user_status_mock
.
side_effect
=
[(
'pending'
,
''
),
(
'approved'
,
''
)]
generate_certificate
.
apply_async
(
kwargs
=
kwargs
)
.
get
()
user_status_mock
.
assert_has_calls
([
call
(
student
),
call
(
student
)
])
generate_user_certs_mock
.
assert_called_once_with
(
student
=
student
,
course_key
=
CourseKey
.
from_string
(
course_key
)
)
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