Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-proctoring
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
OpenEdx
edx-proctoring
Commits
03c92774
Commit
03c92774
authored
Aug 19, 2015
by
Hasnain
Committed by
Chris Dodge
Sep 01, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
send email on proctoring attempt status change
parent
2a7c7e96
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
265 additions
and
15 deletions
+265
-15
edx_proctoring/api.py
+74
-0
edx_proctoring/constants.py
+25
-0
edx_proctoring/models.py
+32
-12
edx_proctoring/templates/emails/proctoring_attempt_status_email.html
+10
-0
edx_proctoring/tests/test_api.py
+117
-1
edx_proctoring/tests/test_services.py
+4
-2
settings.py
+3
-0
No files found.
edx_proctoring/api.py
View file @
03c92774
...
...
@@ -14,7 +14,9 @@ from django.utils.translation import ugettext as _
from
django.conf
import
settings
from
django.template
import
Context
,
loader
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.core.mail.message
import
EmailMessage
from
edx_proctoring
import
constants
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
...
...
@@ -556,6 +558,7 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
# see if the status transition this changes credit requirement status
if
ProctoredExamStudentAttemptStatus
.
needs_credit_status_update
(
to_status
):
# trigger credit workflow, as needed
credit_service
=
get_runtime_service
(
'credit'
)
...
...
@@ -643,9 +646,80 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
exam_attempt_obj
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt_obj
.
save
()
# email will be send when the exam is proctored and not practice exam
# and the status is verified, submitted or rejected
should_send_status_email
=
(
exam_attempt_obj
.
taking_as_proctored
and
not
exam_attempt_obj
.
is_sample_attempt
and
ProctoredExamStudentAttemptStatus
.
needs_status_change_email
(
exam_attempt_obj
.
status
)
)
if
should_send_status_email
:
# trigger credit workflow, as needed
credit_service
=
get_runtime_service
(
'credit'
)
# call service to get course name.
credit_state
=
credit_service
.
get_credit_state
(
exam_attempt_obj
.
user_id
,
exam_attempt_obj
.
proctored_exam
.
course_id
,
return_course_name
=
True
)
send_proctoring_attempt_status_email
(
exam_attempt_obj
,
credit_state
.
get
(
'course_name'
,
_
(
'your course'
))
)
return
exam_attempt_obj
.
id
def
send_proctoring_attempt_status_email
(
exam_attempt_obj
,
course_name
):
"""
Sends an email about change in proctoring attempt status.
"""
course_info_url
=
''
email_template
=
loader
.
get_template
(
'emails/proctoring_attempt_status_email.html'
)
try
:
course_info_url
=
reverse
(
'courseware.views.course_info'
,
args
=
[
exam_attempt_obj
.
proctored_exam
.
course_id
])
except
NoReverseMatch
:
# we are allowing a failure here since we can't guarantee
# that we are running in-proc with the edx-platform LMS
# (for example unit tests)
pass
scheme
=
'https'
if
getattr
(
settings
,
'HTTPS'
,
'on'
)
==
'on'
else
'http'
course_url
=
'{scheme}://{site_name}{course_info_url}'
.
format
(
scheme
=
scheme
,
site_name
=
constants
.
SITE_NAME
,
course_info_url
=
course_info_url
)
body
=
email_template
.
render
(
Context
({
'course_url'
:
course_url
,
'course_name'
:
course_name
,
'exam_name'
:
exam_attempt_obj
.
proctored_exam
.
exam_name
,
'status'
:
ProctoredExamStudentAttemptStatus
.
get_status_alias
(
exam_attempt_obj
.
status
),
'platform'
:
constants
.
PLATFORM_NAME
,
'contact_email'
:
constants
.
CONTACT_EMAIL
})
)
subject
=
(
_
(
'Proctoring Session Results Update for {course_name} {exam_name}'
)
.
format
(
course_name
=
course_name
,
exam_name
=
exam_attempt_obj
.
proctored_exam
.
exam_name
)
)
EmailMessage
(
body
=
body
,
from_email
=
constants
.
FROM_EMAIL
,
to
=
[
exam_attempt_obj
.
user
.
email
],
subject
=
subject
)
.
send
()
def
remove_exam_attempt
(
attempt_id
):
"""
Removes an exam attempt given the attempt id.
...
...
edx_proctoring/constants.py
0 → 100644
View file @
03c92774
"""
Lists of constants that can be used in the edX proctoring
"""
from
django.conf
import
settings
SITE_NAME
=
(
settings
.
PROCTORING_SETTINGS
[
'SITE_NAME'
]
if
'SITE_NAME'
in
settings
.
PROCTORING_SETTINGS
else
settings
.
SITE_NAME
)
PLATFORM_NAME
=
(
settings
.
PROCTORING_SETTINGS
[
'PLATFORM_NAME'
]
if
'PLATFORM_NAME'
in
settings
.
PROCTORING_SETTINGS
else
settings
.
PLATFORM_NAME
)
FROM_EMAIL
=
(
settings
.
PROCTORING_SETTINGS
[
'STATUS_EMAIL_FROM_ADDRESS'
]
if
'STATUS_EMAIL_FROM_ADDRESS'
in
settings
.
PROCTORING_SETTINGS
else
settings
.
DEFAULT_FROM_EMAIL
)
CONTACT_EMAIL
=
(
settings
.
PROCTORING_SETTINGS
[
'CONTACT_EMAIL'
]
if
'CONTACT_EMAIL'
in
settings
.
PROCTORING_SETTINGS
else
settings
.
CONTACT_EMAIL
)
edx_proctoring/models.py
View file @
03c92774
...
...
@@ -6,6 +6,7 @@ from django.db.models import Q
from
django.db.models.signals
import
post_save
,
pre_delete
from
django.dispatch
import
receiver
from
model_utils.models
import
TimeStampedModel
from
django.utils.translation
import
ugettext
as
_
from
django.contrib.auth.models
import
User
from
edx_proctoring.exceptions
import
UserNotFoundException
...
...
@@ -134,6 +135,13 @@ class ProctoredExamStudentAttemptStatus(object):
# the exam is believed to be in error
error
=
'error'
# status alias for sending email
status_alias_mapping
=
{
submitted
:
_
(
'pending'
),
verified
:
_
(
'satisfactory'
),
rejected
:
_
(
'unsatisfactory'
)
}
@classmethod
def
is_completed_status
(
cls
,
status
):
"""
...
...
@@ -141,10 +149,8 @@ class ProctoredExamStudentAttemptStatus(object):
that it cannot go backwards in state
"""
return
status
in
[
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
timed_out
,
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
error
cls
.
declined
,
cls
.
timed_out
,
cls
.
submitted
,
cls
.
verified
,
cls
.
rejected
,
cls
.
not_reviewed
,
cls
.
error
]
@classmethod
...
...
@@ -153,9 +159,7 @@ class ProctoredExamStudentAttemptStatus(object):
Returns a boolean if the passed in status is in an "incomplete" state.
"""
return
status
in
[
ProctoredExamStudentAttemptStatus
.
eligible
,
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
ready_to_start
,
ProctoredExamStudentAttemptStatus
.
started
,
ProctoredExamStudentAttemptStatus
.
ready_to_submit
cls
.
eligible
,
cls
.
created
,
cls
.
ready_to_start
,
cls
.
started
,
cls
.
ready_to_submit
]
@classmethod
...
...
@@ -164,9 +168,8 @@ class ProctoredExamStudentAttemptStatus(object):
Returns a boolean if the passed in to_status calls for an update to the credit requirement status.
"""
return
to_status
in
[
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
error
cls
.
verified
,
cls
.
rejected
,
cls
.
declined
,
cls
.
not_reviewed
,
cls
.
submitted
,
cls
.
error
]
@classmethod
...
...
@@ -176,10 +179,27 @@ class ProctoredExamStudentAttemptStatus(object):
to other attempts.
"""
return
to_status
in
[
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
declined
cls
.
rejected
,
cls
.
declined
]
@classmethod
def
needs_status_change_email
(
cls
,
to_status
):
"""
We need to send out emails for rejected, verified and submitted statuses.
"""
return
to_status
in
[
cls
.
rejected
,
cls
.
submitted
,
cls
.
verified
]
@classmethod
def
get_status_alias
(
cls
,
status
):
"""
Returns status alias used in email
"""
return
cls
.
status_alias_mapping
.
get
(
status
,
''
)
class
ProctoredExamStudentAttemptManager
(
models
.
Manager
):
"""
...
...
edx_proctoring/templates/emails/proctoring_attempt_status_email.html
0 → 100644
View file @
03c92774
{% load i18n %}
{% blocktrans %}
This email is to let you know that the status of your proctoring session review for {{ exam_name }} in
<a
href=
"{{ course_url }}"
>
{{ course_name }}
</a>
is {{ status }}. If you have any questions about proctoring,
contact {{ platform }} support at {{ contact_email }}.
{% endblocktrans %}
\ No newline at end of file
edx_proctoring/tests/test_api.py
View file @
03c92774
# coding=utf-8
# pylint: disable=too-many-lines, invalid-name
"""
...
...
@@ -5,6 +6,7 @@ All tests for the models.py
"""
import
ddt
from
datetime
import
datetime
,
timedelta
from
django.core
import
mail
from
mock
import
patch
import
pytz
from
freezegun
import
freeze_time
...
...
@@ -107,6 +109,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
practice_exam_submitted_msg
=
'You have submitted this practice proctored exam'
self
.
ready_to_start_msg
=
'Your Proctoring Installation and Set Up is Complete'
self
.
practice_exam_failed_msg
=
'There was a problem with your practice proctoring session'
self
.
proctored_exam_email_subject
=
'Proctoring Session Results Update'
self
.
proctored_exam_email_body
=
'the status of your proctoring session review'
set_runtime_service
(
'credit'
,
MockCreditService
())
set_runtime_service
(
'instructor'
,
MockInstructorService
())
...
...
@@ -182,6 +186,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
started_at
=
started_at
if
started_at
else
datetime
.
now
(
pytz
.
UTC
),
status
=
ProctoredExamStudentAttemptStatus
.
started
,
allowed_time_limit_mins
=
10
,
taking_as_proctored
=
is_proctored
,
is_sample_attempt
=
is_sample_attempt
)
...
...
@@ -1472,7 +1477,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Assert that we get the expected status summaries
"""
set_runtime_service
(
'credit'
,
MockCreditService
(
course_name
=
''
))
expected
=
{
'status'
:
ProctoredExamStudentAttemptStatus
.
eligible
,
'short_description'
:
'Ungraded Practice Exam'
,
...
...
@@ -1558,3 +1563,114 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
assertEquals
(
attempt
[
'last_poll_timestamp'
],
now
)
self
.
assertEquals
(
attempt
[
'last_poll_ipaddr'
],
'1.1.1.1'
)
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
)
def
test_send_email
(
self
,
status
):
"""
Assert that email is sent on the following statuses of proctoring attempt.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
credit_state
=
get_runtime_service
(
'credit'
)
.
get_credit_state
(
self
.
user_id
,
self
.
course_id
)
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
status
)
self
.
assertEquals
(
len
(
mail
.
outbox
),
1
)
self
.
assertIn
(
self
.
proctored_exam_email_subject
,
mail
.
outbox
[
0
]
.
subject
)
self
.
assertIn
(
self
.
proctored_exam_email_body
,
mail
.
outbox
[
0
]
.
body
)
self
.
assertIn
(
ProctoredExamStudentAttemptStatus
.
get_status_alias
(
status
),
mail
.
outbox
[
0
]
.
body
)
self
.
assertIn
(
credit_state
[
'course_name'
],
mail
.
outbox
[
0
]
.
body
)
def
test_send_email_unicode
(
self
):
"""
Assert that email can be sent with a unicode course name.
"""
course_name
=
u'अआईउऊऋऌ अआईउऊऋऌ'
set_runtime_service
(
'credit'
,
MockCreditService
(
course_name
=
course_name
))
exam_attempt
=
self
.
_create_started_exam_attempt
()
credit_state
=
get_runtime_service
(
'credit'
)
.
get_credit_state
(
self
.
user_id
,
self
.
course_id
)
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
ProctoredExamStudentAttemptStatus
.
submitted
)
self
.
assertEquals
(
len
(
mail
.
outbox
),
1
)
self
.
assertIn
(
self
.
proctored_exam_email_subject
,
mail
.
outbox
[
0
]
.
subject
)
self
.
assertIn
(
course_name
,
mail
.
outbox
[
0
]
.
subject
)
self
.
assertIn
(
self
.
proctored_exam_email_body
,
mail
.
outbox
[
0
]
.
body
)
self
.
assertIn
(
ProctoredExamStudentAttemptStatus
.
get_status_alias
(
ProctoredExamStudentAttemptStatus
.
submitted
),
mail
.
outbox
[
0
]
.
body
)
self
.
assertIn
(
credit_state
[
'course_name'
],
mail
.
outbox
[
0
]
.
body
)
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
eligible
,
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
ready_to_start
,
ProctoredExamStudentAttemptStatus
.
started
,
ProctoredExamStudentAttemptStatus
.
ready_to_submit
,
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
timed_out
,
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
error
)
@patch.dict
(
'settings.PROCTORING_SETTINGS'
,
{
'ALLOW_TIMED_OUT_STATE'
:
True
})
def
test_not_send_email
(
self
,
status
):
"""
Assert that email is not sent on the following statuses of proctoring attempt.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
status
)
self
.
assertEquals
(
len
(
mail
.
outbox
),
0
)
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
)
def
test_not_send_email_sample_exam
(
self
,
status
):
"""
Assert that email is not sent when there is practice/sample exam
"""
exam_attempt
=
self
.
_create_started_exam_attempt
(
is_sample_attempt
=
True
)
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
status
)
self
.
assertEquals
(
len
(
mail
.
outbox
),
0
)
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
)
def
test_not_send_email_timed_exam
(
self
,
status
):
"""
Assert that email is not sent when exam is timed/not-proctoring
"""
exam_attempt
=
self
.
_create_started_exam_attempt
(
is_proctored
=
False
)
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
status
)
self
.
assertEquals
(
len
(
mail
.
outbox
),
0
)
edx_proctoring/tests/test_services.py
View file @
03c92774
...
...
@@ -17,17 +17,19 @@ class MockCreditService(object):
Simple mock of the Credit Service
"""
def
__init__
(
self
,
enrollment_mode
=
'verified'
,
profile_fullname
=
'Wolfgang von Strucker'
):
def
__init__
(
self
,
enrollment_mode
=
'verified'
,
profile_fullname
=
'Wolfgang von Strucker'
,
course_name
=
'edx demo'
):
"""
Initializer
"""
self
.
status
=
{
'course_name'
:
course_name
,
'enrollment_mode'
:
enrollment_mode
,
'profile_fullname'
:
profile_fullname
,
'credit_requirement_status'
:
[]
}
def
get_credit_state
(
self
,
user_id
,
course_key
):
# pylint: disable=unused-argument
def
get_credit_state
(
self
,
user_id
,
course_key
,
return_course_name
=
False
):
# pylint: disable=unused-argument
"""
Mock implementation
"""
...
...
settings.py
View file @
03c92774
...
...
@@ -91,3 +91,6 @@ PROCTORING_SETTINGS = {
},
'ALLOW_CALLBACK_SIMULATION'
:
False
}
DEFAULT_FROM_EMAIL
=
'no-reply@example.com'
CONTACT_EMAIL
=
'info@edx.org'
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