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
7e368e10
Commit
7e368e10
authored
Apr 19, 2017
by
Eric Fischer
Committed by
GitHub
Apr 19, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14913 from edx/efischer/rekill
Re-kill ICRV block
parents
602b737b
5c26acc1
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
97 additions
and
3045 deletions
+97
-3045
cms/djangoapps/contentstore/tests/test_utils.py
+8
-8
cms/templates/visibility_editor.html
+1
-2
lms/djangoapps/courseware/module_render.py
+1
-2
lms/djangoapps/grades/tests/test_tasks.py
+5
-5
lms/djangoapps/verify_student/admin.py
+1
-45
lms/djangoapps/verify_student/models.py
+17
-302
lms/djangoapps/verify_student/services.py
+0
-122
lms/djangoapps/verify_student/tests/test_models.py
+1
-306
lms/djangoapps/verify_student/tests/test_services.py
+0
-194
lms/djangoapps/verify_student/tests/test_views.py
+2
-655
lms/djangoapps/verify_student/urls.py
+0
-12
lms/djangoapps/verify_student/views.py
+5
-334
openedx/core/djangoapps/credit/api/eligibility.py
+0
-41
openedx/core/djangoapps/credit/partition_schemes.py
+0
-136
openedx/core/djangoapps/credit/signals.py
+0
-20
openedx/core/djangoapps/credit/tasks.py
+3
-83
openedx/core/djangoapps/credit/tests/test_api.py
+17
-17
openedx/core/djangoapps/credit/tests/test_models.py
+3
-3
openedx/core/djangoapps/credit/tests/test_partition.py
+0
-182
openedx/core/djangoapps/credit/tests/test_tasks.py
+4
-112
openedx/core/djangoapps/credit/tests/test_verification_access.py
+0
-275
openedx/core/djangoapps/credit/verification_access.py
+0
-187
openedx/core/djangoapps/user_api/partition_schemes.py
+14
-0
openedx/core/djangolib/model_mixins.py
+14
-0
requirements/edx/github.txt
+0
-1
setup.py
+1
-1
No files found.
cms/djangoapps/contentstore/tests/test_utils.py
View file @
7e368e10
...
...
@@ -535,9 +535,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
),
UserPartition
(
id
=
1
,
name
=
"
Verification
user partition"
,
scheme
=
UserPartition
.
get_scheme
(
"
verification
"
),
description
=
"
Verification
user partition"
,
name
=
"
Completely random
user partition"
,
scheme
=
UserPartition
.
get_scheme
(
"
random
"
),
description
=
"
Random
user partition"
,
groups
=
[
Group
(
id
=
0
,
name
=
"Group C"
),
],
...
...
@@ -562,9 +562,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
),
UserPartition
(
id
=
1
,
name
=
"
Verification
user partition"
,
scheme
=
UserPartition
.
get_scheme
(
"
verification
"
),
description
=
"
Verification
user partition"
,
name
=
"
Completely random
user partition"
,
scheme
=
UserPartition
.
get_scheme
(
"
random
"
),
description
=
"
Random
user partition"
,
groups
=
[
Group
(
id
=
0
,
name
=
"Group C"
),
],
...
...
@@ -572,9 +572,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
])
# Expect that the partition with no groups is excluded from the results
partitions
=
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"
verification
"
])
partitions
=
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"
random
"
])
self
.
assertEqual
(
len
(
partitions
),
1
)
self
.
assertEqual
(
partitions
[
0
][
"scheme"
],
"
verification
"
)
self
.
assertEqual
(
partitions
[
0
][
"scheme"
],
"
random
"
)
def
_set_partitions
(
self
,
partitions
):
"""Set the user partitions of the course descriptor. """
...
...
cms/templates/visibility_editor.html
View file @
7e368e10
...
...
@@ -109,4 +109,4 @@ is_staff_locked = ancestor_has_staff_lock(xblock)
</div>
</div>
</form>
% endif
\ No newline at end of file
% endif
lms/djangoapps/courseware/module_render.py
View file @
7e368e10
...
...
@@ -47,7 +47,7 @@ from lms.djangoapps.grades.signals.signals import SCORE_PUBLISHED
from
lms.djangoapps.lms_xblock.field_data
import
LmsFieldData
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
from
lms.djangoapps.lms_xblock.runtime
import
LmsModuleSystem
from
lms.djangoapps.verify_student.services
import
VerificationService
,
ReverificationService
from
lms.djangoapps.verify_student.services
import
VerificationService
from
openedx.core.djangoapps.bookmarks.services
import
BookmarksService
from
openedx.core.djangoapps.crawlers.models
import
CrawlersConfig
from
openedx.core.djangoapps.credit.services
import
CreditService
...
...
@@ -680,7 +680,6 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
'field-data'
:
field_data
,
'user'
:
DjangoXBlockUserService
(
user
,
user_is_staff
=
user_is_staff
),
'verification'
:
VerificationService
(),
'reverification'
:
ReverificationService
(),
'proctoring'
:
ProctoringService
(),
'milestones'
:
milestones_helpers
.
get_service
(),
'credit'
:
CreditService
(),
...
...
lms/djangoapps/grades/tests/test_tasks.py
View file @
7e368e10
...
...
@@ -156,8 +156,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
28
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
24
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
7
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
8
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
4
,
False
),
)
@ddt.unpack
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
...
...
@@ -170,7 +170,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
28
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
7
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
8
),
)
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
...
...
@@ -216,7 +216,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
11
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
1
0
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
1
1
),
)
@ddt.unpack
def
test_persistent_grades_not_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
@@ -231,7 +231,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
25
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
4
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
)
@ddt.unpack
def
test_persistent_grades_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
lms/djangoapps/verify_student/admin.py
View file @
7e368e10
...
...
@@ -6,12 +6,7 @@ Admin site configurations for verify_student.
from
config_models.admin
import
ConfigurationModelAdmin
from
ratelimitbackend
import
admin
from
lms.djangoapps.verify_student.models
import
(
IcrvStatusEmailsConfiguration
,
SkippedReverification
,
SoftwareSecurePhotoVerification
,
VerificationStatus
,
)
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
@admin.register
(
SoftwareSecurePhotoVerification
)
...
...
@@ -22,42 +17,3 @@ class SoftwareSecurePhotoVerificationAdmin(admin.ModelAdmin):
list_display
=
(
'id'
,
'user'
,
'status'
,
'receipt_id'
,
'submitted_at'
,
'updated_at'
,)
raw_id_fields
=
(
'user'
,
'reviewing_user'
,
'copy_id_photo_from'
,)
search_fields
=
(
'receipt_id'
,
'user__username'
,)
@admin.register
(
VerificationStatus
)
class
VerificationStatusAdmin
(
admin
.
ModelAdmin
):
"""
Admin for the VerificationStatus table.
"""
list_display
=
(
'timestamp'
,
'user'
,
'status'
,
'checkpoint'
)
readonly_fields
=
()
search_fields
=
(
'checkpoint__checkpoint_location'
,
'user__username'
)
raw_id_fields
=
(
'user'
,)
def
get_readonly_fields
(
self
,
request
,
obj
=
None
):
"""When editing an existing record, all fields should be read-only.
VerificationStatus records should be immutable; to change the user's
status, create a new record with the updated status and a more
recent timestamp.
"""
if
obj
:
return
self
.
readonly_fields
+
(
'status'
,
'checkpoint'
,
'user'
,
'response'
,
'error'
)
return
self
.
readonly_fields
@admin.register
(
SkippedReverification
)
class
SkippedReverificationAdmin
(
admin
.
ModelAdmin
):
"""Admin for the SkippedReverification table. """
list_display
=
(
'created_at'
,
'user'
,
'course_id'
,
'checkpoint'
)
raw_id_fields
=
(
'user'
,)
readonly_fields
=
(
'user'
,
'course_id'
)
search_fields
=
(
'user__username'
,
'course_id'
,
'checkpoint__checkpoint_location'
)
def
has_add_permission
(
self
,
request
):
"""Skipped verifications can't be created in Django admin. """
return
False
admin
.
site
.
register
(
IcrvStatusEmailsConfiguration
,
ConfigurationModelAdmin
)
lms/djangoapps/verify_student/models.py
View file @
7e368e10
...
...
@@ -18,17 +18,14 @@ from email.utils import formatdate
import
pytz
import
requests
import
uuid
from
lazy
import
lazy
from
opaque_keys.edx.keys
import
UsageKey
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.urlresolvers
import
reverse
from
django.core.cache
import
cache
from
django.core.files.base
import
ContentFile
from
django.dispatch
import
receiver
from
django.db
import
models
,
transaction
from
django.db
import
models
from
django.utils.functional
import
cached_property
from
django.utils.translation
import
ugettext
as
_
,
ugettext_lazy
...
...
@@ -42,10 +39,9 @@ from lms.djangoapps.verify_student.ssencrypt import (
random_aes_key
,
encrypt_and_encode
,
generate_signed_message
,
rsa_encrypt
)
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangolib.model_mixins
import
DeprecatedModelMixin
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -1103,12 +1099,9 @@ def invalidate_deadline_caches(sender, **kwargs): # pylint: disable=unused-argu
cache
.
delete
(
VerificationDeadline
.
ALL_DEADLINES_CACHE_KEY
)
class
VerificationCheckpoint
(
models
.
Model
):
"""Represents a point at which a user is asked to re-verify his/her
identity.
Each checkpoint is uniquely identified by a
(course_id, checkpoint_location) tuple.
class
VerificationCheckpoint
(
DeprecatedModelMixin
,
models
.
Model
):
# pylint: disable=model-missing-unicode
"""
DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn).
"""
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
checkpoint_location
=
models
.
CharField
(
max_length
=
255
)
...
...
@@ -1118,86 +1111,10 @@ class VerificationCheckpoint(models.Model):
app_label
=
"verify_student"
unique_together
=
(
'course_id'
,
'checkpoint_location'
)
def
__unicode__
(
self
):
"""
Unicode representation of the checkpoint.
"""
return
u"{checkpoint} in {course}"
.
format
(
checkpoint
=
self
.
checkpoint_name
,
course
=
self
.
course_id
)
@lazy
def
checkpoint_name
(
self
):
"""Lazy method for getting checkpoint name of reverification block.
Return location of the checkpoint if no related assessment found in
database.
"""
checkpoint_key
=
UsageKey
.
from_string
(
self
.
checkpoint_location
)
try
:
checkpoint_name
=
modulestore
()
.
get_item
(
checkpoint_key
)
.
related_assessment
except
ItemNotFoundError
:
log
.
warning
(
u"Verification checkpoint block with location '
%
s' and course id '
%
s' "
u"not found in database."
,
self
.
checkpoint_location
,
unicode
(
self
.
course_id
)
)
checkpoint_name
=
self
.
checkpoint_location
return
checkpoint_name
def
add_verification_attempt
(
self
,
verification_attempt
):
"""Add the verification attempt in M2M relation of photo_verification.
Arguments:
verification_attempt(object): SoftwareSecurePhotoVerification object
Returns:
None
"""
self
.
photo_verification
.
add
(
verification_attempt
)
# pylint: disable=no-member
def
get_user_latest_status
(
self
,
user_id
):
"""Get the status of the latest checkpoint attempt of the given user.
Args:
user_id(str): Id of user
Returns:
VerificationStatus object if found any else None
"""
try
:
return
self
.
checkpoint_status
.
filter
(
user_id
=
user_id
)
.
latest
()
except
ObjectDoesNotExist
:
return
None
@classmethod
def
get_or_create_verification_checkpoint
(
cls
,
course_id
,
checkpoint_location
):
"""
Get or create the verification checkpoint for given 'course_id' and
checkpoint name.
Arguments:
course_id (CourseKey): CourseKey
checkpoint_location (str): Verification checkpoint location
Raises:
IntegrityError if create fails due to concurrent create.
Returns:
VerificationCheckpoint object if exists otherwise None
"""
with
transaction
.
atomic
():
checkpoint
,
__
=
cls
.
objects
.
get_or_create
(
course_id
=
course_id
,
checkpoint_location
=
checkpoint_location
)
return
checkpoint
class
VerificationStatus
(
models
.
Model
):
"""This model is an append-only table that represents user status changes
during the verification process.
A verification status represents a user’s progress through the verification
process for a particular checkpoint.
class
VerificationStatus
(
DeprecatedModelMixin
,
models
.
Model
):
# pylint: disable=model-missing-unicode
"""
DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn).
"""
SUBMITTED_STATUS
=
"submitted"
APPROVED_STATUS
=
"approved"
...
...
@@ -1224,172 +1141,24 @@ class VerificationStatus(models.Model):
verbose_name
=
"Verification Status"
verbose_name_plural
=
"Verification Statuses"
@classmethod
def
add_verification_status
(
cls
,
checkpoint
,
user
,
status
):
"""Create new verification status object.
Arguments:
checkpoint(VerificationCheckpoint): VerificationCheckpoint object
user(User): user object
status(str): Status from VERIFICATION_STATUS_CHOICES
Returns:
None
"""
cls
.
objects
.
create
(
checkpoint
=
checkpoint
,
user
=
user
,
status
=
status
)
@classmethod
def
add_status_from_checkpoints
(
cls
,
checkpoints
,
user
,
status
):
"""Create new verification status objects for a user against the given
checkpoints.
Arguments:
checkpoints(list): list of VerificationCheckpoint objects
user(User): user object
status(str): Status from VERIFICATION_STATUS_CHOICES
Returns:
None
"""
for
checkpoint
in
checkpoints
:
cls
.
objects
.
create
(
checkpoint
=
checkpoint
,
user
=
user
,
status
=
status
)
@classmethod
def
get_user_status_at_checkpoint
(
cls
,
user
,
course_key
,
location
):
"""
Get the user's latest status at the checkpoint.
Arguments:
user (User): The user whose status we are retrieving.
course_key (CourseKey): The identifier for the course.
location (UsageKey): The location of the checkpoint in the course.
Returns:
unicode or None
"""
try
:
return
cls
.
objects
.
filter
(
user
=
user
,
checkpoint__course_id
=
course_key
,
checkpoint__checkpoint_location
=
unicode
(
location
),
)
.
latest
()
.
status
except
cls
.
DoesNotExist
:
return
None
@classmethod
def
get_user_attempts
(
cls
,
user_id
,
course_key
,
checkpoint_location
):
"""
Get re-verification attempts against a user for a given 'checkpoint'
and 'course_id'.
Arguments:
user_id (str): User Id string
course_key (str): A CourseKey of a course
checkpoint_location (str): Verification checkpoint location
Returns:
Count of re-verification attempts
"""
return
cls
.
objects
.
filter
(
user_id
=
user_id
,
checkpoint__course_id
=
course_key
,
checkpoint__checkpoint_location
=
checkpoint_location
,
status
=
cls
.
SUBMITTED_STATUS
)
.
count
()
@classmethod
def
get_location_id
(
cls
,
photo_verification
):
"""Get the location ID of reverification XBlock.
Args:
photo_verification(object): SoftwareSecurePhotoVerification object
Return:
Location Id of XBlock if any else empty string
"""
try
:
verification_status
=
cls
.
objects
.
filter
(
checkpoint__photo_verification
=
photo_verification
)
.
latest
()
return
verification_status
.
checkpoint
.
checkpoint_location
except
cls
.
DoesNotExist
:
return
""
@classmethod
def
get_all_checkpoints
(
cls
,
user_id
,
course_key
):
"""Return dict of all the checkpoints with their status.
Args:
user_id(int): Id of user.
course_key(unicode): Unicode of course key
Returns:
dict: {checkpoint:status}
"""
all_checks_points
=
cls
.
objects
.
filter
(
user_id
=
user_id
,
checkpoint__course_id
=
course_key
)
check_points
=
{}
for
check
in
all_checks_points
:
check_points
[
check
.
checkpoint
.
checkpoint_location
]
=
check
.
status
return
check_points
@classmethod
def
cache_key_name
(
cls
,
user_id
,
course_key
):
"""Return the name of the key to use to cache the current configuration
Args:
user_id(int): Id of user.
course_key(unicode): Unicode of course key
Returns:
Unicode cache key
"""
return
u"verification.{}.{}"
.
format
(
user_id
,
unicode
(
course_key
))
@receiver
(
models
.
signals
.
post_save
,
sender
=
VerificationStatus
)
@receiver
(
models
.
signals
.
post_delete
,
sender
=
VerificationStatus
)
def
invalidate_verification_status_cache
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument, invalid-name
"""Invalidate the cache of VerificationStatus model. """
cache_key
=
VerificationStatus
.
cache_key_name
(
instance
.
user
.
id
,
unicode
(
instance
.
checkpoint
.
course_id
)
)
cache
.
delete
(
cache_key
)
# DEPRECATED: this feature has been permanently enabled.
# Once the application code has been updated in production,
# this table can be safely deleted.
class
InCourseReverificationConfiguration
(
ConfigurationModel
):
"""Configure in-course re-verification.
Enable or disable in-course re-verification feature.
When this flag is disabled, the "in-course re-verification" feature
will be disabled.
When the flag is enabled, the "in-course re-verification" feature
will be enabled.
class
InCourseReverificationConfiguration
(
DeprecatedModelMixin
,
ConfigurationModel
):
# pylint: disable=model-missing-unicode
"""
DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn).
"""
pass
class
IcrvStatusEmailsConfiguration
(
ConfigurationModel
):
"""Toggle in-course reverification (ICRV) status emails
Disabled by default. When disabled, ICRV status emails will not be sent.
When enabled, ICRV status emails are sent.
class
IcrvStatusEmailsConfiguration
(
DeprecatedModelMixin
,
ConfigurationModel
):
# pylint: disable=model-missing-unicode
"""
DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn).
"""
pass
class
SkippedReverification
(
models
.
Model
):
"""Model for tracking skipped Reverification of a user against a specific
course.
If a user skipped a Reverification checkpoint for a specific course then in
future that user cannot see the reverification link.
class
SkippedReverification
(
DeprecatedModelMixin
,
models
.
Model
):
# pylint: disable=model-missing-unicode
"""
DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn).
"""
user
=
models
.
ForeignKey
(
User
)
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
...
...
@@ -1399,57 +1168,3 @@ class SkippedReverification(models.Model):
class
Meta
(
object
):
app_label
=
"verify_student"
unique_together
=
((
'user'
,
'course_id'
),)
@classmethod
@transaction.atomic
def
add_skipped_reverification_attempt
(
cls
,
checkpoint
,
user_id
,
course_id
):
"""Create skipped reverification object.
Arguments:
checkpoint(VerificationCheckpoint): VerificationCheckpoint object
user_id(str): User Id of currently logged in user
course_id(CourseKey): CourseKey
Returns:
None
"""
cls
.
objects
.
create
(
checkpoint
=
checkpoint
,
user_id
=
user_id
,
course_id
=
course_id
)
@classmethod
def
check_user_skipped_reverification_exists
(
cls
,
user_id
,
course_id
):
"""Check existence of a user's skipped re-verification attempt for a
specific course.
Arguments:
user_id(str): user id
course_id(CourseKey): CourseKey
Returns:
Boolean
"""
has_skipped
=
cls
.
objects
.
filter
(
user_id
=
user_id
,
course_id
=
course_id
)
.
exists
()
return
has_skipped
@classmethod
def
cache_key_name
(
cls
,
user_id
,
course_key
):
"""Return the name of the key to use to cache the current configuration
Arguments:
user(User): user object
course_key(CourseKey): CourseKey
Returns:
string: cache key name
"""
return
u"skipped_reverification.{}.{}"
.
format
(
user_id
,
unicode
(
course_key
))
@receiver
(
models
.
signals
.
post_save
,
sender
=
SkippedReverification
)
@receiver
(
models
.
signals
.
post_delete
,
sender
=
SkippedReverification
)
def
invalidate_skipped_verification_cache
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument, invalid-name
"""Invalidate the cache of skipped verification model. """
cache_key
=
SkippedReverification
.
cache_key_name
(
instance
.
user
.
id
,
unicode
(
instance
.
course_id
)
)
cache
.
delete
(
cache_key
)
lms/djangoapps/verify_student/services.py
View file @
7e368e10
...
...
@@ -11,7 +11,6 @@ from django.db import IntegrityError
from
opaque_keys.edx.keys
import
CourseKey
from
student.models
import
User
,
CourseEnrollment
from
lms.djangoapps.verify_student.models
import
VerificationCheckpoint
,
VerificationStatus
,
SkippedReverification
from
.models
import
SoftwareSecurePhotoVerification
...
...
@@ -47,124 +46,3 @@ class VerificationService(object):
Returns the URL for a user to verify themselves.
"""
return
reverse
(
'verify_student_reverify'
)
class
ReverificationService
(
object
):
"""
Reverification XBlock service
"""
SKIPPED_STATUS
=
"skipped"
NON_VERIFIED_TRACK
=
"not-verified"
def
get_status
(
self
,
user_id
,
course_id
,
related_assessment_location
):
"""Get verification attempt status against a user for a given
'checkpoint' and 'course_id'.
Args:
user_id (str): User Id string
course_id (str): A string of course id
related_assessment_location (str): Location of Reverification XBlock
Returns: str or None
"""
user
=
User
.
objects
.
get
(
id
=
user_id
)
course_key
=
CourseKey
.
from_string
(
course_id
)
if
not
CourseEnrollment
.
is_enrolled_as_verified
(
user
,
course_key
):
return
self
.
NON_VERIFIED_TRACK
elif
SkippedReverification
.
check_user_skipped_reverification_exists
(
user_id
,
course_key
):
return
self
.
SKIPPED_STATUS
try
:
checkpoint_status
=
VerificationStatus
.
objects
.
filter
(
user_id
=
user_id
,
checkpoint__course_id
=
course_key
,
checkpoint__checkpoint_location
=
related_assessment_location
)
.
latest
()
return
checkpoint_status
.
status
except
ObjectDoesNotExist
:
return
None
def
start_verification
(
self
,
course_id
,
related_assessment_location
):
"""Create re-verification link against a verification checkpoint.
Args:
course_id(str): A string of course id
related_assessment_location(str): Location of Reverification XBlock
Returns:
Re-verification link
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
# Get-or-create the verification checkpoint
VerificationCheckpoint
.
get_or_create_verification_checkpoint
(
course_key
,
related_assessment_location
)
re_verification_link
=
reverse
(
'verify_student_incourse_reverify'
,
args
=
(
unicode
(
course_key
),
unicode
(
related_assessment_location
)
)
)
return
re_verification_link
def
skip_verification
(
self
,
user_id
,
course_id
,
related_assessment_location
):
"""Add skipped verification attempt entry for a user against a given
'checkpoint'.
Args:
user_id(str): User Id string
course_id(str): A string of course_id
related_assessment_location(str): Location of Reverification XBlock
Returns:
None
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
checkpoint
=
VerificationCheckpoint
.
objects
.
get
(
course_id
=
course_key
,
checkpoint_location
=
related_assessment_location
)
user
=
User
.
objects
.
get
(
id
=
user_id
)
# user can skip a reverification attempt only if that user has not already
# skipped an attempt
try
:
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
,
user_id
,
course_key
)
except
IntegrityError
:
log
.
exception
(
"Skipped attempt already exists for user
%
s: with course
%
s:"
,
user_id
,
unicode
(
course_id
))
return
try
:
# Avoid circular import
from
openedx.core.djangoapps.credit.api
import
set_credit_requirement_status
# As a user skips the reverification it declines to fulfill the requirement so
# requirement sets to declined.
set_credit_requirement_status
(
user
,
course_key
,
'reverification'
,
checkpoint
.
checkpoint_location
,
status
=
'declined'
)
except
Exception
as
err
:
# pylint: disable=broad-except
log
.
error
(
"Unable to add credit requirement status for user with id
%
d:
%
s"
,
user_id
,
err
)
def
get_attempts
(
self
,
user_id
,
course_id
,
related_assessment_location
):
"""Get re-verification attempts against a user for a given 'checkpoint'
and 'course_id'.
Args:
user_id(str): User Id string
course_id(str): A string of course id
related_assessment_location(str): Location of Reverification XBlock
Returns:
Number of re-verification attempts of a user
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
return
VerificationStatus
.
get_user_attempts
(
user_id
,
course_key
,
related_assessment_location
)
lms/djangoapps/verify_student/tests/test_models.py
View file @
7e368e10
...
...
@@ -5,7 +5,6 @@ import json
import
boto
import
ddt
from
django.conf
import
settings
from
django.db
import
IntegrityError
from
freezegun
import
freeze_time
import
mock
from
mock
import
patch
...
...
@@ -24,9 +23,7 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from
lms.djangoapps.verify_student.models
import
(
SoftwareSecurePhotoVerification
,
VerificationException
,
VerificationCheckpoint
,
VerificationStatus
,
SkippedReverification
,
VerificationDeadline
VerificationException
,
VerificationDeadline
)
...
...
@@ -522,308 +519,6 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
self
.
assertEqual
(
fourth_result
,
first_result
)
@ddt.ddt
class
VerificationCheckpointTest
(
ModuleStoreTestCase
):
"""Tests for the VerificationCheckpoint model. """
def
setUp
(
self
):
super
(
VerificationCheckpointTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
self
.
checkpoint_midterm
=
u'i4x://{org}/{course}/edx-reverification-block/midterm_uuid'
.
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
)
self
.
checkpoint_final
=
u'i4x://{org}/{course}/edx-reverification-block/final_uuid'
.
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
)
@ddt.data
(
'midterm'
,
'final'
)
def
test_get_or_create_verification_checkpoint
(
self
,
checkpoint
):
"""
Test that a reverification checkpoint is created properly.
"""
checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'
.
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
,
checkpoint
=
checkpoint
)
# create the 'VerificationCheckpoint' checkpoint
verification_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
checkpoint_location
)
self
.
assertEqual
(
VerificationCheckpoint
.
get_or_create_verification_checkpoint
(
self
.
course
.
id
,
checkpoint_location
),
verification_checkpoint
)
def
test_get_or_create_verification_checkpoint_for_not_existing_values
(
self
):
# Retrieving a checkpoint that doesn't yet exist will create it
location
=
u'i4x://edX/DemoX/edx-reverification-block/invalid_location'
checkpoint
=
VerificationCheckpoint
.
get_or_create_verification_checkpoint
(
self
.
course
.
id
,
location
)
self
.
assertIsNot
(
checkpoint
,
None
)
self
.
assertEqual
(
checkpoint
.
course_id
,
self
.
course
.
id
)
self
.
assertEqual
(
checkpoint
.
checkpoint_location
,
location
)
def
test_get_or_create_integrity_error
(
self
):
# Create the checkpoint
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_midterm
,
)
# Simulate that the get-or-create operation raises an IntegrityError.
# This can happen when two processes both try to get-or-create at the same time
# when the database is set to REPEATABLE READ.
# To avoid IntegrityError situations when calling this method, set the view to
# use a READ COMMITTED transaction instead.
with
patch
.
object
(
VerificationCheckpoint
.
objects
,
"get_or_create"
)
as
mock_get_or_create
:
mock_get_or_create
.
side_effect
=
IntegrityError
with
self
.
assertRaises
(
IntegrityError
):
_
=
VerificationCheckpoint
.
get_or_create_verification_checkpoint
(
self
.
course
.
id
,
self
.
checkpoint_midterm
)
def
test_unique_together_constraint
(
self
):
"""
Test the unique together constraint.
"""
# create the VerificationCheckpoint checkpoint
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_midterm
)
# test creating the VerificationCheckpoint checkpoint with same course
# id and checkpoint name
with
self
.
assertRaises
(
IntegrityError
):
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_midterm
)
def
test_add_verification_attempt_software_secure
(
self
):
"""
Test adding Software Secure photo verification attempts for the
reverification checkpoints.
"""
# adding two check points.
first_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_midterm
)
second_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_final
)
# make an attempt for the 'first_checkpoint'
first_checkpoint
.
add_verification_attempt
(
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
))
self
.
assertEqual
(
first_checkpoint
.
photo_verification
.
count
(),
1
)
# make another attempt for the 'first_checkpoint'
first_checkpoint
.
add_verification_attempt
(
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
))
self
.
assertEqual
(
first_checkpoint
.
photo_verification
.
count
(),
2
)
# make new attempt for the 'second_checkpoint'
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
)
second_checkpoint
.
add_verification_attempt
(
attempt
)
self
.
assertEqual
(
second_checkpoint
.
photo_verification
.
count
(),
1
)
# remove the attempt from 'second_checkpoint'
second_checkpoint
.
photo_verification
.
remove
(
attempt
)
self
.
assertEqual
(
second_checkpoint
.
photo_verification
.
count
(),
0
)
@ddt.ddt
class
VerificationStatusTest
(
ModuleStoreTestCase
):
""" Tests for the VerificationStatus model. """
def
setUp
(
self
):
super
(
VerificationStatusTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
self
.
first_checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/first_checkpoint_uuid'
.
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
)
self
.
first_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
first_checkpoint_location
)
self
.
second_checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/second_checkpoint_uuid'
.
\
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
)
self
.
second_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
second_checkpoint_location
)
@ddt.data
(
'submitted'
,
"approved"
,
"denied"
,
"error"
)
def
test_add_verification_status
(
self
,
status
):
""" Adding verification status using the class method. """
# adding verification status
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
first_checkpoint
,
user
=
self
.
user
,
status
=
status
)
# test the status from database
result
=
VerificationStatus
.
objects
.
filter
(
checkpoint
=
self
.
first_checkpoint
)[
0
]
self
.
assertEqual
(
result
.
status
,
status
)
self
.
assertEqual
(
result
.
user
,
self
.
user
)
@ddt.data
(
"approved"
,
"denied"
,
"error"
)
def
test_add_status_from_checkpoints
(
self
,
status
):
"""Test verification status for reverification checkpoints after
submitting software secure photo verification.
"""
# add initial verification status for checkpoints
initial_status
=
"submitted"
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
first_checkpoint
,
user
=
self
.
user
,
status
=
initial_status
)
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
second_checkpoint
,
user
=
self
.
user
,
status
=
initial_status
)
# now add verification status for multiple checkpoint points
VerificationStatus
.
add_status_from_checkpoints
(
checkpoints
=
[
self
.
first_checkpoint
,
self
.
second_checkpoint
],
user
=
self
.
user
,
status
=
status
)
# test that verification status entries with new status have been added
# for both checkpoints
result
=
VerificationStatus
.
objects
.
filter
(
user
=
self
.
user
,
checkpoint
=
self
.
first_checkpoint
)
self
.
assertEqual
(
len
(
result
),
len
(
self
.
first_checkpoint
.
checkpoint_status
.
all
()))
self
.
assertEqual
(
list
(
result
.
values_list
(
'checkpoint__checkpoint_location'
,
flat
=
True
)),
list
(
self
.
first_checkpoint
.
checkpoint_status
.
values_list
(
'checkpoint__checkpoint_location'
,
flat
=
True
))
)
result
=
VerificationStatus
.
objects
.
filter
(
user
=
self
.
user
,
checkpoint
=
self
.
second_checkpoint
)
self
.
assertEqual
(
len
(
result
),
len
(
self
.
second_checkpoint
.
checkpoint_status
.
all
()))
self
.
assertEqual
(
list
(
result
.
values_list
(
'checkpoint__checkpoint_location'
,
flat
=
True
)),
list
(
self
.
second_checkpoint
.
checkpoint_status
.
values_list
(
'checkpoint__checkpoint_location'
,
flat
=
True
))
)
def
test_get_location_id
(
self
):
"""
Getting location id for a specific checkpoint.
"""
# creating software secure attempt against checkpoint
self
.
first_checkpoint
.
add_verification_attempt
(
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
))
# add initial verification status for checkpoint
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
first_checkpoint
,
user
=
self
.
user
,
status
=
'submitted'
,
)
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
filter
(
user
=
self
.
user
)
self
.
assertIsNotNone
(
VerificationStatus
.
get_location_id
(
attempt
))
self
.
assertEqual
(
VerificationStatus
.
get_location_id
(
None
),
''
)
def
test_get_user_attempts
(
self
):
"""
Test adding verification status.
"""
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
first_checkpoint
,
user
=
self
.
user
,
status
=
'submitted'
)
actual_attempts
=
VerificationStatus
.
get_user_attempts
(
self
.
user
.
id
,
self
.
course
.
id
,
self
.
first_checkpoint_location
)
self
.
assertEqual
(
actual_attempts
,
1
)
class
SkippedReverificationTest
(
ModuleStoreTestCase
):
"""
Tests for the SkippedReverification model.
"""
def
setUp
(
self
):
super
(
SkippedReverificationTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
dummy_checkpoint_location
=
u'i4x://edX/DemoX/edx-reverification-block/midterm_uuid'
self
.
checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
dummy_checkpoint_location
)
def
test_add_skipped_attempts
(
self
):
"""
Test 'add_skipped_reverification_attempt' method.
"""
# add verification status
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
checkpoint
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
)
)
# test the status of skipped reverification from database
result
=
SkippedReverification
.
objects
.
filter
(
course_id
=
self
.
course
.
id
)[
0
]
self
.
assertEqual
(
result
.
checkpoint
,
self
.
checkpoint
)
self
.
assertEqual
(
result
.
user
,
self
.
user
)
self
.
assertEqual
(
result
.
course_id
,
self
.
course
.
id
)
def
test_unique_constraint
(
self
):
"""Test that adding skipped re-verification with same user and course
id will raise 'IntegrityError' exception.
"""
# add verification object
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
checkpoint
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
)
)
with
self
.
assertRaises
(
IntegrityError
):
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
checkpoint
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
)
)
# create skipped attempt for different user
user2
=
UserFactory
.
create
()
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
checkpoint
,
user_id
=
user2
.
id
,
course_id
=
unicode
(
self
.
course
.
id
)
)
# test the status of skipped reverification from database
result
=
SkippedReverification
.
objects
.
filter
(
user
=
user2
)[
0
]
self
.
assertEqual
(
result
.
checkpoint
,
self
.
checkpoint
)
self
.
assertEqual
(
result
.
user
,
user2
)
self
.
assertEqual
(
result
.
course_id
,
self
.
course
.
id
)
def
test_check_user_skipped_reverification_exists
(
self
):
"""
Test the 'check_user_skipped_reverification_exists' method's response.
"""
# add verification status
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
checkpoint
,
user_id
=
self
.
user
.
id
,
course_id
=
unicode
(
self
.
course
.
id
)
)
self
.
assertTrue
(
SkippedReverification
.
check_user_skipped_reverification_exists
(
user_id
=
self
.
user
.
id
,
course_id
=
self
.
course
.
id
)
)
user2
=
UserFactory
.
create
()
self
.
assertFalse
(
SkippedReverification
.
check_user_skipped_reverification_exists
(
user_id
=
user2
.
id
,
course_id
=
self
.
course
.
id
)
)
class
VerificationDeadlineTest
(
CacheIsolationTestCase
):
"""
Tests for the VerificationDeadline model.
...
...
lms/djangoapps/verify_student/tests/test_services.py
deleted
100644 → 0
View file @
602b737b
"""
Tests of re-verification service.
"""
import
ddt
from
opaque_keys.edx.keys
import
CourseKey
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
lms.djangoapps.verify_student.models
import
VerificationCheckpoint
,
VerificationStatus
,
SkippedReverification
from
lms.djangoapps.verify_student.services
import
ReverificationService
from
openedx.core.djangoapps.credit.api
import
get_credit_requirement_status
,
set_credit_requirements
from
openedx.core.djangoapps.credit.models
import
CreditCourse
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
@ddt.ddt
class
TestReverificationService
(
ModuleStoreTestCase
):
"""
Tests for the re-verification service.
"""
def
setUp
(
self
):
super
(
TestReverificationService
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
(
username
=
"rusty"
,
password
=
"test"
)
self
.
course
=
CourseFactory
.
create
(
org
=
'Robot'
,
number
=
'999'
,
display_name
=
'Test Course'
)
self
.
course_id
=
self
.
course
.
id
CourseModeFactory
.
create
(
mode_slug
=
"verified"
,
course_id
=
self
.
course_id
,
min_price
=
100
,
)
self
.
course_key
=
CourseKey
.
from_string
(
unicode
(
self
.
course_id
))
self
.
item
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
self
.
final_checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/final_uuid'
.
format
(
org
=
self
.
course_id
.
org
,
course
=
self
.
course_id
.
course
)
# Enroll in a verified mode
self
.
enrollment
=
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_id
,
mode
=
CourseMode
.
VERIFIED
)
@ddt.data
(
'final'
,
'midterm'
)
def
test_start_verification
(
self
,
checkpoint_name
):
"""Test the 'start_verification' service method.
Check that if a reverification checkpoint exists for a specific course
then 'start_verification' method returns that checkpoint otherwise it
creates that checkpoint.
"""
reverification_service
=
ReverificationService
()
checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'
.
format
(
org
=
self
.
course_id
.
org
,
course
=
self
.
course_id
.
course
,
checkpoint
=
checkpoint_name
)
expected_url
=
(
'/verify_student/reverify'
'/{course_key}'
'/{checkpoint_location}/'
)
.
format
(
course_key
=
unicode
(
self
.
course_id
),
checkpoint_location
=
checkpoint_location
)
self
.
assertEqual
(
reverification_service
.
start_verification
(
unicode
(
self
.
course_id
),
checkpoint_location
),
expected_url
)
def
test_get_status
(
self
):
"""Test the verification statuses of a user for a given 'checkpoint'
and 'course_id'.
"""
reverification_service
=
ReverificationService
()
self
.
assertIsNone
(
reverification_service
.
get_status
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
)
)
checkpoint_obj
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
unicode
(
self
.
course_id
),
checkpoint_location
=
self
.
final_checkpoint_location
)
VerificationStatus
.
objects
.
create
(
checkpoint
=
checkpoint_obj
,
user
=
self
.
user
,
status
=
'submitted'
)
self
.
assertEqual
(
reverification_service
.
get_status
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
),
'submitted'
)
VerificationStatus
.
objects
.
create
(
checkpoint
=
checkpoint_obj
,
user
=
self
.
user
,
status
=
'approved'
)
self
.
assertEqual
(
reverification_service
.
get_status
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
),
'approved'
)
def
test_skip_verification
(
self
):
"""
Test adding skip attempt of a user for a reverification checkpoint.
"""
reverification_service
=
ReverificationService
()
VerificationCheckpoint
.
objects
.
create
(
course_id
=
unicode
(
self
.
course_id
),
checkpoint_location
=
self
.
final_checkpoint_location
)
reverification_service
.
skip_verification
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
)
self
.
assertEqual
(
SkippedReverification
.
objects
.
filter
(
user
=
self
.
user
,
course_id
=
self
.
course_id
)
.
count
(),
1
)
# now test that a user can have only one entry for a skipped
# reverification for a course
reverification_service
.
skip_verification
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
)
self
.
assertEqual
(
SkippedReverification
.
objects
.
filter
(
user
=
self
.
user
,
course_id
=
self
.
course_id
)
.
count
(),
1
)
# testing service for skipped attempt.
self
.
assertEqual
(
reverification_service
.
get_status
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
),
'skipped'
)
@ddt.data
(
*
CourseMode
.
CREDIT_ELIGIBLE_MODES
)
def
test_declined_verification_on_skip
(
self
,
mode
):
"""Test that status with value 'declined' is added in credit
requirement status model when a user skip's an ICRV.
"""
reverification_service
=
ReverificationService
()
checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
unicode
(
self
.
course_id
),
checkpoint_location
=
self
.
final_checkpoint_location
)
# Create credit course and set credit requirements.
CreditCourse
.
objects
.
create
(
course_key
=
self
.
course_key
,
enabled
=
True
)
self
.
enrollment
.
update_enrollment
(
mode
=
mode
)
set_credit_requirements
(
self
.
course_key
,
[
{
"namespace"
:
"reverification"
,
"name"
:
checkpoint
.
checkpoint_location
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
]
)
reverification_service
.
skip_verification
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
)
requirement_status
=
get_credit_requirement_status
(
self
.
course_key
,
self
.
user
.
username
,
'reverification'
,
checkpoint
.
checkpoint_location
)
self
.
assertEqual
(
SkippedReverification
.
objects
.
filter
(
user
=
self
.
user
,
course_id
=
self
.
course_id
)
.
count
(),
1
)
self
.
assertEqual
(
len
(
requirement_status
),
1
)
self
.
assertEqual
(
requirement_status
[
0
]
.
get
(
'name'
),
checkpoint
.
checkpoint_location
)
self
.
assertEqual
(
requirement_status
[
0
]
.
get
(
'status'
),
'declined'
)
def
test_get_attempts
(
self
):
"""Check verification attempts count against a user for a given
'checkpoint' and 'course_id'.
"""
reverification_service
=
ReverificationService
()
course_id
=
unicode
(
self
.
course_id
)
self
.
assertEqual
(
reverification_service
.
get_attempts
(
self
.
user
.
id
,
course_id
,
self
.
final_checkpoint_location
),
0
)
# now create a checkpoint and add user's entry against it then test
# that the 'get_attempts' service method returns correct count
checkpoint_obj
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
course_id
,
checkpoint_location
=
self
.
final_checkpoint_location
)
VerificationStatus
.
objects
.
create
(
checkpoint
=
checkpoint_obj
,
user
=
self
.
user
,
status
=
'submitted'
)
self
.
assertEqual
(
reverification_service
.
get_attempts
(
self
.
user
.
id
,
course_id
,
self
.
final_checkpoint_location
),
1
)
def
test_not_in_verified_track
(
self
):
# No longer enrolled in a verified track
self
.
enrollment
.
update_enrollment
(
mode
=
CourseMode
.
HONOR
)
# Should be marked as "skipped" (opted out)
service
=
ReverificationService
()
status
=
service
.
get_status
(
self
.
user
.
id
,
unicode
(
self
.
course_id
),
self
.
final_checkpoint_location
)
self
.
assertEqual
(
status
,
service
.
NON_VERIFIED_TRACK
)
lms/djangoapps/verify_student/tests/test_views.py
View file @
7e368e10
...
...
@@ -16,7 +16,7 @@ import boto
import
moto
import
pytz
from
bs4
import
BeautifulSoup
from
mock
import
patch
,
Mock
,
ANY
from
mock
import
patch
,
Mock
import
requests
from
django.conf
import
settings
...
...
@@ -25,15 +25,12 @@ from django.core import mail
from
django.test
import
TestCase
from
django.test.client
import
Client
,
RequestFactory
from
django.test.utils
import
override_settings
from
django.utils
import
timezone
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.keys
import
UsageKey
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
courseware.url_helpers
import
get_redirect_url
from
common.test.utils
import
XssTestMixin
from
commerce.models
import
CommerceConfiguration
from
commerce.tests
import
TEST_PAYMENT_DATA
,
TEST_API_URL
,
TEST_PUBLIC_URL_ROOT
...
...
@@ -43,22 +40,17 @@ from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_t
from
shoppingcart.models
import
Order
,
CertificateItem
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.models
import
CourseEnrollment
from
util.date_utils
import
get_default_time_display
from
util.testing
import
UrlResetMixin
from
lms.djangoapps.verify_student.views
import
(
checkout_with_ecommerce_service
,
render_to_response
,
PayAndVerifyView
,
_compose_message_reverification_email
)
from
lms.djangoapps.verify_student.models
import
(
VerificationDeadline
,
SoftwareSecurePhotoVerification
,
VerificationCheckpoint
,
VerificationStatus
,
IcrvStatusEmailsConfiguration
,
)
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.factories
import
check_mongo_calls
def
mock_render_to_response
(
*
args
,
**
kwargs
):
...
...
@@ -1840,159 +1832,6 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
)
self
.
assertIn
(
'Result Unknown not understood'
,
response
.
content
)
@mock.patch
(
'lms.djangoapps.verify_student.ssencrypt.has_valid_signature'
,
mock
.
Mock
(
side_effect
=
mocked_has_valid_signature
)
)
def
test_in_course_reverify_disabled
(
self
):
"""
Test for verification passed.
"""
data
=
{
"EdX-ID"
:
self
.
receipt_id
,
"Result"
:
"PASS"
,
"Reason"
:
""
,
"MessageType"
:
"You have been verified."
}
json_data
=
json
.
dumps
(
data
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_results_callback'
),
data
=
json_data
,
content_type
=
'application/json'
,
HTTP_AUTHORIZATION
=
'test BBBBBBBBBBBBBBBBBBBB:testing'
,
HTTP_DATE
=
'testdate'
)
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
get
(
receipt_id
=
self
.
receipt_id
)
self
.
assertEqual
(
attempt
.
status
,
u'approved'
)
self
.
assertEquals
(
response
.
content
,
'OK!'
)
# Verify that photo submission confirmation email was sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
0
)
user_status
=
VerificationStatus
.
objects
.
filter
(
user
=
self
.
user
)
.
count
()
self
.
assertEqual
(
user_status
,
0
)
@mock.patch
(
'lms.djangoapps.verify_student.ssencrypt.has_valid_signature'
,
mock
.
Mock
(
side_effect
=
mocked_has_valid_signature
)
)
def
test_pass_in_course_reverify_result
(
self
):
"""
Test for verification passed.
"""
# Verify that ICRV status email was sent when config is enabled
IcrvStatusEmailsConfiguration
.
objects
.
create
(
enabled
=
True
)
self
.
create_reverification_xblock
()
data
=
{
"EdX-ID"
:
self
.
receipt_id
,
"Result"
:
"PASS"
,
"Reason"
:
""
,
"MessageType"
:
"You have been verified."
}
json_data
=
json
.
dumps
(
data
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_results_callback'
),
data
=
json_data
,
content_type
=
'application/json'
,
HTTP_AUTHORIZATION
=
'test BBBBBBBBBBBBBBBBBBBB:testing'
,
HTTP_DATE
=
'testdate'
)
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
get
(
receipt_id
=
self
.
receipt_id
)
self
.
assertEqual
(
attempt
.
status
,
u'approved'
)
self
.
assertEquals
(
response
.
content
,
'OK!'
)
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
self
.
assertEqual
(
"Re-verification Status"
,
mail
.
outbox
[
0
]
.
subject
)
@mock.patch
(
'verify_student.ssencrypt.has_valid_signature'
,
mock
.
Mock
(
side_effect
=
mocked_has_valid_signature
))
def
test_icrv_status_email_with_disable_config
(
self
):
"""
Verify that photo re-verification status email was not sent when config is disable
"""
IcrvStatusEmailsConfiguration
.
objects
.
create
(
enabled
=
False
)
self
.
create_reverification_xblock
()
data
=
{
"EdX-ID"
:
self
.
receipt_id
,
"Result"
:
"PASS"
,
"Reason"
:
""
,
"MessageType"
:
"You have been verified."
}
json_data
=
json
.
dumps
(
data
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_results_callback'
),
data
=
json_data
,
content_type
=
'application/json'
,
HTTP_AUTHORIZATION
=
'test BBBBBBBBBBBBBBBBBBBB:testing'
,
HTTP_DATE
=
'testdate'
)
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
get
(
receipt_id
=
self
.
receipt_id
)
self
.
assertEqual
(
attempt
.
status
,
u'approved'
)
self
.
assertEquals
(
response
.
content
,
'OK!'
)
self
.
assertEqual
(
len
(
mail
.
outbox
),
0
)
@mock.patch
(
'lms.djangoapps.verify_student.views._send_email'
)
@mock.patch
(
'lms.djangoapps.verify_student.ssencrypt.has_valid_signature'
,
mock
.
Mock
(
side_effect
=
mocked_has_valid_signature
)
)
def
test_reverification_on_callback
(
self
,
mock_send_email
):
"""
Test software secure callback flow for re-verification.
"""
IcrvStatusEmailsConfiguration
.
objects
.
create
(
enabled
=
True
)
# Create the 'edx-reverification-block' in course tree
self
.
create_reverification_xblock
()
# create dummy data for software secure photo verification result callback
data
=
{
"EdX-ID"
:
self
.
receipt_id
,
"Result"
:
"PASS"
,
"Reason"
:
""
,
"MessageType"
:
"You have been verified."
}
json_data
=
json
.
dumps
(
data
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_results_callback'
),
data
=
json_data
,
content_type
=
'application/json'
,
HTTP_AUTHORIZATION
=
'test BBBBBBBBBBBBBBBBBBBB:testing'
,
HTTP_DATE
=
'testdate'
)
self
.
assertEqual
(
response
.
content
,
'OK!'
)
# now check that '_send_email' method is called on result callback
# with required parameters
subject
=
"Re-verification Status"
mock_send_email
.
assert_called_once_with
(
self
.
user
.
id
,
subject
,
ANY
)
def
create_reverification_xblock
(
self
):
"""
Create the reverification XBlock.
"""
# Create the 'edx-reverification-block' in course tree
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
subsection
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'sequential'
,
display_name
=
'Test Subsection'
)
vertical
=
ItemFactory
.
create
(
parent
=
subsection
,
category
=
'vertical'
,
display_name
=
'Test Unit'
)
reverification
=
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'edx-reverification-block'
,
display_name
=
'Test Verification Block'
)
# Create checkpoint
checkpoint
=
VerificationCheckpoint
(
course_id
=
self
.
course_id
,
checkpoint_location
=
reverification
.
location
)
checkpoint
.
save
()
# Add a re-verification attempt
checkpoint
.
add_verification_attempt
(
self
.
attempt
)
# Add a re-verification attempt status for the user
VerificationStatus
.
add_verification_status
(
checkpoint
,
self
.
user
,
"submitted"
)
@attr
(
shard
=
2
)
class
TestReverifyView
(
TestCase
):
...
...
@@ -2104,495 +1943,3 @@ class TestReverifyView(TestCase):
"""
response
=
self
.
_get_reverify_page
()
self
.
assertContains
(
response
,
"reverify-blocked"
)
@attr
(
shard
=
2
)
class
TestInCourseReverifyView
(
ModuleStoreTestCase
):
"""
Tests for the incourse reverification views.
"""
IMAGE_DATA
=
"abcd,1234"
def
build_course
(
self
):
"""
Build up a course tree with a Reverificaiton xBlock.
"""
self
.
course_key
=
SlashSeparatedCourseKey
(
"Robot"
,
"999"
,
"Test_Course"
)
self
.
course
=
CourseFactory
.
create
(
org
=
'Robot'
,
number
=
'999'
,
display_name
=
'Test Course'
)
# Create the course modes
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
min_price
=
0
if
mode
in
[
"honor"
,
"audit"
]
else
1
CourseModeFactory
.
create
(
mode_slug
=
mode
,
course_id
=
self
.
course_key
,
min_price
=
min_price
)
# Create the 'edx-reverification-block' in course tree
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
subsection
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'sequential'
,
display_name
=
'Test Subsection'
)
vertical
=
ItemFactory
.
create
(
parent
=
subsection
,
category
=
'vertical'
,
display_name
=
'Test Unit'
)
self
.
reverification
=
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'edx-reverification-block'
,
display_name
=
'Test Verification Block'
)
self
.
section_location
=
section
.
location
self
.
subsection_location
=
subsection
.
location
self
.
vertical_location
=
vertical
.
location
self
.
reverification_location
=
unicode
(
self
.
reverification
.
location
)
self
.
reverification_assessment
=
self
.
reverification
.
related_assessment
def
setUp
(
self
):
super
(
TestInCourseReverifyView
,
self
)
.
setUp
()
self
.
build_course
()
self
.
user
=
UserFactory
.
create
(
username
=
"rusty"
,
password
=
"test"
)
self
.
client
.
login
(
username
=
"rusty"
,
password
=
"test"
)
# Enroll the user in the default mode (honor) to emulate
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
mode
=
"verified"
)
# mocking and patching for bi events
analytics_patcher
=
patch
(
'lms.djangoapps.verify_student.views.analytics'
)
self
.
mock_tracker
=
analytics_patcher
.
start
()
self
.
addCleanup
(
analytics_patcher
.
stop
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_invalid_checkpoint_get
(
self
):
# Retrieve a checkpoint that doesn't yet exist
response
=
self
.
client
.
get
(
self
.
_get_url
(
self
.
course_key
,
"invalid_checkpoint"
))
self
.
assertEqual
(
response
.
status_code
,
404
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_initial_redirect_get
(
self
):
self
.
_create_checkpoint
()
response
=
self
.
client
.
get
(
self
.
_get_url
(
self
.
course_key
,
self
.
reverification_location
))
url
=
reverse
(
'verify_student_verify_now'
,
kwargs
=
{
"course_id"
:
unicode
(
self
.
course_key
)})
url
+=
u"?{params}"
.
format
(
params
=
urllib
.
urlencode
({
"checkpoint"
:
self
.
reverification_location
}))
self
.
assertRedirects
(
response
,
url
)
@override_settings
(
LMS_SEGMENT_KEY
=
"foobar"
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_get
(
self
):
"""
Test incourse reverification.
"""
self
.
_create_checkpoint
()
self
.
_create_initial_verification
()
response
=
self
.
client
.
get
(
self
.
_get_url
(
self
.
course_key
,
self
.
reverification_location
))
self
.
assertEquals
(
response
.
status_code
,
200
)
# verify that Google Analytics event fires after successfully
# submitting the photo verification
self
.
mock_tracker
.
track
.
assert_called_once_with
(
# pylint: disable=no-member
self
.
user
.
id
,
'edx.bi.reverify.started'
,
{
'category'
:
"verification"
,
'label'
:
unicode
(
self
.
course_key
),
'checkpoint'
:
self
.
reverification_assessment
},
context
=
{
'ip'
:
'127.0.0.1'
,
'Google Analytics'
:
{
'clientId'
:
None
}
}
)
self
.
mock_tracker
.
reset_mock
()
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_checkpoint_post
(
self
):
"""Verify that POST requests including an invalid checkpoint location
results in a 400 response.
"""
response
=
self
.
_submit_photos
(
self
.
course_key
,
self
.
reverification_location
,
self
.
IMAGE_DATA
)
self
.
assertEquals
(
response
.
status_code
,
400
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_id_required_if_no_initial_verification
(
self
):
self
.
_create_checkpoint
()
# Since the user has no initial verification and we're not sending the ID photo,
# we should expect a 400 bad request
response
=
self
.
_submit_photos
(
self
.
course_key
,
self
.
reverification_location
,
self
.
IMAGE_DATA
)
self
.
assertEqual
(
response
.
status_code
,
400
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_index_error_post
(
self
):
self
.
_create_checkpoint
()
self
.
_create_initial_verification
()
response
=
self
.
_submit_photos
(
self
.
course_key
,
self
.
reverification_location
,
""
)
self
.
assertEqual
(
response
.
status_code
,
400
)
@override_settings
(
LMS_SEGMENT_KEY
=
"foobar"
)
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_incourse_reverify_post
(
self
):
self
.
_create_checkpoint
()
self
.
_create_initial_verification
()
response
=
self
.
_submit_photos
(
self
.
course_key
,
self
.
reverification_location
,
self
.
IMAGE_DATA
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Check that the checkpoint status has been updated
status
=
VerificationStatus
.
get_user_status_at_checkpoint
(
self
.
user
,
self
.
course_key
,
self
.
reverification_location
)
self
.
assertEqual
(
status
,
"submitted"
)
# Test that Google Analytics event fires after successfully submitting
# photo verification
self
.
mock_tracker
.
track
.
assert_called_once_with
(
# pylint: disable=no-member
self
.
user
.
id
,
'edx.bi.reverify.submitted'
,
{
'category'
:
"verification"
,
'label'
:
unicode
(
self
.
course_key
),
'checkpoint'
:
self
.
reverification_assessment
},
context
=
{
'ip'
:
'127.0.0.1'
,
'Google Analytics'
:
{
'clientId'
:
None
}
}
)
self
.
mock_tracker
.
reset_mock
()
def
_create_checkpoint
(
self
):
"""
Helper method for creating a reverification checkpoint.
"""
checkpoint
=
VerificationCheckpoint
(
course_id
=
self
.
course_key
,
checkpoint_location
=
self
.
reverification_location
)
checkpoint
.
save
()
def
_create_initial_verification
(
self
):
"""
Helper method for initial verification.
"""
attempt
=
SoftwareSecurePhotoVerification
(
user
=
self
.
user
,
photo_id_key
=
"dummy_photo_id_key"
)
attempt
.
mark_ready
()
attempt
.
save
()
attempt
.
submit
()
def
_get_url
(
self
,
course_key
,
checkpoint_location
):
"""
Construct the reverification url.
Arguments:
course_key (unicode): The ID of the course
checkpoint_location (str): Location of verification checkpoint
Returns:
url
"""
return
reverse
(
'verify_student_incourse_reverify'
,
kwargs
=
{
"course_id"
:
unicode
(
course_key
),
"usage_id"
:
checkpoint_location
}
)
def
_submit_photos
(
self
,
course_key
,
checkpoint_location
,
face_image_data
):
""" Submit photos for verification. """
url
=
reverse
(
"verify_student_submit_photos"
)
data
=
{
"course_key"
:
unicode
(
course_key
),
"checkpoint"
:
checkpoint_location
,
"face_image"
:
face_image_data
,
}
return
self
.
client
.
post
(
url
,
data
)
@attr
(
shard
=
2
)
class
TestEmailMessageWithCustomICRVBlock
(
ModuleStoreTestCase
):
"""
Test email sending on re-verification
"""
def
build_course
(
self
):
"""
Build up a course tree with a Reverificaiton xBlock.
"""
self
.
course_key
=
SlashSeparatedCourseKey
(
"Robot"
,
"999"
,
"Test_Course"
)
self
.
course
=
CourseFactory
.
create
(
org
=
'Robot'
,
number
=
'999'
,
display_name
=
'Test Course'
)
self
.
due_date
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
20
)
self
.
allowed_attempts
=
1
# Create the course modes
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
min_price
=
0
if
mode
in
[
"honor"
,
"audit"
]
else
1
CourseModeFactory
.
create
(
mode_slug
=
mode
,
course_id
=
self
.
course_key
,
min_price
=
min_price
)
# Create the 'edx-reverification-block' in course tree
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
subsection
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'sequential'
,
display_name
=
'Test Subsection'
)
vertical
=
ItemFactory
.
create
(
parent
=
subsection
,
category
=
'vertical'
,
display_name
=
'Test Unit'
)
self
.
reverification
=
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'edx-reverification-block'
,
display_name
=
'Test Verification Block'
,
metadata
=
{
'attempts'
:
self
.
allowed_attempts
,
'due'
:
self
.
due_date
}
)
self
.
section_location
=
section
.
location
self
.
subsection_location
=
subsection
.
location
self
.
vertical_location
=
vertical
.
location
self
.
reverification_location
=
unicode
(
self
.
reverification
.
location
)
self
.
assessment
=
self
.
reverification
.
related_assessment
self
.
re_verification_link
=
reverse
(
'verify_student_incourse_reverify'
,
args
=
(
unicode
(
self
.
course_key
),
self
.
reverification_location
)
)
def
setUp
(
self
):
"""
Setup method for testing photo verification email messages.
"""
super
(
TestEmailMessageWithCustomICRVBlock
,
self
)
.
setUp
()
self
.
build_course
()
self
.
check_point
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
reverification_location
)
self
.
check_point
.
add_verification_attempt
(
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
))
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
check_point
,
user
=
self
.
user
,
status
=
'submitted'
)
self
.
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
filter
(
user
=
self
.
user
)
location_id
=
VerificationStatus
.
get_location_id
(
self
.
attempt
)
usage_key
=
UsageKey
.
from_string
(
location_id
)
redirect_url
=
get_redirect_url
(
self
.
course_key
,
usage_key
.
replace
(
course_key
=
self
.
course_key
))
self
.
request
=
RequestFactory
()
.
get
(
'/url'
)
self
.
course_link
=
self
.
request
.
build_absolute_uri
(
redirect_url
)
def
test_approved_email_message
(
self
):
subject
,
body
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"approved"
,
self
.
request
)
self
.
assertIn
(
"We have successfully verified your identity for the {assessment} "
"assessment in the {course_name} course."
.
format
(
assessment
=
self
.
assessment
,
course_name
=
self
.
course
.
display_name_with_default_escaped
),
body
)
self
.
check_courseware_link_exists
(
body
)
self
.
assertIn
(
"Re-verification Status"
,
subject
)
def
test_denied_email_message_with_valid_due_date_and_attempts_allowed
(
self
):
subject
,
body
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"denied"
,
self
.
request
)
self
.
assertIn
(
"We could not verify your identity for the {assessment} assessment "
"in the {course_name} course. You have used "
"{used_attempts} out of {allowed_attempts} attempts to "
"verify your identity"
.
format
(
course_name
=
self
.
course
.
display_name_with_default_escaped
,
assessment
=
self
.
assessment
,
used_attempts
=
1
,
allowed_attempts
=
self
.
allowed_attempts
+
1
),
body
)
self
.
assertIn
(
"You must verify your identity before the assessment "
"closes on {due_date}"
.
format
(
due_date
=
get_default_time_display
(
self
.
due_date
)
),
body
)
reverify_link
=
self
.
request
.
build_absolute_uri
(
self
.
re_verification_link
)
self
.
assertIn
(
"To try to verify your identity again, select the following link:"
,
body
)
self
.
assertIn
(
reverify_link
,
body
)
self
.
assertIn
(
"Re-verification Status"
,
subject
)
def
test_denied_email_message_with_due_date_and_no_attempts
(
self
):
""" Denied email message if due date is still open but user has no
attempts available.
"""
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
check_point
,
user
=
self
.
user
,
status
=
'submitted'
)
__
,
body
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"denied"
,
self
.
request
)
self
.
assertIn
(
"We could not verify your identity for the {assessment} assessment "
"in the {course_name} course. You have used "
"{used_attempts} out of {allowed_attempts} attempts to "
"verify your identity, and verification is no longer "
"possible"
.
format
(
course_name
=
self
.
course
.
display_name_with_default_escaped
,
assessment
=
self
.
assessment
,
used_attempts
=
2
,
allowed_attempts
=
self
.
allowed_attempts
+
1
),
body
)
self
.
check_courseware_link_exists
(
body
)
def
test_denied_email_message_with_close_verification_dates
(
self
):
# Due date given and expired
return_value
=
datetime
.
now
(
tz
=
pytz
.
UTC
)
+
timedelta
(
days
=
22
)
with
patch
.
object
(
timezone
,
'now'
,
return_value
=
return_value
):
__
,
body
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"denied"
,
self
.
request
)
self
.
assertIn
(
"We could not verify your identity for the {assessment} assessment "
"in the {course_name} course. You have used "
"{used_attempts} out of {allowed_attempts} attempts to "
"verify your identity, and verification is no longer "
"possible"
.
format
(
course_name
=
self
.
course
.
display_name_with_default_escaped
,
assessment
=
self
.
assessment
,
used_attempts
=
1
,
allowed_attempts
=
self
.
allowed_attempts
+
1
),
body
)
def
test_check_num_queries
(
self
):
# Get the re-verification block to check the call made
with
check_mongo_calls
(
1
):
ver_block
=
modulestore
()
.
get_item
(
self
.
reverification
.
location
)
# Expect that the verification block is fetched
self
.
assertIsNotNone
(
ver_block
)
def
check_courseware_link_exists
(
self
,
body
):
"""Checking courseware url and signature information of EDX"""
self
.
assertIn
(
"To go to the courseware, select the following link:"
,
body
)
self
.
assertIn
(
"{course_link}"
.
format
(
course_link
=
self
.
course_link
),
body
)
self
.
assertIn
(
"Thanks,"
,
body
)
self
.
assertIn
(
u"The {platform_name} team"
.
format
(
platform_name
=
settings
.
PLATFORM_NAME
),
body
)
@attr
(
shard
=
2
)
class
TestEmailMessageWithDefaultICRVBlock
(
ModuleStoreTestCase
):
"""
Test for In-course Re-verification
"""
def
build_course
(
self
):
"""
Build up a course tree with a Reverificaiton xBlock.
"""
self
.
course_key
=
SlashSeparatedCourseKey
(
"Robot"
,
"999"
,
"Test_Course"
)
self
.
course
=
CourseFactory
.
create
(
org
=
'Robot'
,
number
=
'999'
,
display_name
=
'Test Course'
)
# Create the course modes
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
min_price
=
0
if
mode
in
[
"honor"
,
"audit"
]
else
1
CourseModeFactory
.
create
(
mode_slug
=
mode
,
course_id
=
self
.
course_key
,
min_price
=
min_price
)
# Create the 'edx-reverification-block' in course tree
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
subsection
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'sequential'
,
display_name
=
'Test Subsection'
)
vertical
=
ItemFactory
.
create
(
parent
=
subsection
,
category
=
'vertical'
,
display_name
=
'Test Unit'
)
self
.
reverification
=
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'edx-reverification-block'
,
display_name
=
'Test Verification Block'
)
self
.
section_location
=
section
.
location
self
.
subsection_location
=
subsection
.
location
self
.
vertical_location
=
vertical
.
location
self
.
reverification_location
=
unicode
(
self
.
reverification
.
location
)
self
.
assessment
=
self
.
reverification
.
related_assessment
self
.
re_verification_link
=
reverse
(
'verify_student_incourse_reverify'
,
args
=
(
unicode
(
self
.
course_key
),
self
.
reverification_location
)
)
def
setUp
(
self
):
super
(
TestEmailMessageWithDefaultICRVBlock
,
self
)
.
setUp
()
self
.
build_course
()
self
.
check_point
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
reverification_location
)
self
.
check_point
.
add_verification_attempt
(
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
))
self
.
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
filter
(
user
=
self
.
user
)
self
.
request
=
RequestFactory
()
.
get
(
'/url'
)
def
test_denied_email_message_with_no_attempt_allowed
(
self
):
VerificationStatus
.
add_verification_status
(
checkpoint
=
self
.
check_point
,
user
=
self
.
user
,
status
=
'submitted'
)
__
,
body
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"denied"
,
self
.
request
)
self
.
assertIn
(
"We could not verify your identity for the {assessment} assessment "
"in the {course_name} course. You have used "
"{used_attempts} out of {allowed_attempts} attempts to "
"verify your identity, and verification is no longer "
"possible"
.
format
(
course_name
=
self
.
course
.
display_name_with_default_escaped
,
assessment
=
self
.
assessment
,
used_attempts
=
1
,
allowed_attempts
=
1
),
body
)
def
test_error_on_compose_email
(
self
):
resp
=
_compose_message_reverification_email
(
self
.
course
.
id
,
self
.
user
.
id
,
self
.
reverification_location
,
"denied"
,
True
)
self
.
assertIsNone
(
resp
)
lms/djangoapps/verify_student/urls.py
View file @
7e368e10
...
...
@@ -105,18 +105,6 @@ urlpatterns = patterns(
views
.
ReverifyView
.
as_view
(),
name
=
"verify_student_reverify"
),
# Endpoint for in-course reverification
# Users are sent to this end-point from within courseware
# to re-verify their identities by re-submitting face photos.
url
(
r'^reverify/{course_id}/{usage_id}/$'
.
format
(
course_id
=
settings
.
COURSE_ID_PATTERN
,
usage_id
=
settings
.
USAGE_ID_PATTERN
),
views
.
InCourseReverifyView
.
as_view
(),
name
=
"verify_student_incourse_reverify"
),
)
# Fake response page for incourse reverification ( software secure )
...
...
lms/djangoapps/verify_student/views.py
View file @
7e368e10
...
...
@@ -6,7 +6,6 @@ import datetime
import
decimal
import
json
import
logging
import
urllib
from
pytz
import
UTC
from
ipware.ip
import
get_ip
...
...
@@ -16,9 +15,7 @@ from django.core.mail import send_mail
from
django.core.urlresolvers
import
reverse
from
django.db
import
transaction
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
,
Http404
from
django.contrib.auth.models
import
User
from
django.shortcuts
import
redirect
from
django.utils
import
timezone
from
django.utils.decorators
import
method_decorator
from
django.utils.translation
import
ugettext
as
_
,
ugettext_lazy
from
django.views.decorators.csrf
import
csrf_exempt
...
...
@@ -28,7 +25,7 @@ from django.views.generic.base import View
import
analytics
from
eventtracking
import
tracker
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
opaque_keys.edx.keys
import
CourseKey
from
commerce.utils
import
EcommerceService
from
course_modes.models
import
CourseMode
...
...
@@ -40,7 +37,6 @@ from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from
openedx.core.djangoapps.user_api.accounts
import
NAME_MIN_LENGTH
from
openedx.core.djangoapps.user_api.accounts.api
import
update_account_settings
from
openedx.core.djangoapps.user_api.errors
import
UserNotFound
,
AccountValidationError
from
openedx.core.djangoapps.credit.api
import
set_credit_requirement_status
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.lib.log_utils
import
audit_log
from
student.models
import
CourseEnrollment
...
...
@@ -52,13 +48,9 @@ from lms.djangoapps.verify_student.ssencrypt import has_valid_signature
from
lms.djangoapps.verify_student.models
import
(
VerificationDeadline
,
SoftwareSecurePhotoVerification
,
VerificationCheckpoint
,
VerificationStatus
,
IcrvStatusEmailsConfiguration
,
)
from
lms.djangoapps.verify_student.image
import
decode_image_data
,
InvalidImageData
from
util.json_request
import
JsonResponse
from
util.date_utils
import
get_default_time_display
from
util.db
import
outer_atomic
from
xmodule.modulestore.django
import
modulestore
from
django.contrib.staticfiles.storage
import
staticfiles_storage
...
...
@@ -856,9 +848,7 @@ class SubmitPhotosView(View):
"""
# If the user already has an initial verification attempt, we can re-use the photo ID
# the user submitted with the initial attempt. This is useful for the in-course reverification
# case in which users submit only the face photo and have it matched against their ID photos
# submitted with the initial verification.
# the user submitted with the initial attempt.
initial_verification
=
SoftwareSecurePhotoVerification
.
get_initial_verification
(
request
.
user
)
# Validate the POST parameters
...
...
@@ -889,35 +879,9 @@ class SubmitPhotosView(View):
# Submit the attempt
attempt
=
self
.
_submit_attempt
(
request
.
user
,
face_image
,
photo_id_image
,
initial_verification
)
# If this attempt was submitted at a checkpoint, then associate
# the attempt with the checkpoint.
submitted_at_checkpoint
=
"checkpoint"
in
params
and
"course_key"
in
params
if
submitted_at_checkpoint
:
checkpoint
=
self
.
_associate_attempt_with_checkpoint
(
request
.
user
,
attempt
,
params
[
"course_key"
],
params
[
"checkpoint"
]
)
# If the submission came from an in-course checkpoint
if
initial_verification
is
not
None
and
submitted_at_checkpoint
:
self
.
_fire_event
(
request
.
user
,
"edx.bi.reverify.submitted"
,
{
"category"
:
"verification"
,
"label"
:
unicode
(
params
[
"course_key"
]),
"checkpoint"
:
checkpoint
.
checkpoint_name
,
})
# Send a URL that the client can redirect to in order
# to return to the checkpoint in the courseware.
redirect_url
=
get_redirect_url
(
params
[
"course_key"
],
params
[
"checkpoint"
])
return
JsonResponse
({
"url"
:
redirect_url
})
# Otherwise, the submission came from an initial verification flow.
else
:
self
.
_fire_event
(
request
.
user
,
"edx.bi.verify.submitted"
,
{
"category"
:
"verification"
})
self
.
_send_confirmation_email
(
request
.
user
)
redirect_url
=
None
return
JsonResponse
({})
self
.
_fire_event
(
request
.
user
,
"edx.bi.verify.submitted"
,
{
"category"
:
"verification"
})
self
.
_send_confirmation_email
(
request
.
user
)
return
JsonResponse
({})
def
_validate_parameters
(
self
,
request
,
has_initial_verification
):
"""
...
...
@@ -938,7 +902,6 @@ class SubmitPhotosView(View):
"face_image"
,
"photo_id_image"
,
"course_key"
,
"checkpoint"
,
"full_name"
]
if
param_name
in
request
.
POST
...
...
@@ -974,14 +937,6 @@ class SubmitPhotosView(View):
except
InvalidKeyError
:
return
None
,
HttpResponseBadRequest
(
_
(
"Invalid course key"
))
if
"checkpoint"
in
params
:
try
:
params
[
"checkpoint"
]
=
UsageKey
.
from_string
(
params
[
"checkpoint"
])
.
replace
(
course_key
=
params
[
"course_key"
]
)
except
InvalidKeyError
:
return
None
,
HttpResponseBadRequest
(
_
(
"Invalid checkpoint location"
))
return
params
,
None
def
_update_full_name
(
self
,
user
,
full_name
):
...
...
@@ -1070,24 +1025,6 @@ class SubmitPhotosView(View):
return
attempt
def
_associate_attempt_with_checkpoint
(
self
,
user
,
attempt
,
course_key
,
usage_id
):
"""
Associate the verification attempt with a checkpoint within a course.
Arguments:
user (User): The user making the attempt.
attempt (SoftwareSecurePhotoVerification): The verification attempt.
course_key (CourseKey): The identifier for the course.
usage_key (UsageKey): The location of the checkpoint within the course.
Returns:
VerificationCheckpoint
"""
checkpoint
=
VerificationCheckpoint
.
get_or_create_verification_checkpoint
(
course_key
,
usage_id
)
checkpoint
.
add_verification_attempt
(
attempt
)
VerificationStatus
.
add_verification_status
(
checkpoint
,
user
,
"submitted"
)
return
checkpoint
def
_send_confirmation_email
(
self
,
user
):
"""
Send an email confirming that the user submitted photos
...
...
@@ -1134,125 +1071,6 @@ class SubmitPhotosView(View):
analytics
.
track
(
user
.
id
,
event_name
,
parameters
,
context
=
context
)
def
_compose_message_reverification_email
(
course_key
,
user_id
,
related_assessment_location
,
status
,
request
):
# pylint: disable=invalid-name
"""
Compose subject and message for photo reverification email.
Args:
course_key(CourseKey): CourseKey object
user_id(str): User Id
related_assessment_location(str): Location of reverification XBlock
photo_verification(QuerySet): Queryset of SoftwareSecure objects
status(str): Approval status
is_secure(Bool): Is running on secure protocol or not
Returns:
None if any error occurred else Tuple of subject and message strings
"""
try
:
usage_key
=
UsageKey
.
from_string
(
related_assessment_location
)
reverification_block
=
modulestore
()
.
get_item
(
usage_key
)
course
=
modulestore
()
.
get_course
(
course_key
)
redirect_url
=
get_redirect_url
(
course_key
,
usage_key
.
replace
(
course_key
=
course_key
))
subject
=
"Re-verification Status"
context
=
{
"status"
:
status
,
"course_name"
:
course
.
display_name_with_default_escaped
,
"assessment"
:
reverification_block
.
related_assessment
}
# Allowed attempts is 1 if not set on verification block
allowed_attempts
=
reverification_block
.
attempts
+
1
used_attempts
=
VerificationStatus
.
get_user_attempts
(
user_id
,
course_key
,
related_assessment_location
)
left_attempts
=
allowed_attempts
-
used_attempts
is_attempt_allowed
=
left_attempts
>
0
verification_open
=
True
if
reverification_block
.
due
:
verification_open
=
timezone
.
now
()
<=
reverification_block
.
due
context
[
"left_attempts"
]
=
left_attempts
context
[
"is_attempt_allowed"
]
=
is_attempt_allowed
context
[
"verification_open"
]
=
verification_open
context
[
"due_date"
]
=
get_default_time_display
(
reverification_block
.
due
)
context
[
'platform_name'
]
=
configuration_helpers
.
get_value
(
'PLATFORM_NAME'
,
settings
.
PLATFORM_NAME
)
context
[
"used_attempts"
]
=
used_attempts
context
[
"allowed_attempts"
]
=
allowed_attempts
context
[
"support_link"
]
=
configuration_helpers
.
get_value
(
'email_from_address'
,
settings
.
CONTACT_EMAIL
)
re_verification_link
=
reverse
(
'verify_student_incourse_reverify'
,
args
=
(
unicode
(
course_key
),
related_assessment_location
)
)
context
[
"course_link"
]
=
request
.
build_absolute_uri
(
redirect_url
)
context
[
"reverify_link"
]
=
request
.
build_absolute_uri
(
re_verification_link
)
message
=
render_to_string
(
'emails/reverification_processed.txt'
,
context
)
log
.
info
(
"Sending email to User_Id=
%
s. Attempts left for this user are
%
s. "
"Allowed attempts
%
s. "
"Due Date
%
s"
,
str
(
user_id
),
left_attempts
,
allowed_attempts
,
str
(
reverification_block
.
due
)
)
return
subject
,
message
# Catch all exception to avoid raising back to view
except
:
# pylint: disable=bare-except
log
.
exception
(
"The email for re-verification sending failed for user_id
%
s"
,
user_id
)
def
_send_email
(
user_id
,
subject
,
message
):
""" Send email to given user
Args:
user_id(str): User Id
subject(str): Subject lines of emails
message(str): Email message body
Returns:
None
"""
from_address
=
configuration_helpers
.
get_value
(
'email_from_address'
,
settings
.
DEFAULT_FROM_EMAIL
)
user
=
User
.
objects
.
get
(
id
=
user_id
)
user
.
email_user
(
subject
,
message
,
from_address
)
def
_set_user_requirement_status
(
attempt
,
namespace
,
status
,
reason
=
None
):
"""Sets the status of a credit requirement for the user,
based on a verification checkpoint.
"""
checkpoint
=
None
try
:
checkpoint
=
VerificationCheckpoint
.
objects
.
get
(
photo_verification
=
attempt
)
except
VerificationCheckpoint
.
DoesNotExist
:
log
.
error
(
"Unable to find checkpoint for user with id
%
d"
,
attempt
.
user
.
id
)
if
checkpoint
is
not
None
:
try
:
set_credit_requirement_status
(
attempt
.
user
,
checkpoint
.
course_id
,
namespace
,
checkpoint
.
checkpoint_location
,
status
=
status
,
reason
=
reason
,
)
except
Exception
:
# pylint: disable=broad-except
# Catch exception if unable to add credit requirement
# status for user
log
.
error
(
"Unable to add Credit requirement status for user with id
%
d"
,
attempt
.
user
.
id
)
@require_POST
@csrf_exempt
# SS does its own message signing, and their API won't have a cookie value
def
results_callback
(
request
):
...
...
@@ -1310,15 +1128,11 @@ def results_callback(request):
log
.
debug
(
"Approving verification for
%
s"
,
receipt_id
)
attempt
.
approve
()
status
=
"approved"
_set_user_requirement_status
(
attempt
,
'reverification'
,
'satisfied'
)
elif
result
==
"FAIL"
:
log
.
debug
(
"Denying verification for
%
s"
,
receipt_id
)
attempt
.
deny
(
json
.
dumps
(
reason
),
error_code
=
error_code
)
status
=
"denied"
_set_user_requirement_status
(
attempt
,
'reverification'
,
'failed'
,
json
.
dumps
(
reason
)
)
elif
result
==
"SYSTEM FAIL"
:
log
.
debug
(
"System failure for
%
s -- resetting to must_retry"
,
receipt_id
)
attempt
.
system_error
(
json
.
dumps
(
reason
),
error_code
=
error_code
)
...
...
@@ -1330,22 +1144,6 @@ def results_callback(request):
"Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL"
.
format
(
result
)
)
checkpoints
=
VerificationCheckpoint
.
objects
.
filter
(
photo_verification
=
attempt
)
.
all
()
VerificationStatus
.
add_status_from_checkpoints
(
checkpoints
=
checkpoints
,
user
=
attempt
.
user
,
status
=
status
)
# Trigger ICRV email only if ICRV status emails config is enabled
icrv_status_emails
=
IcrvStatusEmailsConfiguration
.
current
()
if
icrv_status_emails
.
enabled
and
checkpoints
:
user_id
=
attempt
.
user
.
id
course_key
=
checkpoints
[
0
]
.
course_id
related_assessment_location
=
checkpoints
[
0
]
.
checkpoint_location
subject
,
message
=
_compose_message_reverification_email
(
course_key
,
user_id
,
related_assessment_location
,
status
,
request
)
_send_email
(
user_id
,
subject
,
message
)
return
HttpResponse
(
"OK!"
)
...
...
@@ -1398,130 +1196,3 @@ class ReverifyView(View):
"status"
:
status
}
return
render_to_response
(
"verify_student/reverify_not_allowed.html"
,
context
)
class
InCourseReverifyView
(
View
):
"""
The in-course reverification view.
In-course reverification occurs while a student is taking a course.
At points in the course, students are prompted to submit face photos,
which are matched against the ID photos the user submitted during their
initial verification.
Students are prompted to enter this flow from an "In Course Reverification"
XBlock (courseware component) that course authors add to the course.
See https://github.com/edx/edx-reverification-block for more details.
"""
@method_decorator
(
login_required
)
def
get
(
self
,
request
,
course_id
,
usage_id
):
"""Display the view for face photo submission.
Args:
request(HttpRequest): HttpRequest object
course_id(str): A string of course id
usage_id(str): Location of Reverification XBlock in courseware
Returns:
HttpResponse
"""
user
=
request
.
user
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
modulestore
()
.
get_course
(
course_key
)
if
course
is
None
:
log
.
error
(
u"Could not find course '
%
s' for in-course reverification."
,
course_key
)
raise
Http404
try
:
checkpoint
=
VerificationCheckpoint
.
objects
.
get
(
course_id
=
course_key
,
checkpoint_location
=
usage_id
)
except
VerificationCheckpoint
.
DoesNotExist
:
log
.
error
(
u"No verification checkpoint exists for the "
u"course '
%
s' and checkpoint location '
%
s'."
,
course_key
,
usage_id
)
raise
Http404
initial_verification
=
SoftwareSecurePhotoVerification
.
get_initial_verification
(
user
)
if
not
initial_verification
:
return
self
.
_redirect_to_initial_verification
(
user
,
course_key
,
usage_id
)
# emit the reverification event
self
.
_track_reverification_events
(
'edx.bi.reverify.started'
,
user
.
id
,
course_id
,
checkpoint
.
checkpoint_name
)
context
=
{
'course_key'
:
unicode
(
course_key
),
'course_name'
:
course
.
display_name_with_default_escaped
,
'checkpoint_name'
:
checkpoint
.
checkpoint_name
,
'platform_name'
:
configuration_helpers
.
get_value
(
'PLATFORM_NAME'
,
settings
.
PLATFORM_NAME
),
'usage_id'
:
usage_id
,
'capture_sound'
:
staticfiles_storage
.
url
(
"audio/camera_capture.wav"
),
}
return
render_to_response
(
"verify_student/incourse_reverify.html"
,
context
)
def
_track_reverification_events
(
self
,
event_name
,
user_id
,
course_id
,
checkpoint
):
"""Track re-verification events for a user against a reverification
checkpoint of a course.
Arguments:
event_name (str): Name of event being tracked
user_id (str): The ID of the user
course_id (unicode): ID associated with the course
checkpoint (str): Checkpoint name
Returns:
None
"""
log
.
info
(
u"In-course reverification: event
%
s occurred for user '
%
s' in course '
%
s' at checkpoint '
%
s'"
,
event_name
,
user_id
,
course_id
,
checkpoint
)
if
settings
.
LMS_SEGMENT_KEY
:
tracking_context
=
tracker
.
get_tracker
()
.
resolve_context
()
analytics
.
track
(
user_id
,
event_name
,
{
'category'
:
"verification"
,
'label'
:
unicode
(
course_id
),
'checkpoint'
:
checkpoint
},
context
=
{
'ip'
:
tracking_context
.
get
(
'ip'
),
'Google Analytics'
:
{
'clientId'
:
tracking_context
.
get
(
'client_id'
)
}
}
)
def
_redirect_to_initial_verification
(
self
,
user
,
course_key
,
checkpoint
):
"""
Redirect because the user does not have an initial verification.
We will redirect the user to the initial verification flow,
passing the identifier for this checkpoint. When the user
submits a verification attempt, it will count for *both*
the initial and checkpoint verification.
Arguments:
user (User): The user who made the request.
course_key (CourseKey): The identifier for the course for which
the user is attempting to re-verify.
checkpoint (string): Location of the checkpoint in the courseware.
Returns:
HttpResponse
"""
log
.
info
(
u"User
%
s does not have an initial verification, so "
u"he/she will be redirected to the
\"
verify later
\"
flow "
u"for the course
%
s."
,
user
.
id
,
course_key
)
base_url
=
reverse
(
'verify_student_verify_now'
,
kwargs
=
{
'course_id'
:
unicode
(
course_key
)})
params
=
urllib
.
urlencode
({
"checkpoint"
:
checkpoint
})
full_url
=
u"{base}?{params}"
.
format
(
base
=
base_url
,
params
=
params
)
return
redirect
(
full_url
)
openedx/core/djangoapps/credit/api/eligibility.py
View file @
7e368e10
...
...
@@ -48,12 +48,6 @@ def set_credit_requirements(course_key, requirements):
"course-v1-edX-DemoX-1T2015",
[
{
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {},
},
{
"namespace": "proctored_exam",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Final Exam",
...
...
@@ -107,12 +101,6 @@ def get_credit_requirements(course_key, namespace=None):
requirements =
[
{
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {},
},
{
"namespace": "proctored_exam",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Final Exam",
...
...
@@ -216,17 +204,6 @@ def set_credit_requirement_status(user, course_key, req_namespace, req_name, sta
Keyword Arguments:
status (str): Status of the requirement (either "satisfied" or "failed")
reason (dict): Reason of the status
Example:
>>> set_credit_requirement_status(
"staff",
CourseKey.from_string("course-v1-edX-DemoX-1T2015"),
"reverification",
"i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
status="satisfied",
reason={}
)
"""
# Check whether user has credit eligible enrollment.
enrollment_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
user
,
course_key
)
...
...
@@ -317,14 +294,6 @@ def remove_credit_requirement_status(username, course_key, req_namespace, req_na
req_name (str): Name of the requirement
(e.g. "grade" or the location of the ICRV XBlock)
Example:
>>> remove_credit_requirement_status(
"staff",
CourseKey.from_string("course-v1-edX-DemoX-1T2015"),
"reverification",
"i4x://edX/DemoX/edx-reverification-block/assessment_uuid".
)
"""
# Find the requirement we're trying to remove
...
...
@@ -365,16 +334,6 @@ def get_credit_requirement_status(course_key, username, namespace=None, name=Non
[
{
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "In Course Reverification",
"criteria": {},
"reason": {},
"status": "failed",
"status_date": "2015-06-26 07:49:13",
"order": 0,
},
{
"namespace": "proctored_exam",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Proctored Mid Term Exam",
...
...
openedx/core/djangoapps/credit/partition_schemes.py
deleted
100644 → 0
View file @
602b737b
"""
Partition scheme for in-course reverification.
This is responsible for placing users into one of two groups,
ALLOW or DENY, for a partition associated with a particular
in-course reverification checkpoint.
NOTE: This really should be defined in the verify_student app,
which owns the verification and reverification process.
It isn't defined there now because (a) we need access to this in both Studio
and the LMS, but verify_student is specific to the LMS, and
(b) in-course reverification checkpoints currently have messaging that's
specific to credit requirements.
"""
import
logging
from
django.core.cache
import
cache
from
lms.djangoapps.verify_student.models
import
SkippedReverification
,
VerificationStatus
from
student.models
import
CourseEnrollment
from
xmodule.partitions.partitions
import
NoSuchUserPartitionGroupError
log
=
logging
.
getLogger
(
__name__
)
class
VerificationPartitionScheme
(
object
):
"""
Assign users to groups for a particular verification checkpoint.
Users in the ALLOW group can see gated content;
users in the DENY group cannot.
"""
DENY
=
0
ALLOW
=
1
@classmethod
def
get_group_for_user
(
cls
,
course_key
,
user
,
user_partition
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Return the user's group depending their enrollment and verification
status.
Args:
course_key (CourseKey): CourseKey
user (User): user object
user_partition (UserPartition): The user partition object.
Returns:
string of allowed access group
"""
checkpoint
=
user_partition
.
parameters
[
'location'
]
# Retrieve all information we need to determine the user's group
# as a multi-get from the cache.
is_verified
,
has_skipped
,
has_completed
=
_get_user_statuses
(
user
,
course_key
,
checkpoint
)
# Decide whether the user should have access to content gated by this checkpoint.
# Intuitively, we allow access if the user doesn't need to do anything at the checkpoint,
# either because the user is in a non-verified track or the user has already submitted.
#
# Note that we do NOT wait the user's reverification attempt to be approved,
# since this can take some time and the user might miss an assignment deadline.
partition_group
=
cls
.
DENY
if
not
is_verified
or
has_skipped
or
has_completed
:
partition_group
=
cls
.
ALLOW
# Return matching user partition group if it exists
try
:
return
user_partition
.
get_group
(
partition_group
)
except
NoSuchUserPartitionGroupError
:
log
.
error
(
(
u"Could not find group with ID
%
s for verified partition "
"with ID
%
s in course
%
s. The user will not be assigned a group."
),
partition_group
,
user_partition
.
id
,
course_key
)
return
None
def
_get_user_statuses
(
user
,
course_key
,
checkpoint
):
"""
Retrieve all the information we need to determine the user's group.
This will retrieve the information as a multi-get from the cache.
Args:
user (User): User object
course_key (CourseKey): Identifier for the course.
checkpoint (unicode): Location of the checkpoint in the course (serialized usage key)
Returns:
tuple of booleans of the form (is_verified, has_skipped, has_completed)
"""
enrollment_cache_key
=
CourseEnrollment
.
cache_key_name
(
user
.
id
,
unicode
(
course_key
))
has_skipped_cache_key
=
SkippedReverification
.
cache_key_name
(
user
.
id
,
unicode
(
course_key
))
verification_status_cache_key
=
VerificationStatus
.
cache_key_name
(
user
.
id
,
unicode
(
course_key
))
# Try a multi-get from the cache
cache_values
=
cache
.
get_many
([
enrollment_cache_key
,
has_skipped_cache_key
,
verification_status_cache_key
])
# Retrieve whether the user is enrolled in a verified mode.
is_verified
=
cache_values
.
get
(
enrollment_cache_key
)
if
is_verified
is
None
:
is_verified
=
CourseEnrollment
.
is_enrolled_as_verified
(
user
,
course_key
)
cache
.
set
(
enrollment_cache_key
,
is_verified
)
# Retrieve whether the user has skipped any checkpoints in this course
has_skipped
=
cache_values
.
get
(
has_skipped_cache_key
)
if
has_skipped
is
None
:
has_skipped
=
SkippedReverification
.
check_user_skipped_reverification_exists
(
user
,
course_key
)
cache
.
set
(
has_skipped_cache_key
,
has_skipped
)
# Retrieve the user's verification status for each checkpoint in the course.
verification_statuses
=
cache_values
.
get
(
verification_status_cache_key
)
if
verification_statuses
is
None
:
verification_statuses
=
VerificationStatus
.
get_all_checkpoints
(
user
.
id
,
course_key
)
cache
.
set
(
verification_status_cache_key
,
verification_statuses
)
# Check whether the user has completed this checkpoint
# "Completion" here means *any* submission, regardless of its status
# since we want to show the user the content if they've submitted
# photos.
checkpoint
=
verification_statuses
.
get
(
checkpoint
)
has_completed_check
=
bool
(
checkpoint
)
return
(
is_verified
,
has_skipped
,
has_completed_check
)
openedx/core/djangoapps/credit/signals.py
View file @
7e368e10
...
...
@@ -9,7 +9,6 @@ from django.utils import timezone
from
opaque_keys.edx.keys
import
CourseKey
from
xmodule.modulestore.django
import
SignalHandler
from
openedx.core.djangoapps.credit.verification_access
import
update_verification_partitions
from
openedx.core.djangoapps.signals.signals
import
COURSE_GRADE_CHANGED
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -33,25 +32,6 @@ def on_course_publish(course_key):
log
.
info
(
u'Added task to update credit requirements for course "
%
s" to the task queue'
,
course_key
)
@receiver
(
SignalHandler
.
pre_publish
)
def
on_pre_publish
(
sender
,
course_key
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Create user partitions for verification checkpoints.
This is a pre-publish step since we need to write to the course descriptor.
"""
from
openedx.core.djangoapps.credit
import
api
if
api
.
is_credit_course
(
course_key
):
# For now, we are tagging content with in-course-reverification access groups
# only in credit courses on publish. In the long run, this is not where we want to put this.
# This really should be a transformation on the course structure performed as a pre-processing
# step by the LMS, and the transformation should be owned by the verify_student app.
# Since none of that infrastructure currently exists, we're doing it this way instead.
log
.
info
(
u"Starting to update in-course reverification access rules"
)
update_verification_partitions
(
course_key
)
log
.
info
(
u"Finished updating in-course reverification access rules"
)
@receiver
(
COURSE_GRADE_CHANGED
)
def
listen_for_grade_calculation
(
sender
,
user
,
course_grade
,
course_key
,
deadline
,
**
kwargs
):
# pylint: disable=unused-argument
"""Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status.
...
...
openedx/core/djangoapps/credit/tasks.py
View file @
7e368e10
...
...
@@ -18,12 +18,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
LOGGER
=
get_task_logger
(
__name__
)
# XBlocks that can be added as credit requirements
CREDIT_REQUIREMENT_XBLOCK_CATEGORIES
=
[
"edx-reverification-block"
,
]
# pylint: disable=not-callable
@task
(
default_retry_delay
=
settings
.
CREDIT_TASK_DEFAULT_RETRY_DELAY
,
max_retries
=
settings
.
CREDIT_TASK_MAX_RETRIES
)
def
update_credit_course_requirements
(
course_id
):
# pylint: disable=invalid-name
...
...
@@ -67,18 +61,14 @@ def _get_course_credit_requirements(course_key):
List of credit requirements (dictionaries)
"""
credit_xblock_requirements
=
_get_credit_course_requirement_xblocks
(
course_key
)
min_grade_requirement
=
_get_min_grade_requirement
(
course_key
)
proctored_exams_requirements
=
_get_proctoring_requirements
(
course_key
)
block_requirements
=
credit_xblock_requirements
+
proctored_exams_requirements
# sort credit requirements list based on start date and put all the
# requirements with no start date at the end of requirement list.
sorted_block_requirements
=
sorted
(
block_requirements
,
key
=
lambda
x
:
(
x
[
'start_date'
]
is
None
,
x
[
'start_date'
],
x
[
'display_name'
])
sorted_exam_requirements
=
sorted
(
proctored_exams_requirements
,
key
=
lambda
x
:
(
x
[
'start_date'
]
is
None
,
x
[
'start_date'
],
x
[
'display_name'
])
)
credit_requirements
=
(
min_grade_requirement
+
sorted_
block
_requirements
min_grade_requirement
+
sorted_
exam
_requirements
)
return
credit_requirements
...
...
@@ -112,76 +102,6 @@ def _get_min_grade_requirement(course_key):
return
[]
def
_get_credit_course_requirement_xblocks
(
course_key
):
# pylint: disable=invalid-name
"""Generate a course structure dictionary for the specified course.
Args:
course_key (CourseKey): Identifier for the course.
Returns:
The list of credit requirements xblocks dicts
"""
requirements
=
[]
# Retrieve all XBlocks from the course that we know to be credit requirements.
# For performance reasons, we look these up by their "category" to avoid
# loading and searching the entire course tree.
for
category
in
CREDIT_REQUIREMENT_XBLOCK_CATEGORIES
:
requirements
.
extend
([
{
"namespace"
:
block
.
get_credit_requirement_namespace
(),
"name"
:
block
.
get_credit_requirement_name
(),
"display_name"
:
block
.
get_credit_requirement_display_name
(),
'start_date'
:
block
.
start
,
"criteria"
:
{},
}
for
block
in
_get_xblocks
(
course_key
,
category
)
if
_is_credit_requirement
(
block
)
])
return
requirements
def
_get_xblocks
(
course_key
,
category
):
"""
Retrieve all XBlocks in the course for a particular category.
Returns only XBlocks that are published and haven't been deleted.
"""
xblocks
=
get_course_blocks
(
course_key
,
category
)
return
xblocks
def
_is_credit_requirement
(
xblock
):
"""
Check if the given XBlock is a credit requirement.
Args:
xblock(XBlock): The given XBlock object
Returns:
True if XBlock is a credit requirement else False
"""
required_methods
=
[
"get_credit_requirement_namespace"
,
"get_credit_requirement_name"
,
"get_credit_requirement_display_name"
]
for
method_name
in
required_methods
:
if
not
callable
(
getattr
(
xblock
,
method_name
,
None
)):
LOGGER
.
error
(
"XBlock
%
s is marked as a credit requirement but does not "
"implement
%
s"
,
unicode
(
xblock
),
method_name
)
return
False
return
True
def
_get_proctoring_requirements
(
course_key
):
"""
Will return list of requirements regarding any exams that have been
...
...
openedx/core/djangoapps/credit/tests/test_api.py
View file @
7e368e10
...
...
@@ -287,7 +287,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Set initial requirements
requirements
=
[
{
"namespace"
:
"
reverification
"
,
"namespace"
:
"
grade
"
,
"name"
:
"midterm"
,
"display_name"
:
"Midterm"
,
"criteria"
:
{},
...
...
@@ -328,8 +328,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
requirements
=
[
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
@@ -453,8 +453,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
},
},
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
@@ -516,15 +516,15 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Set the requirement to "declined" and check that it's actually set
api
.
set_credit_requirement_status
(
self
.
user
,
self
.
course_key
,
"
reverification
"
,
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"
grade
"
,
"
other_grade
"
,
status
=
"declined"
)
req_status
=
api
.
get_credit_requirement_status
(
self
.
course_key
,
username
,
namespace
=
"
reverification
"
,
name
=
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
namespace
=
"
grade
"
,
name
=
"
other_grade
"
)
self
.
assertEqual
(
req_status
[
0
][
"status"
],
"declined"
)
...
...
@@ -571,8 +571,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
},
},
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
@@ -643,8 +643,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
},
},
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
@@ -770,8 +770,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
},
},
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
@@ -833,8 +833,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
},
},
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"namespace"
:
"
grade
"
,
"name"
:
"
other_grade
"
,
"display_name"
:
"Assessment 1"
,
"criteria"
:
{},
}
...
...
openedx/core/djangoapps/credit/tests/test_models.py
View file @
7e368e10
...
...
@@ -61,9 +61,9 @@ class CreditEligibilityModelTests(TestCase):
self
.
assertEqual
(
created
,
True
)
requirement
=
{
"namespace"
:
"
reverification
"
,
"name"
:
"
i4x://edX/DemoX/edx-reverification-block/assessment_uuid
"
,
"display_name"
:
"
Assessment 1
"
,
"namespace"
:
"
new_grade
"
,
"name"
:
"
new_grade
"
,
"display_name"
:
"
New Grade
"
,
"criteria"
:
{},
}
credit_req
,
created
=
CreditRequirement
.
add_or_update_course_requirement
(
credit_course
,
requirement
,
1
)
...
...
openedx/core/djangoapps/credit/tests/test_partition.py
deleted
100644 → 0
View file @
602b737b
# -*- coding: utf-8 -*-
"""
Tests for In-Course Reverification Access Control Partition scheme
"""
import
ddt
from
nose.plugins.attrib
import
attr
from
lms.djangoapps.verify_student.models
import
(
VerificationCheckpoint
,
VerificationStatus
,
SkippedReverification
,
)
from
openedx.core.djangoapps.credit.partition_schemes
import
VerificationPartitionScheme
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
xmodule.partitions.partitions
import
UserPartition
,
Group
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
@attr
(
shard
=
2
)
@ddt.ddt
@skip_unless_lms
class
ReverificationPartitionTest
(
ModuleStoreTestCase
):
"""Tests for the Reverification Partition Scheme. """
SUBMITTED
=
"submitted"
APPROVED
=
"approved"
DENIED
=
"denied"
ENABLED_CACHES
=
[
'default'
,
'mongo_metadata_inheritance'
,
'loc_cache'
]
def
setUp
(
self
):
super
(
ReverificationPartitionTest
,
self
)
.
setUp
()
# creating course, checkpoint location and user partition mock object.
self
.
course
=
CourseFactory
.
create
()
self
.
checkpoint_location
=
u'i4x://{org}/{course}/edx-reverification-block/first_uuid'
.
format
(
org
=
self
.
course
.
id
.
org
,
course
=
self
.
course
.
id
.
course
)
scheme
=
UserPartition
.
get_scheme
(
"verification"
)
self
.
user_partition
=
UserPartition
(
id
=
0
,
name
=
u"Verification Checkpoint"
,
description
=
u"Verification Checkpoint"
,
scheme
=
scheme
,
parameters
=
{
"location"
:
self
.
checkpoint_location
},
groups
=
[
Group
(
scheme
.
ALLOW
,
"Allow access to content"
),
Group
(
scheme
.
DENY
,
"Deny access to content"
),
]
)
self
.
first_checkpoint
=
VerificationCheckpoint
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
checkpoint_location
=
self
.
checkpoint_location
)
def
create_user_and_enroll
(
self
,
enrollment_type
):
"""Create and enroll users with provided enrollment type."""
user
=
UserFactory
.
create
()
CourseEnrollment
.
objects
.
create
(
user
=
user
,
course_id
=
self
.
course
.
id
,
mode
=
enrollment_type
,
is_active
=
True
)
return
user
def
add_verification_status
(
self
,
user
,
status
):
"""Adding the verification status for a user."""
VerificationStatus
.
add_status_from_checkpoints
(
checkpoints
=
[
self
.
first_checkpoint
],
user
=
user
,
status
=
status
)
@ddt.data
(
(
"verified"
,
SUBMITTED
,
VerificationPartitionScheme
.
ALLOW
),
(
"verified"
,
APPROVED
,
VerificationPartitionScheme
.
ALLOW
),
(
"verified"
,
DENIED
,
VerificationPartitionScheme
.
ALLOW
),
(
"verified"
,
None
,
VerificationPartitionScheme
.
DENY
),
(
"honor"
,
None
,
VerificationPartitionScheme
.
ALLOW
),
)
@ddt.unpack
def
test_get_group_for_user
(
self
,
enrollment_type
,
verification_status
,
expected_group
):
# creating user and enroll them.
user
=
self
.
create_user_and_enroll
(
enrollment_type
)
if
verification_status
:
self
.
add_verification_status
(
user
,
verification_status
)
self
.
_assert_group_assignment
(
user
,
expected_group
)
def
test_get_group_for_user_with_skipped
(
self
):
# Check that a user is in verified allow group if that user has skipped
# any ICRV block.
user
=
self
.
create_user_and_enroll
(
'verified'
)
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
first_checkpoint
,
user_id
=
user
.
id
,
course_id
=
self
.
course
.
id
)
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
def
test_cache_with_skipped_icrv
(
self
):
# Check that a user is in verified allow group if that user has skipped
# any ICRV block.
user
=
self
.
create_user_and_enroll
(
'verified'
)
SkippedReverification
.
add_skipped_reverification_attempt
(
checkpoint
=
self
.
first_checkpoint
,
user_id
=
user
.
id
,
course_id
=
self
.
course
.
id
)
# this will warm the cache.
with
self
.
assertNumQueries
(
3
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
# no db queries this time.
with
self
.
assertNumQueries
(
0
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
def
test_cache_with_submitted_status
(
self
):
# Check that a user is in verified allow group if that user has approved status at
# any ICRV block.
user
=
self
.
create_user_and_enroll
(
'verified'
)
self
.
add_verification_status
(
user
,
VerificationStatus
.
APPROVED_STATUS
)
# this will warm the cache.
with
self
.
assertNumQueries
(
4
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
# no db queries this time.
with
self
.
assertNumQueries
(
0
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
def
test_cache_with_denied_status
(
self
):
# Check that a user is in verified allow group if that user has denied at
# any ICRV block.
user
=
self
.
create_user_and_enroll
(
'verified'
)
self
.
add_verification_status
(
user
,
VerificationStatus
.
DENIED_STATUS
)
# this will warm the cache.
with
self
.
assertNumQueries
(
4
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
# no db queries this time.
with
self
.
assertNumQueries
(
0
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
def
test_cache_with_honor
(
self
):
# Check that a user is in honor mode.
# any ICRV block.
user
=
self
.
create_user_and_enroll
(
'honor'
)
# this will warm the cache.
with
self
.
assertNumQueries
(
3
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
# no db queries this time.
with
self
.
assertNumQueries
(
0
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
ALLOW
)
def
test_cache_with_verified_deny_group
(
self
):
# Check that a user is in verified mode. But not perform any action
user
=
self
.
create_user_and_enroll
(
'verified'
)
# this will warm the cache.
with
self
.
assertNumQueries
(
3
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
DENY
)
# no db queries this time.
with
self
.
assertNumQueries
(
0
):
self
.
_assert_group_assignment
(
user
,
VerificationPartitionScheme
.
DENY
)
def
_assert_group_assignment
(
self
,
user
,
expected_group_id
):
"""Check that the user was assigned to a group. """
actual_group
=
VerificationPartitionScheme
.
get_group_for_user
(
self
.
course
.
id
,
user
,
self
.
user_partition
)
self
.
assertEqual
(
actual_group
.
id
,
expected_group_id
)
openedx/core/djangoapps/credit/tests/test_tasks.py
View file @
7e368e10
...
...
@@ -4,16 +4,14 @@ Tests for credit course tasks.
import
mock
from
nose.plugins.attrib
import
attr
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
from
pytz
import
UTC
from
openedx.core.djangoapps.credit.api
import
get_credit_requirements
from
openedx.core.djangoapps.credit.exceptions
import
InvalidCreditRequirements
from
openedx.core.djangoapps.credit.models
import
CreditCourse
from
openedx.core.djangoapps.credit.signals
import
on_course_publish
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls_range
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
edx_proctoring.api
import
create_exam
...
...
@@ -34,25 +32,6 @@ class TestTaskExecution(ModuleStoreTestCase):
"""
raise
InvalidCreditRequirements
def
add_icrv_xblock
(
self
,
related_assessment_name
=
None
,
start_date
=
None
):
""" Create the 'edx-reverification-block' in course tree """
block
=
ItemFactory
.
create
(
parent
=
self
.
vertical
,
category
=
'edx-reverification-block'
,
)
if
related_assessment_name
is
not
None
:
block
.
related_assessment
=
related_assessment_name
block
.
start
=
start_date
self
.
store
.
update_item
(
block
,
ModuleStoreEnum
.
UserID
.
test
)
with
self
.
store
.
branch_setting
(
ModuleStoreEnum
.
Branch
.
draft_preferred
,
self
.
course
.
id
):
self
.
store
.
publish
(
block
.
location
,
ModuleStoreEnum
.
UserID
.
test
)
return
block
def
setUp
(
self
):
super
(
TestTaskExecution
,
self
)
.
setUp
()
...
...
@@ -86,19 +65,6 @@ class TestTaskExecution(ModuleStoreTestCase):
requirements
=
get_credit_requirements
(
self
.
course
.
id
)
self
.
assertEqual
(
len
(
requirements
),
1
)
def
test_task_adding_icrv_requirements
(
self
):
"""Make sure that the receiver correctly fires off the task when
invoked by signal.
"""
self
.
add_credit_course
(
self
.
course
.
id
)
self
.
add_icrv_xblock
()
requirements
=
get_credit_requirements
(
self
.
course
.
id
)
self
.
assertEqual
(
len
(
requirements
),
0
)
on_course_publish
(
self
.
course
.
id
)
requirements
=
get_credit_requirements
(
self
.
course
.
id
)
self
.
assertEqual
(
len
(
requirements
),
2
)
def
test_proctored_exam_requirements
(
self
):
"""
Make sure that proctored exams are being registered as requirements
...
...
@@ -202,71 +168,6 @@ class TestTaskExecution(ModuleStoreTestCase):
if
requirement
[
'namespace'
]
==
'proctored_exam'
])
def
test_query_counts
(
self
):
self
.
add_credit_course
(
self
.
course
.
id
)
self
.
add_icrv_xblock
()
with
check_mongo_calls_range
(
max_finds
=
11
):
on_course_publish
(
self
.
course
.
id
)
def
test_remove_icrv_requirement
(
self
):
self
.
add_credit_course
(
self
.
course
.
id
)
self
.
add_icrv_xblock
()
on_course_publish
(
self
.
course
.
id
)
# There should be one ICRV requirement
requirements
=
get_credit_requirements
(
self
.
course
.
id
,
namespace
=
"reverification"
)
self
.
assertEqual
(
len
(
requirements
),
1
)
# Delete the parent section containing the ICRV block
with
self
.
store
.
branch_setting
(
ModuleStoreEnum
.
Branch
.
draft_preferred
,
self
.
course
.
id
):
self
.
store
.
delete_item
(
self
.
subsection
.
location
,
ModuleStoreEnum
.
UserID
.
test
)
# Check that the ICRV block is no longer visible in the requirements
on_course_publish
(
self
.
course
.
id
)
requirements
=
get_credit_requirements
(
self
.
course
.
id
,
namespace
=
"reverification"
)
self
.
assertEqual
(
len
(
requirements
),
0
)
def
test_icrv_requirement_ordering
(
self
):
self
.
add_credit_course
(
self
.
course
.
id
)
# Create multiple ICRV blocks
start
=
datetime
.
now
(
UTC
)
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm A"
,
start_date
=
start
)
start
=
start
-
timedelta
(
days
=
1
)
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm B"
,
start_date
=
start
)
# Primary sort is based on start date
on_course_publish
(
self
.
course
.
id
)
requirements
=
get_credit_requirements
(
self
.
course
.
id
,
namespace
=
"reverification"
)
self
.
assertEqual
(
len
(
requirements
),
2
)
self
.
assertEqual
(
requirements
[
0
][
"display_name"
],
"Midterm B"
)
self
.
assertEqual
(
requirements
[
1
][
"display_name"
],
"Midterm A"
)
# Add two additional ICRV blocks that have no start date
# and the same name.
start
=
datetime
.
now
(
UTC
)
first_block
=
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm Start Date"
)
start
=
start
+
timedelta
(
days
=
1
)
second_block
=
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm Start Date"
)
on_course_publish
(
self
.
course
.
id
)
requirements
=
get_credit_requirements
(
self
.
course
.
id
,
namespace
=
"reverification"
)
self
.
assertEqual
(
len
(
requirements
),
4
)
# Since we are now primarily sorting on start_date and display_name if
# start_date is present otherwise we are just sorting on display_name.
self
.
assertEqual
(
requirements
[
0
][
"display_name"
],
"Midterm B"
)
self
.
assertEqual
(
requirements
[
1
][
"display_name"
],
"Midterm A"
)
self
.
assertEqual
(
requirements
[
2
][
"display_name"
],
"Midterm Start Date"
)
self
.
assertEqual
(
requirements
[
3
][
"display_name"
],
"Midterm Start Date"
)
# Since the last two requirements have the same display name,
# we need to also check that their internal names (locations) are the same.
self
.
assertEqual
(
requirements
[
2
][
"name"
],
first_block
.
get_credit_requirement_name
())
self
.
assertEqual
(
requirements
[
3
][
"name"
],
second_block
.
get_credit_requirement_name
())
@mock.patch
(
'openedx.core.djangoapps.credit.tasks.set_credit_requirements'
,
mock
.
Mock
(
...
...
@@ -290,7 +191,7 @@ class TestTaskExecution(ModuleStoreTestCase):
def
test_credit_requirement_blocks_ordering
(
self
):
"""
Test ordering of
the proctoring and ICRV blocks are in proper order
.
Test ordering of
proctoring blocks
.
"""
self
.
add_credit_course
(
self
.
course
.
id
)
...
...
@@ -315,24 +216,15 @@ class TestTaskExecution(ModuleStoreTestCase):
self
.
assertEqual
(
requirements
[
1
][
'display_name'
],
'A Proctored Exam'
)
self
.
assertEqual
(
requirements
[
1
][
'criteria'
],
{})
# Create multiple ICRV blocks
start
=
datetime
.
now
(
UTC
)
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm A"
,
start_date
=
start
)
start
=
start
-
timedelta
(
days
=
1
)
self
.
add_icrv_xblock
(
related_assessment_name
=
"Midterm B"
,
start_date
=
start
)
# Primary sort is based on start date
on_course_publish
(
self
.
course
.
id
)
requirements
=
get_credit_requirements
(
self
.
course
.
id
)
# grade requirement is added on publish of the requirements
self
.
assertEqual
(
len
(
requirements
),
4
)
self
.
assertEqual
(
len
(
requirements
),
2
)
# check requirements are added in the desired order
# 1st Minimum grade then the blocks with start date than other blocks
self
.
assertEqual
(
requirements
[
0
][
"display_name"
],
"Minimum Grade"
)
self
.
assertEqual
(
requirements
[
1
][
"display_name"
],
"A Proctored Exam"
)
self
.
assertEqual
(
requirements
[
2
][
"display_name"
],
"Midterm B"
)
self
.
assertEqual
(
requirements
[
3
][
"display_name"
],
"Midterm A"
)
def
add_credit_course
(
self
,
course_key
):
"""Add the course as a credit.
...
...
openedx/core/djangoapps/credit/tests/test_verification_access.py
deleted
100644 → 0
View file @
602b737b
"""
Tests for in-course reverification user partition creation.
This should really belong to the verify_student app,
but we can't move it there because it's in the LMS and we're
currently applying these rules on publish from Studio.
In the future, this functionality should be a course transformation
defined in the verify_student app, and these tests should be moved
into verify_student.
"""
from
mock
import
patch
from
nose.plugins.attrib
import
attr
from
django.conf
import
settings
from
openedx.core.djangoapps.credit.models
import
CreditCourse
from
openedx.core.djangoapps.credit.partition_schemes
import
VerificationPartitionScheme
from
openedx.core.djangoapps.credit.verification_access
import
update_verification_partitions
from
openedx.core.djangoapps.credit.signals
import
on_pre_publish
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
SignalHandler
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
,
TEST_DATA_SPLIT_MODULESTORE
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls_range
from
xmodule.partitions.partitions
import
Group
,
UserPartition
@attr
(
shard
=
2
)
class
CreateVerificationPartitionTest
(
ModuleStoreTestCase
):
"""
Tests for applying verification access rules.
"""
# Run the tests in split modulestore
# While verification access will work in old-Mongo, it's not something
# we're committed to supporting, since this feature is meant for use
# in new courses.
MODULESTORE
=
TEST_DATA_SPLIT_MODULESTORE
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
setUp
(
self
):
super
(
CreateVerificationPartitionTest
,
self
)
.
setUp
()
# Disconnect the signal receiver -- we'll invoke the update code ourselves
SignalHandler
.
pre_publish
.
disconnect
(
receiver
=
on_pre_publish
)
self
.
addCleanup
(
SignalHandler
.
pre_publish
.
connect
,
receiver
=
on_pre_publish
)
# Create a dummy course with a single verification checkpoint
# Because we need to check "exam" content surrounding the ICRV checkpoint,
# we need to create a fairly large course structure, with multiple sections,
# subsections, verticals, units, and items.
self
.
course
=
CourseFactory
()
self
.
sections
=
[
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section A'
),
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section B'
),
]
self
.
subsections
=
[
ItemFactory
.
create
(
parent
=
self
.
sections
[
0
],
category
=
'sequential'
,
display_name
=
'Test Subsection A 1'
),
ItemFactory
.
create
(
parent
=
self
.
sections
[
0
],
category
=
'sequential'
,
display_name
=
'Test Subsection A 2'
),
ItemFactory
.
create
(
parent
=
self
.
sections
[
1
],
category
=
'sequential'
,
display_name
=
'Test Subsection B 1'
),
ItemFactory
.
create
(
parent
=
self
.
sections
[
1
],
category
=
'sequential'
,
display_name
=
'Test Subsection B 2'
),
]
self
.
verticals
=
[
ItemFactory
.
create
(
parent
=
self
.
subsections
[
0
],
category
=
'vertical'
,
display_name
=
'Test Unit A 1 a'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
0
],
category
=
'vertical'
,
display_name
=
'Test Unit A 1 b'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
1
],
category
=
'vertical'
,
display_name
=
'Test Unit A 2 a'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
1
],
category
=
'vertical'
,
display_name
=
'Test Unit A 2 b'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
2
],
category
=
'vertical'
,
display_name
=
'Test Unit B 1 a'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
2
],
category
=
'vertical'
,
display_name
=
'Test Unit B 1 b'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
3
],
category
=
'vertical'
,
display_name
=
'Test Unit B 2 a'
),
ItemFactory
.
create
(
parent
=
self
.
subsections
[
3
],
category
=
'vertical'
,
display_name
=
'Test Unit B 2 b'
),
]
self
.
icrv
=
ItemFactory
.
create
(
parent
=
self
.
verticals
[
0
],
category
=
'edx-reverification-block'
)
self
.
sibling_problem
=
ItemFactory
.
create
(
parent
=
self
.
verticals
[
0
],
category
=
'problem'
)
def
test_creates_user_partitions
(
self
):
self
.
_update_partitions
()
# Check that a new user partition was created for the ICRV block
self
.
assertEqual
(
len
(
self
.
course
.
user_partitions
),
1
)
partition
=
self
.
course
.
user_partitions
[
0
]
self
.
assertEqual
(
partition
.
scheme
.
name
,
"verification"
)
self
.
assertEqual
(
partition
.
parameters
[
"location"
],
unicode
(
self
.
icrv
.
location
))
# Check that the groups for the partition were created correctly
self
.
assertEqual
(
len
(
partition
.
groups
),
2
)
self
.
assertItemsEqual
(
[
g
.
id
for
g
in
partition
.
groups
],
[
VerificationPartitionScheme
.
ALLOW
,
VerificationPartitionScheme
.
DENY
,
]
)
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
test_removes_deleted_user_partitions
(
self
):
self
.
_update_partitions
()
# Delete the reverification block, then update the partitions
self
.
store
.
delete_item
(
self
.
icrv
.
location
,
ModuleStoreEnum
.
UserID
.
test
,
revision
=
ModuleStoreEnum
.
RevisionOption
.
published_only
)
self
.
_update_partitions
()
# Check that the user partition was marked as inactive
self
.
assertEqual
(
len
(
self
.
course
.
user_partitions
),
1
)
partition
=
self
.
course
.
user_partitions
[
0
]
self
.
assertFalse
(
partition
.
active
)
self
.
assertEqual
(
partition
.
scheme
.
name
,
"verification"
)
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
test_preserves_partition_id_for_verified_partitions
(
self
):
self
.
_update_partitions
()
partition_id
=
self
.
course
.
user_partitions
[
0
]
.
id
self
.
_update_partitions
()
new_partition_id
=
self
.
course
.
user_partitions
[
0
]
.
id
self
.
assertEqual
(
partition_id
,
new_partition_id
)
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
test_preserves_existing_user_partitions
(
self
):
# Add other, non-verified partition to the course
self
.
course
.
user_partitions
=
[
UserPartition
(
id
=
0
,
name
=
'Cohort user partition'
,
scheme
=
UserPartition
.
get_scheme
(
'cohort'
),
description
=
'Cohorted user partition'
,
groups
=
[
Group
(
id
=
0
,
name
=
"Group A"
),
Group
(
id
=
1
,
name
=
"Group B"
),
],
),
UserPartition
(
id
=
1
,
name
=
'Random user partition'
,
scheme
=
UserPartition
.
get_scheme
(
'random'
),
description
=
'Random user partition'
,
groups
=
[
Group
(
id
=
0
,
name
=
"Group A"
),
Group
(
id
=
1
,
name
=
"Group B"
),
],
),
]
self
.
course
=
self
.
store
.
update_item
(
self
.
course
,
ModuleStoreEnum
.
UserID
.
test
)
# Update the verification partitions.
# The existing partitions should still be available
self
.
_update_partitions
()
partition_ids
=
[
p
.
id
for
p
in
self
.
course
.
user_partitions
]
self
.
assertEqual
(
len
(
partition_ids
),
3
)
self
.
assertIn
(
0
,
partition_ids
)
self
.
assertIn
(
1
,
partition_ids
)
def
test_multiple_reverification_blocks
(
self
):
# Add an additional ICRV block in another section
other_icrv
=
ItemFactory
.
create
(
parent
=
self
.
verticals
[
3
],
category
=
'edx-reverification-block'
)
self
.
_update_partitions
()
# Expect that both ICRV blocks have corresponding partitions
self
.
assertEqual
(
len
(
self
.
course
.
user_partitions
),
2
)
partition_locations
=
[
p
.
parameters
.
get
(
"location"
)
for
p
in
self
.
course
.
user_partitions
]
self
.
assertIn
(
unicode
(
self
.
icrv
.
location
),
partition_locations
)
self
.
assertIn
(
unicode
(
other_icrv
.
location
),
partition_locations
)
# Delete the first ICRV block and update partitions
icrv_location
=
self
.
icrv
.
location
self
.
store
.
delete_item
(
self
.
icrv
.
location
,
ModuleStoreEnum
.
UserID
.
test
,
revision
=
ModuleStoreEnum
.
RevisionOption
.
published_only
)
self
.
_update_partitions
()
# Expect that the correct partition is marked as inactive
self
.
assertEqual
(
len
(
self
.
course
.
user_partitions
),
2
)
partitions_by_loc
=
{
p
.
parameters
[
"location"
]:
p
for
p
in
self
.
course
.
user_partitions
}
self
.
assertFalse
(
partitions_by_loc
[
unicode
(
icrv_location
)]
.
active
)
self
.
assertTrue
(
partitions_by_loc
[
unicode
(
other_icrv
.
location
)]
.
active
)
def
test_query_counts_with_no_reverification_blocks
(
self
):
# Delete the ICRV block, so the number of ICRV blocks is zero
self
.
store
.
delete_item
(
self
.
icrv
.
location
,
ModuleStoreEnum
.
UserID
.
test
,
revision
=
ModuleStoreEnum
.
RevisionOption
.
published_only
)
# 2 calls: get the course (definitions + structures)
# 2 calls: look up ICRV blocks in the course (definitions + structures)
with
check_mongo_calls_range
(
max_finds
=
4
,
max_sends
=
2
):
self
.
_update_partitions
(
reload_items
=
False
)
def
test_query_counts_with_one_reverification_block
(
self
):
# One ICRV block created in the setup method
# Additional call to load the ICRV block
with
check_mongo_calls_range
(
max_finds
=
5
,
max_sends
=
3
):
self
.
_update_partitions
(
reload_items
=
False
)
def
test_query_counts_with_multiple_reverification_blocks
(
self
):
# Total of two ICRV blocks (one created in setup method)
# Additional call to load each ICRV block
ItemFactory
.
create
(
parent
=
self
.
verticals
[
3
],
category
=
'edx-reverification-block'
)
with
check_mongo_calls_range
(
max_finds
=
6
,
max_sends
=
3
):
self
.
_update_partitions
(
reload_items
=
False
)
def
_update_partitions
(
self
,
reload_items
=
True
):
"""Update user partitions in the course descriptor, then reload the content. """
update_verification_partitions
(
self
.
course
.
id
)
# pylint: disable=no-member
# Reload each component so we can see the changes
if
reload_items
:
self
.
course
=
self
.
store
.
get_course
(
self
.
course
.
id
)
# pylint: disable=no-member
self
.
sections
=
[
self
.
_reload_item
(
section
.
location
)
for
section
in
self
.
sections
]
self
.
subsections
=
[
self
.
_reload_item
(
subsection
.
location
)
for
subsection
in
self
.
subsections
]
self
.
verticals
=
[
self
.
_reload_item
(
vertical
.
location
)
for
vertical
in
self
.
verticals
]
self
.
icrv
=
self
.
_reload_item
(
self
.
icrv
.
location
)
self
.
sibling_problem
=
self
.
_reload_item
(
self
.
sibling_problem
.
location
)
def
_reload_item
(
self
,
location
):
"""Safely reload an item from the moduelstore. """
try
:
return
self
.
store
.
get_item
(
location
)
except
ItemNotFoundError
:
return
None
@attr
(
shard
=
2
)
class
WriteOnPublishTest
(
ModuleStoreTestCase
):
"""
Verify that updates to the course descriptor's
user partitions are written automatically on publish.
"""
MODULESTORE
=
TEST_DATA_SPLIT_MODULESTORE
ENABLED_SIGNALS
=
[
'course_published'
,
'pre_publish'
]
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
setUp
(
self
):
super
(
WriteOnPublishTest
,
self
)
.
setUp
()
# Create a dummy course with an ICRV block
self
.
course
=
CourseFactory
()
self
.
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
display_name
=
'Test Section'
)
self
.
subsection
=
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'sequential'
,
display_name
=
'Test Subsection'
)
self
.
vertical
=
ItemFactory
.
create
(
parent
=
self
.
subsection
,
category
=
'vertical'
,
display_name
=
'Test Unit'
)
self
.
icrv
=
ItemFactory
.
create
(
parent
=
self
.
vertical
,
category
=
'edx-reverification-block'
)
# Mark the course as credit
CreditCourse
.
objects
.
create
(
course_key
=
self
.
course
.
id
,
enabled
=
True
)
# pylint: disable=no-member
@patch.dict
(
settings
.
FEATURES
,
{
"ENABLE_COURSEWARE_INDEX"
:
False
})
def
test_can_write_on_publish_signal
(
self
):
# Sanity check -- initially user partitions should be empty
self
.
assertEqual
(
self
.
course
.
user_partitions
,
[])
# Make and publish a change to a block, which should trigger the publish signal
with
self
.
store
.
bulk_operations
(
self
.
course
.
id
):
# pylint: disable=no-member
self
.
icrv
.
display_name
=
"Updated display name"
self
.
store
.
update_item
(
self
.
icrv
,
ModuleStoreEnum
.
UserID
.
test
)
self
.
store
.
publish
(
self
.
icrv
.
location
,
ModuleStoreEnum
.
UserID
.
test
)
# Within the test, the course pre-publish signal should have fired synchronously
# Since the course is marked as credit, the in-course verification partitions
# should have been created.
# We need to verify that these changes were actually persisted to the modulestore.
retrieved_course
=
self
.
store
.
get_course
(
self
.
course
.
id
)
# pylint: disable=no-member
self
.
assertEqual
(
len
(
retrieved_course
.
user_partitions
),
1
)
openedx/core/djangoapps/credit/verification_access.py
deleted
100644 → 0
View file @
602b737b
"""
Create in-course reverification access groups in a course.
We model the rules as a set of user partitions, one for each
verification checkpoint in a course.
For example, suppose that a course has two verification checkpoints,
one at midterm A and one at the midterm B.
Then the user partitions would look like this:
Midterm A: |-- ALLOW --|-- DENY --|
Midterm B: |-- ALLOW --|-- DENY --|
where the groups are defined as:
* ALLOW: The user has access to content gated by the checkpoint.
* DENY: The user does not have access to content gated by the checkpoint.
"""
import
logging
from
util.db
import
generate_int_id
from
openedx.core.djangoapps.credit.utils
import
get_course_blocks
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.partitions.partitions
import
Group
,
UserPartition
log
=
logging
.
getLogger
(
__name__
)
VERIFICATION_SCHEME_NAME
=
"verification"
VERIFICATION_BLOCK_CATEGORY
=
"edx-reverification-block"
def
update_verification_partitions
(
course_key
):
"""
Create a user partition for each verification checkpoint in the course.
This will modify the published version of the course descriptor.
It ensures that any in-course reverification XBlocks in the course
have an associated user partition. Other user partitions (e.g. cohorts)
will be preserved. Partitions associated with deleted reverification checkpoints
will be marked as inactive and will not be used to restrict access.
Arguments:
course_key (CourseKey): identifier for the course.
Returns:
None
"""
# Batch all the queries we're about to do and suppress
# the "publish" signal to avoid an infinite call loop.
with
modulestore
()
.
bulk_operations
(
course_key
,
emit_signals
=
False
):
# Retrieve all in-course reverification blocks in the course
icrv_blocks
=
get_course_blocks
(
course_key
,
VERIFICATION_BLOCK_CATEGORY
)
# Update the verification definitions in the course descriptor
# This will also clean out old verification partitions if checkpoints
# have been deleted.
_set_verification_partitions
(
course_key
,
icrv_blocks
)
def
_unique_partition_id
(
course
):
"""Return a unique user partition ID for the course. """
# Exclude all previously used IDs, even for partitions that have been disabled
# (e.g. if the course author deleted an in-course reverifification block but
# there are courseware components that reference the disabled partition).
used_ids
=
set
(
p
.
id
for
p
in
course
.
user_partitions
)
return
generate_int_id
(
used_ids
=
used_ids
)
def
_other_partitions
(
verified_partitions
,
exclude_partitions
,
course_key
):
"""
Retrieve all partitions NOT associated with the current set of ICRV blocks.
Any partition associated with a deleted ICRV block will be marked as inactive
so its access rules will no longer be enforced.
Arguments:
all_partitions (list of UserPartition): All verified partitions defined in the course.
exclude_partitions (list of UserPartition): Partitions to exclude (e.g. the ICRV partitions already added)
course_key (CourseKey): Identifier for the course (used for logging).
Returns: list of `UserPartition`s
"""
results
=
[]
partition_by_id
=
{
p
.
id
:
p
for
p
in
verified_partitions
}
other_partition_ids
=
set
(
p
.
id
for
p
in
verified_partitions
)
-
set
(
p
.
id
for
p
in
exclude_partitions
)
for
pid
in
other_partition_ids
:
partition
=
partition_by_id
[
pid
]
results
.
append
(
UserPartition
(
id
=
partition
.
id
,
name
=
partition
.
name
,
description
=
partition
.
description
,
scheme
=
partition
.
scheme
,
parameters
=
partition
.
parameters
,
groups
=
partition
.
groups
,
active
=
False
,
)
)
log
.
info
(
(
"Disabled partition
%
s in course
%
s because the "
"associated in-course-reverification checkpoint does not exist."
),
partition
.
id
,
course_key
)
return
results
def
_set_verification_partitions
(
course_key
,
icrv_blocks
):
"""
Create or update user partitions in the course.
Ensures that each ICRV block in the course has an associated user partition
with the groups ALLOW and DENY.
Arguments:
course_key (CourseKey): Identifier for the course.
icrv_blocks (list of XBlock): In-course reverification blocks, e.g. reverification checkpoints.
Returns:
list of UserPartition
"""
scheme
=
UserPartition
.
get_scheme
(
VERIFICATION_SCHEME_NAME
)
if
scheme
is
None
:
log
.
error
(
"Could not retrieve user partition scheme with ID
%
s"
,
VERIFICATION_SCHEME_NAME
)
return
[]
course
=
modulestore
()
.
get_course
(
course_key
)
if
course
is
None
:
log
.
error
(
"Could not find course
%
s"
,
course_key
)
return
[]
verified_partitions
=
[
p
for
p
in
course
.
user_partitions
if
p
.
scheme
==
scheme
]
partition_id_for_location
=
{
p
.
parameters
[
"location"
]:
p
.
id
for
p
in
verified_partitions
if
"location"
in
p
.
parameters
}
partitions
=
[]
for
block
in
icrv_blocks
:
partition
=
UserPartition
(
id
=
partition_id_for_location
.
get
(
unicode
(
block
.
location
),
_unique_partition_id
(
course
)
),
name
=
block
.
related_assessment
,
description
=
u"Verification checkpoint at {}"
.
format
(
block
.
related_assessment
),
scheme
=
scheme
,
parameters
=
{
"location"
:
unicode
(
block
.
location
)},
groups
=
[
Group
(
scheme
.
ALLOW
,
"Completed verification at {}"
.
format
(
block
.
related_assessment
)),
Group
(
scheme
.
DENY
,
"Did not complete verification at {}"
.
format
(
block
.
related_assessment
)),
]
)
partitions
.
append
(
partition
)
log
.
info
(
(
"Configured partition
%
s for course
%
s using a verified partition scheme "
"for the in-course-reverification checkpoint at location
%
s"
),
partition
.
id
,
course_key
,
partition
.
parameters
[
"location"
]
)
# Preserve existing, non-verified partitions from the course
# Mark partitions for deleted in-course reverification as disabled.
partitions
+=
_other_partitions
(
verified_partitions
,
partitions
,
course_key
)
course
.
set_user_partitions_for_scheme
(
partitions
,
scheme
)
modulestore
()
.
update_item
(
course
,
ModuleStoreEnum
.
UserID
.
system
)
log
.
info
(
"Saved updated partitions for the course
%
s"
,
course_key
)
return
partitions
openedx/core/djangoapps/user_api/partition_schemes.py
View file @
7e368e10
...
...
@@ -10,6 +10,20 @@ from xmodule.partitions.partitions import UserPartitionError, NoSuchUserPartitio
log
=
logging
.
getLogger
(
__name__
)
class
NotImplementedPartitionScheme
(
object
):
"""
This "scheme" allows previously-defined schemes to be purged, while giving existing
course data definitions a safe entry point to load.
"""
@classmethod
def
get_group_for_user
(
cls
,
course_key
,
user
,
user_partition
,
assign
=
True
,
track_function
=
None
):
"""
Dummy method, will fail hard if anyone tries to use this scheme.
"""
raise
NotImplementedError
()
class
RandomUserPartitionScheme
(
object
):
"""
This scheme randomly assigns users into the partition's groups.
...
...
openedx/core/djangolib/model_mixins.py
0 → 100644
View file @
7e368e10
"""
Custom Django Model mixins.
"""
class
DeprecatedModelMixin
(
object
):
"""
Used to make a class unusable in practice, but leave database tables intact.
"""
def
__init__
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Override to kill usage of this model.
"""
raise
TypeError
(
"This model has been deprecated and should not be used."
)
requirements/edx/github.txt
View file @
7e368e10
...
...
@@ -86,7 +86,6 @@ git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd3
git+https://github.com/edx/edx-milestones.git@v0.1.10#egg=edx-milestones==0.1.10
git+https://github.com/edx/xblock-utils.git@v1.0.4#egg=xblock-utils==1.0.4
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.2#egg=lti_consumer-xblock==1.1.2
git+https://github.com/edx/edx-proctoring.git@0.18.0#egg=edx-proctoring==0.18.0
...
...
setup.py
View file @
7e368e10
...
...
@@ -41,7 +41,7 @@ setup(
"openedx.user_partition_scheme"
:
[
"random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme"
,
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme"
,
"verification = openedx.core.djangoapps.
credit.partition_schemes:Verification
PartitionScheme"
,
"verification = openedx.core.djangoapps.
user_api.partition_schemes:NotImplemented
PartitionScheme"
,
"enrollment_track = openedx.core.djangoapps.verified_track_content.partition_scheme:EnrollmentTrackPartitionScheme"
,
],
"openedx.block_structure_transformer"
:
[
...
...
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