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
f17ab1ec
Commit
f17ab1ec
authored
Nov 06, 2013
by
Gabe Mulley
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1533 from mulby/gabe/enrollment-event
emit enrollment events
parents
a6c02ccc
5734cae1
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
164 additions
and
16 deletions
+164
-16
common/djangoapps/student/models.py
+61
-8
common/djangoapps/student/tests/tests.py
+64
-1
common/djangoapps/track/contexts.py
+33
-7
lms/djangoapps/courseware/features/certificates.feature
+4
-0
lms/djangoapps/courseware/features/registration.feature
+2
-0
No files found.
common/djangoapps/student/models.py
View file @
f17ab1ec
...
...
@@ -29,6 +29,12 @@ from django.forms import ModelForm, forms
from
course_modes.models
import
CourseMode
import
comment_client
as
cc
from
pytz
import
UTC
import
crum
from
track
import
contexts
from
track.views
import
server_track
from
eventtracking
import
tracker
unenroll_done
=
django
.
dispatch
.
Signal
(
providing_args
=
[
"course_enrollment"
])
...
...
@@ -667,6 +673,11 @@ class PendingEmailChange(models.Model):
activation_key
=
models
.
CharField
((
'activation key'
),
max_length
=
32
,
unique
=
True
,
db_index
=
True
)
EVENT_NAME_ENROLLMENT_ACTIVATED
=
'edx.course.enrollment.activated'
EVENT_NAME_ENROLLMENT_DEACTIVATED
=
'edx.course.enrollment.deactivated'
class
CourseEnrollment
(
models
.
Model
):
"""
Represents a Student's Enrollment record for a single Course. You should
...
...
@@ -737,19 +748,55 @@ class CourseEnrollment(models.Model):
if
user
.
id
is
None
:
user
.
save
()
enrollment
,
_
=
CourseEnrollment
.
objects
.
get_or_create
(
enrollment
,
created
=
CourseEnrollment
.
objects
.
get_or_create
(
user
=
user
,
course_id
=
course_id
,
)
# In case we're reactivating a deactivated enrollment, or changing the
# enrollment mode.
if
enrollment
.
mode
!=
mode
or
enrollment
.
is_active
!=
is_active
:
enrollment
.
mode
=
mode
activation_changed
=
False
if
enrollment
.
is_active
!=
is_active
:
enrollment
.
is_active
=
is_active
activation_changed
=
True
mode_changed
=
False
if
enrollment
.
mode
!=
mode
:
enrollment
.
mode
=
mode
mode_changed
=
True
if
activation_changed
or
mode_changed
:
enrollment
.
save
()
if
created
:
if
is_active
:
enrollment
.
emit_event
(
EVENT_NAME_ENROLLMENT_ACTIVATED
)
else
:
if
activation_changed
:
if
is_active
:
enrollment
.
emit_event
(
EVENT_NAME_ENROLLMENT_ACTIVATED
)
else
:
enrollment
.
emit_event
(
EVENT_NAME_ENROLLMENT_DEACTIVATED
)
return
enrollment
def
emit_event
(
self
,
event_name
):
"""
Emits an event to explicitly track course enrollment and unenrollment.
"""
try
:
context
=
contexts
.
course_context_from_course_id
(
self
.
course_id
)
data
=
{
'user_id'
:
self
.
user
.
id
,
'course_id'
:
self
.
course_id
,
'mode'
:
self
.
mode
,
}
with
tracker
.
get_tracker
()
.
context
(
event_name
,
context
):
server_track
(
crum
.
get_current_request
(),
event_name
,
data
)
except
:
# pylint: disable=bare-except
if
event_name
and
self
.
course_id
:
log
.
exception
(
'Unable to emit event
%
s for user
%
s and course
%
s'
,
event_name
,
self
.
user
.
username
,
self
.
course_id
)
@classmethod
def
enroll
(
cls
,
user
,
course_id
,
mode
=
"honor"
):
"""
...
...
@@ -825,9 +872,13 @@ class CourseEnrollment(models.Model):
"""
try
:
record
=
CourseEnrollment
.
objects
.
get
(
user
=
user
,
course_id
=
course_id
)
record
.
is_active
=
False
record
.
save
()
unenroll_done
.
send
(
sender
=
cls
,
course_enrollment
=
record
)
if
record
.
is_active
:
record
.
is_active
=
False
record
.
save
()
record
.
emit_event
(
EVENT_NAME_ENROLLMENT_DEACTIVATED
)
unenroll_done
.
send
(
sender
=
cls
,
course_enrollment
=
record
)
except
cls
.
DoesNotExist
:
err_msg
=
u"Tried to unenroll student {} from {} but they were not enrolled"
log
.
error
(
err_msg
.
format
(
user
,
course_id
))
...
...
@@ -918,6 +969,7 @@ class CourseEnrollment(models.Model):
if
not
self
.
is_active
:
self
.
is_active
=
True
self
.
save
()
self
.
emit_event
(
EVENT_NAME_ENROLLMENT_ACTIVATED
)
def
deactivate
(
self
):
"""Makes this `CourseEnrollment` record inactive. Saves immediately. An
...
...
@@ -926,6 +978,7 @@ class CourseEnrollment(models.Model):
if
self
.
is_active
:
self
.
is_active
=
False
self
.
save
()
self
.
emit_event
(
EVENT_NAME_ENROLLMENT_DEACTIVATED
)
def
refundable
(
self
):
"""
...
...
common/djangoapps/student/tests/tests.py
View file @
f17ab1ec
...
...
@@ -25,7 +25,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
courseware.tests.tests
import
TEST_DATA_MIXED_MODULESTORE
from
mock
import
Mock
,
patch
from
mock
import
Mock
,
patch
,
sentinel
from
textwrap
import
dedent
from
student.models
import
unique_id_for_user
,
CourseEnrollment
...
...
@@ -276,6 +276,16 @@ class DashboardTest(TestCase):
class
EnrollInCourseTest
(
TestCase
):
"""Tests enrolling and unenrolling in courses."""
def
setUp
(
self
):
patcher
=
patch
(
'student.models.server_track'
)
self
.
mock_server_track
=
patcher
.
start
()
self
.
addCleanup
(
patcher
.
stop
)
crum_patcher
=
patch
(
'student.models.crum.get_current_request'
)
self
.
mock_get_current_request
=
crum_patcher
.
start
()
self
.
addCleanup
(
crum_patcher
.
stop
)
self
.
mock_get_current_request
.
return_value
=
sentinel
.
request
def
test_enrollment
(
self
):
user
=
User
.
objects
.
create_user
(
"joe"
,
"joe@joe.com"
,
"password"
)
course_id
=
"edX/Test101/2013"
...
...
@@ -289,24 +299,28 @@ class EnrollInCourseTest(TestCase):
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled_by_partial
(
user
,
course_id_partial
))
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id
)
# Enrolling them again should be harmless
CourseEnrollment
.
enroll
(
user
,
course_id
)
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled_by_partial
(
user
,
course_id_partial
))
self
.
assert_no_events_were_emitted
()
# Now unenroll the user
CourseEnrollment
.
unenroll
(
user
,
course_id
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled_by_partial
(
user
,
course_id_partial
))
self
.
assert_unenrollment_event_was_emitted
(
user
,
course_id
)
# Unenrolling them again should also be harmless
CourseEnrollment
.
unenroll
(
user
,
course_id
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled_by_partial
(
user
,
course_id_partial
))
self
.
assert_no_events_were_emitted
()
# The enrollment record should still exist, just be inactive
enrollment_record
=
CourseEnrollment
.
objects
.
get
(
...
...
@@ -315,6 +329,37 @@ class EnrollInCourseTest(TestCase):
)
self
.
assertFalse
(
enrollment_record
.
is_active
)
def
assert_no_events_were_emitted
(
self
):
"""Ensures no events were emitted since the last event related assertion"""
self
.
assertFalse
(
self
.
mock_server_track
.
called
)
self
.
mock_server_track
.
reset_mock
()
def
assert_enrollment_event_was_emitted
(
self
,
user
,
course_id
):
"""Ensures an enrollment event was emitted since the last event related assertion"""
self
.
mock_server_track
.
assert_called_once_with
(
sentinel
.
request
,
'edx.course.enrollment.activated'
,
{
'course_id'
:
course_id
,
'user_id'
:
user
.
pk
,
'mode'
:
'honor'
}
)
self
.
mock_server_track
.
reset_mock
()
def
assert_unenrollment_event_was_emitted
(
self
,
user
,
course_id
):
"""Ensures an unenrollment event was emitted since the last event related assertion"""
self
.
mock_server_track
.
assert_called_once_with
(
sentinel
.
request
,
'edx.course.enrollment.deactivated'
,
{
'course_id'
:
course_id
,
'user_id'
:
user
.
pk
,
'mode'
:
'honor'
}
)
self
.
mock_server_track
.
reset_mock
()
def
test_enrollment_non_existent_user
(
self
):
# Testing enrollment of newly unsaved user (i.e. no database entry)
user
=
User
(
username
=
"rusty"
,
email
=
"rusty@fake.edx.org"
)
...
...
@@ -324,11 +369,13 @@ class EnrollInCourseTest(TestCase):
# Unenroll does nothing
CourseEnrollment
.
unenroll
(
user
,
course_id
)
self
.
assert_no_events_were_emitted
()
# Implicit save() happens on new User object when enrolling, so this
# should still work
CourseEnrollment
.
enroll
(
user
,
course_id
)
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id
)
def
test_enrollment_by_email
(
self
):
user
=
User
.
objects
.
create
(
username
=
"jack"
,
email
=
"jack@fake.edx.org"
)
...
...
@@ -336,11 +383,13 @@ class EnrollInCourseTest(TestCase):
CourseEnrollment
.
enroll_by_email
(
"jack@fake.edx.org"
,
course_id
)
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id
)
# This won't throw an exception, even though the user is not found
self
.
assertIsNone
(
CourseEnrollment
.
enroll_by_email
(
"not_jack@fake.edx.org"
,
course_id
)
)
self
.
assert_no_events_were_emitted
()
self
.
assertRaises
(
User
.
DoesNotExist
,
...
...
@@ -349,17 +398,21 @@ class EnrollInCourseTest(TestCase):
course_id
,
ignore_errors
=
False
)
self
.
assert_no_events_were_emitted
()
# Now unenroll them by email
CourseEnrollment
.
unenroll_by_email
(
"jack@fake.edx.org"
,
course_id
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_unenrollment_event_was_emitted
(
user
,
course_id
)
# Harmless second unenroll
CourseEnrollment
.
unenroll_by_email
(
"jack@fake.edx.org"
,
course_id
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_no_events_were_emitted
()
# Unenroll on non-existent user shouldn't throw an error
CourseEnrollment
.
unenroll_by_email
(
"not_jack@fake.edx.org"
,
course_id
)
self
.
assert_no_events_were_emitted
()
def
test_enrollment_multiple_classes
(
self
):
user
=
User
(
username
=
"rusty"
,
email
=
"rusty@fake.edx.org"
)
...
...
@@ -367,15 +420,19 @@ class EnrollInCourseTest(TestCase):
course_id2
=
"MITx/6.003z/2012"
CourseEnrollment
.
enroll
(
user
,
course_id1
)
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id1
)
CourseEnrollment
.
enroll
(
user
,
course_id2
)
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id2
)
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id1
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id2
))
CourseEnrollment
.
unenroll
(
user
,
course_id1
)
self
.
assert_unenrollment_event_was_emitted
(
user
,
course_id1
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id1
))
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id2
))
CourseEnrollment
.
unenroll
(
user
,
course_id2
)
self
.
assert_unenrollment_event_was_emitted
(
user
,
course_id2
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id1
))
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id2
))
...
...
@@ -388,27 +445,33 @@ class EnrollInCourseTest(TestCase):
# (calling CourseEnrollment.enroll() would have)
enrollment
=
CourseEnrollment
.
create_enrollment
(
user
,
course_id
)
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_no_events_were_emitted
()
# Until you explicitly activate it
enrollment
.
activate
()
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id
)
# Activating something that's already active does nothing
enrollment
.
activate
()
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_no_events_were_emitted
()
# Now deactive
enrollment
.
deactivate
()
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_unenrollment_event_was_emitted
(
user
,
course_id
)
# Deactivating something that's already inactive does nothing
enrollment
.
deactivate
()
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_no_events_were_emitted
()
# A deactivated enrollment should be activated if enroll() is called
# for that user/course_id combination
CourseEnrollment
.
enroll
(
user
,
course_id
)
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
user
,
course_id
))
self
.
assert_enrollment_event_was_emitted
(
user
,
course_id
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
...
...
common/djangoapps/track/contexts.py
View file @
f17ab1ec
"""Generates common contexts"""
import
re
import
logging
from
xmodule.course_module
import
CourseDescriptor
COURSE_REGEX
=
re
.
compile
(
r'^.*?/courses/(?P<course_id>(?P<org_id>[^/]+)/[^/]+/[^/]+)'
)
COURSE_REGEX
=
re
.
compile
(
r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)'
)
log
=
logging
.
getLogger
(
__name__
)
def
course_context_from_url
(
url
):
"""
Extracts the course_id from the given `url.`
Extracts the course_id from the given `url` and passes it on to
`course_context_from_course_id()`.
"""
url
=
url
or
''
match
=
COURSE_REGEX
.
match
(
url
)
course_id
=
''
if
match
:
course_id
=
match
.
group
(
'course_id'
)
or
''
return
course_context_from_course_id
(
course_id
)
def
course_context_from_course_id
(
course_id
):
"""
Creates a course context from a `course_id`.
Example Returned Context::
...
...
@@ -18,14 +37,21 @@ def course_context_from_url(url):
}
"""
url
=
url
or
''
course_id
=
course_id
or
''
context
=
{
'course_id'
:
''
,
'course_id'
:
course_id
,
'org_id'
:
''
}
match
=
COURSE_REGEX
.
match
(
url
)
if
match
:
context
.
update
(
match
.
groupdict
())
try
:
location
=
CourseDescriptor
.
id_to_location
(
course_id
)
context
[
'org_id'
]
=
location
.
org
except
ValueError
:
log
.
warning
(
'Unable to parse course_id "{course_id}"'
.
format
(
course_id
=
course_id
),
exc_info
=
True
)
return
context
lms/djangoapps/courseware/features/certificates.feature
View file @
f17ab1ec
...
...
@@ -8,6 +8,7 @@ Feature: LMS.Verified certificates
Given
I am logged in
When
I select the audit track
Then
I should see the course on my dashboard
And
a
"edx.course.enrollment.activated"
server event is emitted
Scenario
:
I
can submit photos to verify my identity
Given
I am logged in
...
...
@@ -36,6 +37,7 @@ Feature: LMS.Verified certificates
Then
I see the course on my dashboard
And
I see that I am on the verified track
And
I do not see the upsell link on my dashboard
And
a
"edx.course.enrollment.activated"
server event is emitted
# Not easily automated
# Scenario: I can re-take photos
...
...
@@ -71,6 +73,7 @@ Feature: LMS.Verified certificates
And
the course has an honor mode
When
I give a reason why I cannot pay
Then
I should see the course on my dashboard
And
a
"edx.course.enrollment.activated"
server event is emitted
Scenario
:
The upsell offer is on the dashboard if I am auditing.
Given
I am logged in
...
...
@@ -91,4 +94,5 @@ Feature: LMS.Verified certificates
And
I navigate to my dashboard
Then
I see the course on my dashboard
And
I see that I am on the verified track
And
a
"edx.course.enrollment.activated"
server event is emitted
lms/djangoapps/courseware/features/registration.feature
View file @
f17ab1ec
...
...
@@ -10,6 +10,7 @@ Feature: LMS.Register for a course
And
I visit the courses page
When
I register for the course
"6.002x"
Then
I should see the course numbered
"6.002x"
in my dashboard
And
a
"edx.course.enrollment.activated"
server event is emitted
Scenario
:
I
can unregister for a course
Given
I am registered for the course
"6.002x"
...
...
@@ -19,3 +20,4 @@ Feature: LMS.Register for a course
Then
I should be on the dashboard page
And
I should see an empty dashboard message
And
I should NOT see the course numbered
"6.002x"
in my dashboard
And
a
"edx.course.enrollment.deactivated"
server event is emitted
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