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
16f06bf6
Commit
16f06bf6
authored
Jun 30, 2017
by
Gregory Martin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Generate eligible certificates on learner track change
parent
32618fa0
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
196 additions
and
56 deletions
+196
-56
lms/djangoapps/certificates/signals.py
+47
-10
lms/djangoapps/certificates/tests/test_signals.py
+113
-41
lms/djangoapps/grades/new/course_grade_factory.py
+4
-3
lms/djangoapps/grades/tests/test_new.py
+1
-1
lms/djangoapps/verify_student/models.py
+7
-0
openedx/core/djangoapps/signals/apps.py
+20
-0
openedx/core/djangoapps/signals/signals.py
+4
-1
No files found.
lms/djangoapps/certificates/signals.py
View file @
16f06bf6
...
...
@@ -15,8 +15,10 @@ from certificates.models import \
GeneratedCertificate
from
certificates.tasks
import
generate_certificate
from
courseware
import
courses
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
openedx.core.djangoapps.models.course_details
import
COURSE_PACING_CHANGE
from
openedx.core.djangoapps.signals.signals
import
COURSE_GRADE_NOW_PASSED
from
openedx.core.djangoapps.signals.signals
import
COURSE_GRADE_NOW_PASSED
,
LEARNER_NOW_VERIFIED
from
student.models
import
CourseEnrollment
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -76,7 +78,6 @@ def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: dis
Listen for a learner passing a course, send cert generation task,
downstream signal from COURSE_GRADE_CHANGED
"""
# No flags enabled
if
(
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
SELF_PACED_ONLY
)
and
...
...
@@ -86,19 +87,55 @@ def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: dis
# Only SELF_PACED_ONLY flag enabled
if
waffle
.
waffle
()
.
is_enabled
(
waffle
.
SELF_PACED_ONLY
):
if
not
courses
.
get_course_by_id
(
course_
key
,
depth
=
0
)
.
self_paced
:
if
not
courses
.
get_course_by_id
(
course_
id
,
depth
=
0
)
.
self_paced
:
return
# Only INSTRUCTOR_PACED_ONLY flag enabled
el
if
waffle
.
waffle
()
.
is_enabled
(
waffle
.
INSTRUCTOR_PACED_ONLY
):
if
courses
.
get_course_by_id
(
course_
key
,
depth
=
0
)
.
self_paced
:
if
waffle
.
waffle
()
.
is_enabled
(
waffle
.
INSTRUCTOR_PACED_ONLY
):
if
courses
.
get_course_by_id
(
course_
id
,
depth
=
0
)
.
self_paced
:
return
if
GeneratedCertificate
.
certificate_for_student
(
self
.
user
,
self
.
course_id
)
is
None
:
generate_certificate
.
apply_async
(
student
=
user
,
course_key
=
course_id
,
)
if
fire_ungenerated_certificate_task
(
user
=
user
,
course_id
=
course_id
):
log
.
info
(
u'Certificate generation task initiated for {user} : {course} via passing grade'
.
format
(
user
=
user
.
id
,
course
=
course_id
))
@receiver
(
LEARNER_NOW_VERIFIED
,
dispatch_uid
=
"learner_track_changed"
)
def
_listen_for_track_change
(
sender
,
user
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Catches a track change signal, determines user status,
calls fire_ungenerated_certificate_task for passing grades
"""
if
(
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
SELF_PACED_ONLY
)
and
not
waffle
.
waffle
()
.
is_enabled
(
waffle
.
INSTRUCTOR_PACED_ONLY
)
):
return
user_enrollments
=
CourseEnrollment
.
enrollments_for_user
(
user
=
user
)
grade_factory
=
CourseGradeFactory
()
for
enrollment
in
user_enrollments
:
if
grade_factory
.
read
(
user
=
user
,
course
=
enrollment
.
course
)
.
passed
:
if
fire_ungenerated_certificate_task
(
user
=
user
,
course_id
=
enrollment
.
course
.
id
):
log
.
info
(
u'Certificate generation task initiated for {user} : {course} via track change'
.
format
(
user
=
user
.
id
,
course
=
enrollment
.
course
.
id
))
def
fire_ungenerated_certificate_task
(
user
,
course_id
):
"""
Helper function to fire un-generated certificate tasks
"""
if
GeneratedCertificate
.
certificate_for_student
(
user
,
course_id
)
is
None
:
generate_certificate
.
apply_async
(
student
=
user
,
course_key
=
course_id
)
return
True
lms/djangoapps/certificates/tests/test_signals.py
View file @
16f06bf6
...
...
@@ -6,10 +6,15 @@ import mock
from
certificates
import
api
as
certs_api
from
certificates.config
import
waffle
from
certificates.models
import
CertificateGenerationConfiguration
,
CertificateWhitelist
from
certificates.models
import
\
CertificateGenerationConfiguration
,
\
CertificateWhitelist
,
\
GeneratedCertificate
,
\
CertificateStatuses
from
certificates.signals
import
_listen_for_course_pacing_changed
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.grades.tests.utils
import
mock_get_score
from
lms.djangoapps.grades.tests.utils
import
mock_passing_grade
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
...
@@ -72,10 +77,7 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
mock_generate_certificate_apply_async
.
assert_not_called
(
student
=
self
.
user
,
course_key
=
self
.
course
.
id
)
mock_generate_certificate_apply_async
.
assert_not_called
()
with
waffle
.
waffle
()
.
override
(
waffle
.
SELF_PACED_ONLY
,
active
=
True
):
CertificateWhitelist
.
objects
.
create
(
user
=
self
.
user
,
...
...
@@ -100,10 +102,7 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user
=
self
.
user
,
course_id
=
self
.
ip_course
.
id
)
mock_generate_certificate_apply_async
.
assert_not_called
(
student
=
self
.
user
,
course_key
=
self
.
ip_course
.
id
)
mock_generate_certificate_apply_async
.
assert_not_called
()
with
waffle
.
waffle
()
.
override
(
waffle
.
INSTRUCTOR_PACED_ONLY
,
active
=
True
):
CertificateWhitelist
.
objects
.
create
(
user
=
self
.
user
,
...
...
@@ -121,7 +120,9 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
"""
def
setUp
(
self
):
super
(
PassingGradeCertsTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
self_paced
=
True
)
self
.
course
=
CourseFactory
.
create
(
self_paced
=
True
,
)
self
.
user
=
UserFactory
.
create
()
self
.
enrollment
=
CourseEnrollmentFactory
(
user
=
self
.
user
,
...
...
@@ -130,6 +131,12 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
mode
=
"verified"
,
)
self
.
ip_course
=
CourseFactory
.
create
(
self_paced
=
False
)
self
.
ip_enrollment
=
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
ip_course
.
id
,
is_active
=
True
,
mode
=
"verified"
,
)
def
test_cert_generation_on_passing_self_paced
(
self
):
with
mock
.
patch
(
...
...
@@ -138,22 +145,13 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
)
as
mock_generate_certificate_apply_async
:
with
waffle
.
waffle
()
.
override
(
waffle
.
SELF_PACED_ONLY
,
active
=
True
):
grade_factory
=
CourseGradeFactory
()
with
mock_get_score
(
0
,
2
):
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_not_called
(
student
=
self
.
user
,
course_key
=
self
.
course
.
id
)
with
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_called
(
student
=
self
.
user
,
course_key
=
self
.
course
.
id
)
# Certs are not re-fired after passing
with
mock_get_score
(
2
,
2
):
# Not passing
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_not_called
()
# Certs fired after passing
with
mock_passing_grade
():
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_
not_called
(
mock_generate_certificate_apply_async
.
assert_
called_with
(
student
=
self
.
user
,
course_key
=
self
.
course
.
id
)
...
...
@@ -165,22 +163,96 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
)
as
mock_generate_certificate_apply_async
:
with
waffle
.
waffle
()
.
override
(
waffle
.
INSTRUCTOR_PACED_ONLY
,
active
=
True
):
grade_factory
=
CourseGradeFactory
()
with
mock_get_score
(
0
,
2
):
grade_factory
.
update
(
self
.
user
,
self
.
ip_course
)
mock_generate_certificate_apply_async
.
assert_not_called
(
student
=
self
.
user
,
course_key
=
self
.
ip_course
.
id
)
with
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
user
,
self
.
ip_course
)
mock_generate_certificate_apply_async
.
assert_called
(
student
=
self
.
user
,
course_key
=
self
.
ip_course
.
id
)
# Certs are not re-fired after passing
with
mock_get_score
(
2
,
2
):
# Not passing
grade_factory
.
update
(
self
.
user
,
self
.
ip_course
)
mock_generate_certificate_apply_async
.
assert_not_called
()
# Certs fired after passing
with
mock_passing_grade
():
grade_factory
.
update
(
self
.
user
,
self
.
ip_course
)
mock_generate_certificate_apply_async
.
assert_
not_called
(
mock_generate_certificate_apply_async
.
assert_
called_with
(
student
=
self
.
user
,
course_key
=
self
.
ip_course
.
id
)
def
test_cert_already_generated
(
self
):
with
mock
.
patch
(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async'
,
return_value
=
None
)
as
mock_generate_certificate_apply_async
:
grade_factory
=
CourseGradeFactory
()
# Create the certificate
GeneratedCertificate
.
eligible_certificates
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
downloadable
)
# Certs are not re-fired after passing
with
mock_passing_grade
():
grade_factory
.
update
(
self
.
user
,
self
.
course
)
mock_generate_certificate_apply_async
.
assert_not_called
()
class
LearnerTrackChangeCertsTest
(
ModuleStoreTestCase
):
"""
Tests for certificate generation task firing on learner verification
"""
def
setUp
(
self
):
super
(
LearnerTrackChangeCertsTest
,
self
)
.
setUp
()
self
.
course_one
=
CourseFactory
.
create
(
self_paced
=
True
)
self
.
user_one
=
UserFactory
.
create
()
self
.
enrollment_one
=
CourseEnrollmentFactory
(
user
=
self
.
user_one
,
course_id
=
self
.
course_one
.
id
,
is_active
=
True
,
mode
=
'honor'
,
)
self
.
user_two
=
UserFactory
.
create
()
self
.
course_two
=
CourseFactory
.
create
(
self_paced
=
False
)
self
.
enrollment_two
=
CourseEnrollmentFactory
(
user
=
self
.
user_two
,
course_id
=
self
.
course_two
.
id
,
is_active
=
True
,
mode
=
'honor'
)
def
test_cert_generation_on_photo_verification_self_paced
(
self
):
with
mock
.
patch
(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async'
,
return_value
=
None
)
as
mock_generate_certificate_apply_async
:
with
mock_passing_grade
():
grade_factory
=
CourseGradeFactory
()
grade_factory
.
update
(
self
.
user_one
,
self
.
course_one
)
with
waffle
.
waffle
()
.
override
(
waffle
.
SELF_PACED_ONLY
,
active
=
True
):
mock_generate_certificate_apply_async
.
assert_not_called
()
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user_one
,
status
=
'submitted'
)
attempt
.
approve
()
mock_generate_certificate_apply_async
.
assert_called_with
(
student
=
self
.
user_one
,
course_key
=
self
.
course_one
.
id
)
def
test_cert_generation_on_photo_verification_instructor_paced
(
self
):
with
mock
.
patch
(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async'
,
return_value
=
None
)
as
mock_generate_certificate_apply_async
:
with
mock_passing_grade
():
grade_factory
=
CourseGradeFactory
()
grade_factory
.
update
(
self
.
user_two
,
self
.
course_two
)
with
waffle
.
waffle
()
.
override
(
waffle
.
INSTRUCTOR_PACED_ONLY
,
active
=
True
):
mock_generate_certificate_apply_async
.
assert_not_called
()
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user_two
,
status
=
'submitted'
)
attempt
.
approve
()
mock_generate_certificate_apply_async
.
assert_called_with
(
student
=
self
.
user_two
,
course_key
=
self
.
course_two
.
id
)
lms/djangoapps/grades/new/course_grade_factory.py
View file @
16f06bf6
...
...
@@ -159,6 +159,7 @@ class CourseGradeFactory(object):
persistent_grade
.
letter_grade
,
persistent_grade
.
passed_timestamp
is
not
None
,
)
log
.
info
(
u'Grades: Read,
%
s, User:
%
s,
%
s'
,
unicode
(
course_data
),
user
.
id
,
persistent_grade
)
return
course_grade
,
persistent_grade
.
grading_policy_hash
...
...
@@ -199,11 +200,11 @@ class CourseGradeFactory(object):
course_key
=
course_data
.
course_key
,
deadline
=
course_data
.
course
.
end
,
)
if
course_grade
.
passed
is
True
:
COURSE_GRADE_NOW_PASSED
.
send
_robust
(
if
course_grade
.
passed
:
COURSE_GRADE_NOW_PASSED
.
send
(
sender
=
CourseGradeFactory
,
user
=
user
,
course_
key
=
course_data
.
course_key
,
course_
id
=
course_data
.
course_key
,
)
log
.
info
(
...
...
lms/djangoapps/grades/tests/test_new.py
View file @
16f06bf6
...
...
@@ -186,7 +186,7 @@ class TestCourseGradeFactory(GradeTestBase):
self
.
assertEqual
(
course_grade
.
letter_grade
,
u'Pass'
if
expected_pass
else
None
)
self
.
assertEqual
(
course_grade
.
percent
,
0.5
)
with
self
.
assertNumQueries
(
1
1
),
mock_get_score
(
1
,
2
):
with
self
.
assertNumQueries
(
1
3
),
mock_get_score
(
1
,
2
):
_assert_create
(
expected_pass
=
True
)
with
self
.
assertNumQueries
(
13
),
mock_get_score
(
1
,
2
):
...
...
lms/djangoapps/verify_student/models.py
View file @
16f06bf6
...
...
@@ -41,11 +41,13 @@ from lms.djangoapps.verify_student.ssencrypt import (
random_aes_key
,
rsa_encrypt
)
from
openedx.core.djangoapps.signals.signals
import
LEARNER_NOW_VERIFIED
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
from
openedx.core.djangolib.model_mixins
import
DeprecatedModelMixin
from
openedx.core.storage
import
get_storage
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -516,6 +518,11 @@ class PhotoVerification(StatusModel):
self
.
reviewing_service
=
service
self
.
status
=
"approved"
self
.
save
()
# Emit signal to find and generate eligible certificates
LEARNER_NOW_VERIFIED
.
send_robust
(
sender
=
PhotoVerification
,
user
=
self
.
user
)
@status_before_must_be
(
"must_retry"
,
"submitted"
,
"approved"
,
"denied"
)
def
deny
(
self
,
...
...
openedx/core/djangoapps/signals/apps.py
0 → 100644
View file @
16f06bf6
"""
Signal handlers are registered at startup here.
"""
from
django.apps
import
AppConfig
class
SignalConfig
(
AppConfig
):
"""
Application Configuration for Signals.
"""
name
=
u'openedx.core.djangoapps.signals'
def
ready
(
self
):
"""
Connect handlers.
"""
# Can't import models at module level in AppConfigs, and models get
# included from the signal handlers
from
.signals
import
handlers
# pylint: disable=unused-variable
openedx/core/djangoapps/signals/signals.py
View file @
16f06bf6
...
...
@@ -16,6 +16,9 @@ COURSE_CERT_AWARDED = Signal(providing_args=["user", "course_key", "mode", "stat
COURSE_GRADE_NOW_PASSED
=
Signal
(
providing_args
=
[
'user'
,
# user object
'course_
key
'
,
# course.id
'course_
id
'
,
# course.id
]
)
# Signal that indicates that a user has become verified
LEARNER_NOW_VERIFIED
=
Signal
(
providing_args
=
[
'user'
])
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