Commit 19334c27 by Andy Armstrong Committed by GitHub

Merge pull request #324 from edx/andya/add-transifex

Upgrade proctoring to use Transifex
parents e6d6b051 2fd98192
......@@ -6,6 +6,7 @@
# Installation artifacts
src
requirements/*.txt
# Sphinx docs:
build
......@@ -47,10 +48,13 @@ Desktop.ini
# Test Artifacts
.coverage
coverage.xml
nosetests.xml
reports/
coverage/
htmlcov/
.cache/
.tox/
acceptance_tests/*.png
node_modules/
......
language: python
cache: pip
python:
- "2.7"
- 2.7
env:
- DJANGO_SETTINGS_MODULE=settings
- DJANGO_SETTINGS_MODULE=test_settings
before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
# Set Travis builds to use docker container
sudo: false
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- pip install --upgrade pip # pip-tools requires pip >= 6.1
install:
- npm install
- "pip install -r local_requirements.txt"
- "pip install -r requirements.txt"
- "pip install -r test_requirements.txt"
- "pip install coveralls"
- pip install setuptools==32.3.1 # need newer version than Travis default
- make install
script:
- coverage run ./manage.py test edx_proctoring
- coverage report -m
- gulp test
- pep8 edx_proctoring
- pylint edx_proctoring --report=no
after_success: coveralls
- make test-all
- gulp test
after_success:
- coverage xml
- bash <(curl -s https://codecov.io/bash) -cF python -f coverage.xml
- bash <(curl -s https://codecov.io/bash) -cF javascript -f build/coverage-js/coverage.xml
[main]
host = https://www.transifex.com
[edx-platform.edx-proctoring]
file_filter = edx_proctoring/locale/<lang>/LC_MESSAGES/django.po
source_file = edx_proctoring/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO
[edx-platform.edx-proctoring-js]
file_filter = edx_proctoring/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = edx_proctoring/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO
We don't maintain a detailed changelog. For details of changes, please see
either the `GitHub commit history`_.
.. _GitHub commit history: https://github.com/edx/edx-proctoring/commits/master
GNU AFFERO GENERAL PUBLIC LICENSE
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
......@@ -669,3 +670,5 @@ of the license. "Corresponding Source" of this work, or works based on this
work, as defined by the terms of this license do not include source code
files for programs used solely to provide those public, independently
available web services.
include AUTHORS
include CHANGELOG.rst
include CONTRIBUTING.rst
include LICENSE.txt
include README.rst
graft edx_proctoring
prune edx_proctoring/static/proctoring/spec
global-exclude *.pyc
.PHONY: help upgrade requirements clean quality requirements docs \
test test-all coverage \
compile_translations dummy_translations extract_translations \
fake_translations pull_translations push_translations
.DEFAULT_GOAL := help
define BROWSER_PYSCRIPT
import os, webbrowser, sys
try:
from urllib import pathname2url
except:
from urllib.request import pathname2url
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT
BROWSER := python -c "$$BROWSER_PYSCRIPT"
help: ## display this help message
@echo "Please use \`make <target>' where <target> is one of"
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}'
clean: ## remove generated byte code, coverage reports, and build artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
rm -fr build/
rm -fr dist/
rm -fr *.egg-info
upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip install -q pip-tools
pip-compile --upgrade -o requirements/base.txt requirements/base.in
pip-compile --upgrade -o requirements/dev.txt requirements/dev.in requirements/quality.in
pip-compile --upgrade -o requirements/doc.txt requirements/base.in requirements/doc.in
pip-compile --upgrade -o requirements/quality.txt requirements/quality.in
pip-compile --upgrade -o requirements/test.txt requirements/base.in requirements/test.in
# Let tox control the Django version for tests
sed '/django==/d' requirements/test.txt > requirements/test.tmp
mv requirements/test.tmp requirements/test.txt
requirements: ## install development environment requirements
pip install -qr requirements/dev.txt --exists-action w
pip-sync requirements/*.txt requirements/private.*
install: upgrade requirements
./manage.py syncdb --noinput --settings=test_settings
./manage.py migrate --settings=test_settings
npm install
coverage: clean ## generate and view HTML coverage report
py.test --cov-report html
$(BROWSER) htmlcov/index.html
docs: ## generate Sphinx HTML documentation, including API docs
tox -e docs
$(BROWSER) docs/_build/html/index.html
compile_translations: ## compile translation files, outputting .po files for each supported language
./manage.py compilemessages
dummy_translations: ## generate dummy translation (.po) files
i18n_tool dummy --config ./edx_proctoring/locale/config.yaml
extract_translations: ## extract strings to be translated, outputting .mo files
rm -rf docs/_build
./manage.py makemessages -l en -v1 -d django --ignore="edx-proctoring/*"
./manage.py makemessages -l en -v1 -d djangojs --ignore="edx-proctoring/*"
fake_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files
pull_translations: ## pull translations from Transifex
tx pull -a
push_translations: ## push source translation files (.po) from Transifex
tx push -s
validate_translations: fake_translations detect_changed_source_translations
quality: ## check coding style with pycodestyle and pylint
tox -e quality
test-python: clean ## run tests in the current virtualenv
./manage.py test edx_proctoring --verbosity=3
test-js:
gulp test
test-all: ## run tests on every supported Python/Django combination
tox -e quality
tox
diff_cover: test
diff-cover coverage.xml
edx-proctoring [![Build Status](https://travis-ci.org/edx/edx-proctoring.svg?branch=master)](https://travis-ci.org/edx/edx-proctoring) [![Coverage Status](https://coveralls.io/repos/edx/edx-proctoring/badge.svg?branch=master&service=github)](https://coveralls.io/github/edx/edx-proctoring?branch=master)
========================
This is the Exam Proctoring subsystem for the Open edX platform.
While technical and developer documentation is forthcoming, here are basic instructions on how to use this
in an Open edX installation.
NOTE: Proctoring will not be available in the Open edX named releases until Dogwood. However, you can use this if you use a copy of edx-platform (master) after 8/20/2015.
In order to use edx-proctoring, you must obtain an account (and secret configuration - see below) with SoftwareSecure, which provides the proctoring review services that edx-proctoring integrates with.
CONFIGURATION:
You will need to turn on the ENABLE_SPECIAL_EXAMS in lms.env.json and cms.env.json FEATURES dictionary:
```
:
"FEATURES": {
:
"ENABLE_SPECIAL_EXAMS": true,
:
}
```
Also in your lms.env.json and cms.env.json file please add the following:
```
"PROCTORING_SETTINGS": {
"LINK_URLS": {
"contact_us": "{add link here}",
"faq": "{add link here}",
"online_proctoring_rules": "{add link here}",
"tech_requirements": "{add link here}"
}
},
```
In your lms.auth.json file, please add the following *secure* information:
```
"PROCTORING_BACKEND_PROVIDER": {
"class": "edx_proctoring.backends.software_secure.SoftwareSecureBackendProvider",
"options": {
"crypto_key": "{add SoftwareSecure crypto key here}",
"exam_register_endpoint": "{add enpoint to SoftwareSecure}",
"exam_sponsor": "{add SoftwareSecure sponsor}",
"organization": "{add SoftwareSecure organization}",
"secret_key": "{add SoftwareSecure secret key}",
"secret_key_id": "{add SoftwareSecure secret key id}",
"software_download_url": "{add SoftwareSecure download url}"
}
},
```
You will need to restart services after these configuration changes for them to take effect.
django-component-views
=============================
.. image:: https://img.shields.io/pypi/v/edx-proctoring.svg
:target: https://pypi.python.org/pypi/edx-proctoring/
:alt: PyPI
.. image:: https://travis-ci.org/edx/edx-proctoring.svg?branch=master
:target: https://travis-ci.org/edx/edx-proctoring
:alt: Travis
.. image:: https://codecov.io/gh/edx/edx-proctoring/branch/master/graph/badge.svg
:target: https://codecov.io/gh/edx/edx-proctoring
:alt: Codecov
.. image:: https://img.shields.io/pypi/pyversions/edx-proctoring.svg
:target: https://pypi.python.org/pypi/edx-proctoring/
:alt: Supported Python versions
.. image:: https://img.shields.io/github/license/edx/django-component-views.svg
:target: https://github.com/edx/edx-proctoring/blob/master/LICENSE.txt
:alt: License
This is the exam proctoring subsystem for the Open edX platform.
Overview
--------
Proctored exams are exams with time limits that learners complete while online
proctoring software monitors their computers and behavior for activity that
might be evidence of cheating. This Python library provides the proctoring
implementation used by Open edX.
Documentation
-------------
For documentation about taking a proctored exam, see `Taking a Proctored Exam`_.
For authoring documentation, see `Including Proctored Exams In Your Course`_.
Installation
------------
To install edx-proctoring:
mkvirtualenv edx-proctoring
make install
To run the tests:
make test-all
For a full list of Make targets:
make help
Configuration
-------------
In order to use edx-proctoring, you must obtain an account (and secret
configuration - see below) with SoftwareSecure, which provides the proctoring
review services that edx-proctoring integrates with.
You will need to turn on the ENABLE_SPECIAL_EXAMS in lms.env.json and
cms.env.json FEATURES dictionary::
"FEATURES": {
:
"ENABLE_SPECIAL_EXAMS": true,
:
}
Also in your lms.env.json and cms.env.json file please add the following::
"PROCTORING_SETTINGS": {
"LINK_URLS": {
"contact_us": "{add link here}",
"faq": "{add link here}",
"online_proctoring_rules": "{add link here}",
"tech_requirements": "{add link here}"
}
},
In your lms.auth.json file, please add the following *secure* information::
"PROCTORING_BACKEND_PROVIDER": {
"class": "edx_proctoring.backends.software_secure.SoftwareSecureBackendProvider",
"options": {
"crypto_key": "{add SoftwareSecure crypto key here}",
"exam_register_endpoint": "{add enpoint to SoftwareSecure}",
"exam_sponsor": "{add SoftwareSecure sponsor}",
"organization": "{add SoftwareSecure organization}",
"secret_key": "{add SoftwareSecure secret key}",
"secret_key_id": "{add SoftwareSecure secret key id}",
"software_download_url": "{add SoftwareSecure download url}"
}
},
You will need to restart services after these configuration changes for them to
take effect.
License
-------
The code in this repository is licensed under the AGPL 3.0 unless
otherwise noted.
Please see ``LICENSE.txt`` for details.
How To Contribute
-----------------
Contributions are very welcome.
Please read `How To Contribute <https://github.com/edx/edx-platform/blob/master/CONTRIBUTING.rst>`_ for details.
Even though they were written with ``edx-platform`` in mind, the guidelines
should be followed for Open edX code in general.
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@edx.org.
Getting Help
------------
Have a question about this repository, or about Open edX in general? Please
refer to this `list of resources`_ if you need any assistance.
.. _list of resources: https://open.edx.org/getting-help
.. _Including Proctored Exams In Your Course: http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/course_features/credit_courses/proctored_exams.html
coverage:
status:
project:
default:
enabled: yes
target: auto
patch:
default:
enabled: yes
target: 100%
comment: false
"""
Init file for the top level edx_proctoring directory
The exam proctoring subsystem for the Open edX platform.
"""
from __future__ import absolute_import
__version__ = '0.17.0'
default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
......@@ -3,8 +3,10 @@ Django Admin pages
"""
# pylint: disable=no-self-argument, no-member
import pytz
from __future__ import absolute_import
from datetime import datetime, timedelta
import pytz
from django import forms
from django.db.models import Q
......@@ -476,6 +478,7 @@ def prettify_course_id(course_id):
"""
return course_id.replace('+', ' ').replace('/', ' ').replace('course-v1:', '')
admin.site.register(ProctoredExamStudentAttempt, ProctoredExamStudentAttemptAdmin)
admin.site.register(ProctoredExamReviewPolicy, ProctoredExamReviewPolicyAdmin)
admin.site.register(ProctoredExamSoftwareSecureReview, ProctoredExamSoftwareSecureReviewAdmin)
......
......@@ -4,13 +4,14 @@
In-Proc API (aka Library) for the edx_proctoring subsystem. This is not to be confused with a HTTP REST
API which is in the views.py file, per edX coding standards
"""
import pytz
import uuid
import logging
from __future__ import absolute_import
from datetime import datetime, timedelta
import logging
import uuid
import pytz
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _, ugettext_noop
from django.conf import settings
from django.template import Context, loader
from django.core.urlresolvers import reverse, NoReverseMatch
......@@ -1280,47 +1281,47 @@ def _resolve_prerequisite_links(exam, prerequisites):
STATUS_SUMMARY_MAP = {
'_default': {
'short_description': _('Taking As Proctored Exam'),
'short_description': ugettext_noop('Taking As Proctored Exam'),
'suggested_icon': 'fa-pencil-square-o',
'in_completed_state': False
},
ProctoredExamStudentAttemptStatus.eligible: {
'short_description': _('Proctored Option Available'),
'short_description': ugettext_noop('Proctored Option Available'),
'suggested_icon': 'fa-pencil-square-o',
'in_completed_state': False
},
ProctoredExamStudentAttemptStatus.declined: {
'short_description': _('Taking As Open Exam'),
'short_description': ugettext_noop('Taking As Open Exam'),
'suggested_icon': 'fa-pencil-square-o',
'in_completed_state': False
},
ProctoredExamStudentAttemptStatus.submitted: {
'short_description': _('Pending Session Review'),
'short_description': ugettext_noop('Pending Session Review'),
'suggested_icon': 'fa-spinner fa-spin',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.second_review_required: {
'short_description': _('Pending Session Review'),
'short_description': ugettext_noop('Pending Session Review'),
'suggested_icon': 'fa-spinner fa-spin',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.verified: {
'short_description': _('Passed Proctoring'),
'short_description': ugettext_noop('Passed Proctoring'),
'suggested_icon': 'fa-check',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.rejected: {
'short_description': _('Failed Proctoring'),
'short_description': ugettext_noop('Failed Proctoring'),
'suggested_icon': 'fa-exclamation-triangle',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.error: {
'short_description': _('Failed Proctoring'),
'short_description': ugettext_noop('Failed Proctoring'),
'suggested_icon': 'fa-exclamation-triangle',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.expired: {
'short_description': _('Proctored Option No Longer Available'),
'short_description': ugettext_noop('Proctored Option No Longer Available'),
'suggested_icon': 'fa-times-circle',
'in_completed_state': False
}
......@@ -1329,17 +1330,17 @@ STATUS_SUMMARY_MAP = {
PRACTICE_STATUS_SUMMARY_MAP = {
'_default': {
'short_description': _('Ungraded Practice Exam'),
'short_description': ugettext_noop('Ungraded Practice Exam'),
'suggested_icon': '',
'in_completed_state': False
},
ProctoredExamStudentAttemptStatus.submitted: {
'short_description': _('Practice Exam Completed'),
'short_description': ugettext_noop('Practice Exam Completed'),
'suggested_icon': 'fa-check',
'in_completed_state': True
},
ProctoredExamStudentAttemptStatus.error: {
'short_description': _('Practice Exam Failed'),
'short_description': ugettext_noop('Practice Exam Failed'),
'suggested_icon': 'fa-exclamation-triangle',
'in_completed_state': True
}
......@@ -1347,7 +1348,7 @@ PRACTICE_STATUS_SUMMARY_MAP = {
TIMED_EXAM_STATUS_SUMMARY_MAP = {
'_default': {
'short_description': _('Timed Exam'),
'short_description': ugettext_noop('Timed Exam'),
'suggested_icon': 'fa-clock-o',
'in_completed_state': False
}
......@@ -1382,7 +1383,13 @@ def get_attempt_status_summary(user_id, course_id, content_id):
# check if the exam is not proctored
if not exam['is_proctored']:
return TIMED_EXAM_STATUS_SUMMARY_MAP['_default']
summary = {}
summary.update(TIMED_EXAM_STATUS_SUMMARY_MAP['_default'])
# Note: translate the short description as it was stored unlocalized
summary.update({
'short_description': _(summary['short_description']) # pylint: disable=translation-of-non-string
})
return summary
# let's check credit eligibility
credit_service = get_runtime_service('credit')
......@@ -1403,13 +1410,17 @@ def get_attempt_status_summary(user_id, course_id, content_id):
status_map = STATUS_SUMMARY_MAP if not exam['is_practice_exam'] else PRACTICE_STATUS_SUMMARY_MAP
summary = None
summary = {}
if status in status_map:
summary = status_map[status]
summary.update(status_map[status])
else:
summary = status_map['_default']
summary.update(status_map['_default'])
summary.update({"status": status})
# Note: translate the short description as it was stored unlocalized
summary.update({
'status': status,
'short_description': _(summary['short_description']) # pylint: disable=translation-of-non-string
})
return summary
......
# -*- coding: utf-8 -*-
"""
edx_proctoring Django application initialization.
"""
from __future__ import absolute_import
from django.apps import AppConfig
class EdxProctoringConfig(AppConfig):
"""
Configuration for the edx_proctoring Django application.
"""
name = 'edx_proctoring'
......@@ -2,6 +2,8 @@
All supporting Proctoring backends
"""
from __future__ import absolute_import
from importlib import import_module
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
......@@ -21,7 +23,7 @@ def get_backend_provider(emphemeral=False):
provider = _BACKEND_PROVIDER
if not _BACKEND_PROVIDER or emphemeral:
config = getattr(settings, 'PROCTORING_BACKEND_PROVIDER')
config = getattr(settings, 'PROCTORING_BACKEND_PROVIDER') # pylint: disable=literal-used-as-attribute
if not config:
raise ImproperlyConfigured("Settings not configured with PROCTORING_BACKEND_PROVIDER!")
......
......@@ -2,6 +2,8 @@
Defines the abstract base class that all backends should derive from
"""
from __future__ import absolute_import
import abc
......
......@@ -2,7 +2,11 @@
Implements a mock proctoring backend provider to be used for testing,
which doesn't require the setup and configuration of the Software Secure backend provider.
"""
from __future__ import absolute_import
import threading
from edx_proctoring.callbacks import start_exam_callback
from edx_proctoring.backends.backend import ProctoringBackendProvider
......
......@@ -2,6 +2,8 @@
Implementation of a backend provider, which does nothing
"""
from __future__ import absolute_import
from edx_proctoring.backends.backend import ProctoringBackendProvider
......
......@@ -2,19 +2,23 @@
Integration with Software Secure's proctoring system
"""
from Crypto.Cipher import DES3
from __future__ import absolute_import
import base64
from hashlib import sha256
import requests
import hmac
import binascii
import datetime
from hashlib import sha256
import hmac
import json
import logging
import unicodedata
import requests
from django.conf import settings
from Crypto.Cipher import DES3
from edx_proctoring.backends.backend import ProctoringBackendProvider
from edx_proctoring import constants
from edx_proctoring.exceptions import (
......
"""
Tests for backend.py
"""
from __future__ import absolute_import
import time
from mock import patch
from django.test import TestCase
from edx_proctoring.backends.backend import ProctoringBackendProvider
from edx_proctoring.backends.null import NullBackendProvider
from edx_proctoring.backends.mock import MockProctoringBackendProvider
......
......@@ -2,94 +2,101 @@
Some canned data for SoftwareSecure callback testing.
"""
TEST_REVIEW_PAYLOAD = '''
{
"examDate": "Jul 15 2015 1:13AM",
"examProcessingStatus": "Review Completed",
"examTakerEmail": "4d07a01a-1502-422e-b943-93ac04dc6ced",
"examTakerFirstName": "John",
"examTakerLastName": "Doe",
"keySetVersion": "",
"examApiData": {
"duration": 1,
"examCode": "4d07a01a-1502-422e-b943-93ac04dc6ced",
"examName": "edX Exams",
"examPassword": "hQxvA8iUKKlsqKt0fQVBaXqmAziGug4NfxUChg94eGacYDcFwaIyBA==",
"examSponsor": "edx LMS",
"examUrl": "http://localhost:8000/api/edx_proctoring/proctoring_launch_callback/start_exam/4d07a01a-1502-422e-b943-93ac04dc6ced",
"orgExtra": {
"courseID": "edX/DemoX/Demo_Course",
"examEndDate": "Wed, 15 Jul 2015 05:11:31 GMT",
"examID": 6,
"examStartDate": "Wed, 15 Jul 2015 05:10:31 GMT",
"noOfStudents": 1
},
"organization": "edx",
"reviewedExam": true,
"reviewerNotes": "Closed Book",
"ssiProduct": "rp-now"
},
"overAllComments": ";Candidates should always wear suit and tie for exams.",
"reviewStatus": "Clean",
"userPhotoBase64String": "",
"videoReviewLink": "http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo",
"examMetaData": {
"examCode": "$attempt_code",
"examName": "edX Exams",
"examSponsor": "edx LMS",
"organization": "edx",
"reviewedExam": "True",
"reviewerNotes": "Closed Book",
"simulatedExam": "False",
"ssiExamToken": "4E44F7AA-275D-4741-B531-73AE2407ECFB",
"ssiProduct": "rp-now",
"ssiRecordLocator": "$external_id"
},
"desktopComments": [
{
"comments": "Browsing other websites",
"duration": 88,
"eventFinish": 88,
"eventStart": 12,
"eventStatus": "Suspicious"
},
{
"comments": "Browsing local computer",
"duration": 88,
"eventFinish": 88,
"eventStart": 15,
"eventStatus": "Rules Violation"
},
{
"comments": "Student never entered the exam.",
"duration": 88,
"eventFinish": 88,
"eventStart": 87,
"eventStatus": "Clean"
}
],
"webCamComments": [
{
"comments": "Photo ID not provided",
"duration": 796,
"eventFinish": 796,
"eventStart": 0,
"eventStatus": "Suspicious"
},
{
"comments": "Exam environment not confirmed",
"duration": 796,
"eventFinish": 796,
"eventStart": 10,
"eventStatus": "Rules Violation"
},
{
"comments": "Looking away from computer",
"duration": 796,
"eventFinish": 796,
"eventStart": 107,
"eventStatus": "Rules Violation"
}
]
}
'''
import json
MOCK_EXAM_ID = "4d07a01a-1502-422e-b943-93ac04dc6ced"
def create_test_review_payload(exam_id=MOCK_EXAM_ID, attempt_code=None, external_id=None):
"""
Returns a test payload for reviews.
"""
return json.dumps({
"examDate": "Jul 15 2015 1:13AM",
"examProcessingStatus": "Review Completed",
"examTakerEmail": "4d07a01a-1502-422e-b943-93ac04dc6ced",
"examTakerFirstName": "John",
"examTakerLastName": "Doe",
"keySetVersion": "",
"examApiData": {
"duration": 1,
"examCode": "4d07a01a-1502-422e-b943-93ac04dc6ced",
"examName": "edX Exams",
"examPassword": "hQxvA8iUKKlsqKt0fQVBaXqmAziGug4NfxUChg94eGacYDcFwaIyBA==",
"examSponsor": "edx LMS",
"examUrl": "http://localhost:8000/api/edx_proctoring/proctoring_launch_callback/start_exam/" + exam_id,
"orgExtra": {
"courseID": "edX/DemoX/Demo_Course",
"examEndDate": "Wed, 15 Jul 2015 05:11:31 GMT",
"examID": exam_id,
"examStartDate": "Wed, 15 Jul 2015 05:10:31 GMT",
"noOfStudents": 1
},
"organization": "edx",
"reviewedExam": True,
"reviewerNotes": "Closed Book",
"ssiProduct": "rp-now"
},
"overAllComments": ";Candidates should always wear suit and tie for exams.",
"reviewStatus": "Clean",
"userPhotoBase64String": "",
"videoReviewLink": "http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo",
"examMetaData": {
"examCode": attempt_code,
"examName": "edX Exams",
"examSponsor": "edx LMS",
"organization": "edx",
"reviewedExam": "True",
"reviewerNotes": "Closed Book",
"simulatedExam": "False",
"ssiExamToken": "4E44F7AA-275D-4741-B531-73AE2407ECFB",
"ssiProduct": "rp-now",
"ssiRecordLocator": external_id
},
"desktopComments": [
{
"comments": "Browsing other websites",
"duration": 88,
"eventFinish": 88,
"eventStart": 12,
"eventStatus": "Suspicious"
},
{
"comments": "Browsing local computer",
"duration": 88,
"eventFinish": 88,
"eventStart": 15,
"eventStatus": "Rules Violation"
},
{
"comments": "Student never entered the exam.",
"duration": 88,
"eventFinish": 88,
"eventStart": 87,
"eventStatus": "Clean"
}
],
"webCamComments": [
{
"comments": "Photo ID not provided",
"duration": 796,
"eventFinish": 796,
"eventStart": 0,
"eventStatus": "Suspicious"
},
{
"comments": "Exam environment not confirmed",
"duration": 796,
"eventFinish": 796,
"eventStart": 10,
"eventStatus": "Rules Violation"
},
{
"comments": "Looking away from computer",
"duration": 796,
"eventFinish": 796,
"eventStart": 107,
"eventStatus": "Rules Violation"
}
]
})
......@@ -4,9 +4,10 @@
Tests for the software_secure module
"""
from __future__ import absolute_import
import json
import ddt
from string import Template # pylint: disable=deprecated-module
from mock import patch
from httmock import all_requests, HTTMock
......@@ -41,10 +42,7 @@ from edx_proctoring.models import (
ProctoredExamStudentAttemptHistory,
ProctoredExamStudentAllowance
)
from edx_proctoring.backends.tests.test_review_payload import (
TEST_REVIEW_PAYLOAD
)
from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload
from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService
from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS
......@@ -110,6 +108,7 @@ class SoftwareSecureTests(TestCase):
"""
When tests are done
"""
super(SoftwareSecureTests, self).tearDown()
set_runtime_service('credit', None)
def test_provider_instance(self):
......@@ -229,7 +228,7 @@ class SoftwareSecureTests(TestCase):
self.assertNotIn('review_policy', context)
# call into real implementation
result = get_backend_provider(emphemeral=True)._get_payload(exam, context) # pylint: disable=protected-access
result = get_backend_provider(emphemeral=True)._get_payload(exam, context)
# assert that we use the default that is defined in system configuration
self.assertEqual(result['reviewerNotes'], constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)
......@@ -256,7 +255,7 @@ class SoftwareSecureTests(TestCase):
# patch the _get_payload method on the backend provider
# so that we can assert that we are called with the review policy
# undefined and that we use the system default
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_no_policy): # pylint: disable=protected-access
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_no_policy):
attempt_id = create_exam_attempt(
exam_id,
self.user.id,
......@@ -291,7 +290,7 @@ class SoftwareSecureTests(TestCase):
"""
# call into real implementation
result = get_backend_provider(emphemeral=True)._get_payload(exam, context) # pylint: disable=protected-access
result = get_backend_provider(emphemeral=True)._get_payload(exam, context)
self.assertFalse(isinstance(result['examName'], unicode))
self.assertTrue(is_ascii(result['examName']))
self.assertGreater(len(result['examName']), 0)
......@@ -308,7 +307,7 @@ class SoftwareSecureTests(TestCase):
)
# patch the _get_payload method on the backend provider
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_unicode_characters): # pylint: disable=protected-access
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_unicode_characters):
attempt_id = create_exam_attempt(
exam_id,
self.user.id,
......@@ -326,7 +325,7 @@ class SoftwareSecureTests(TestCase):
)
# patch the _get_payload method on the backend provider
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_unicode_characters): # pylint: disable=protected-access
with patch.object(get_backend_provider(), '_get_payload', assert_get_payload_mock_unicode_characters):
attempt_id = create_exam_attempt(
exam_id,
self.user.id,
......@@ -475,7 +474,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -515,7 +514,7 @@ class SoftwareSecureTests(TestCase):
"""
provider = get_backend_provider()
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code='not-here',
external_id='also-not-here'
)
......@@ -530,7 +529,7 @@ class SoftwareSecureTests(TestCase):
"""
provider = get_backend_provider()
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code='not-here',
external_id='also-not-here'
)
......@@ -567,7 +566,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id='bogus'
)
......@@ -604,7 +603,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id='bogus'
)
......@@ -644,7 +643,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -698,7 +697,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -736,7 +735,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -801,7 +800,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -859,7 +858,7 @@ class SoftwareSecureTests(TestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......
......@@ -2,9 +2,12 @@
Lists of constants that can be used in the edX proctoring
"""
from django.conf import settings
from __future__ import absolute_import
import datetime
from django.conf import settings
SITE_NAME = (
settings.PROCTORING_SETTINGS['SITE_NAME'] if
'SITE_NAME' in settings.PROCTORING_SETTINGS else settings.SITE_NAME
......
# Configuration for i18n workflow.
locales:
- en # English - Source Language
- ar # Arabic
- az # Azerbaijani
- bg_BG # Bulgarian (Bulgaria)
- bn_BD # Bengali (Bangladesh)
- bn_IN # Bengali (India)
- bs # Bosnian
- ca # Catalan
- ca@valencia # Catalan (Valencia)
- cs # Czech
- cy # Welsh
- da # Danish
- de_DE # German (Germany)
- el # Greek
- en@lolcat # LOLCAT English
- en@pirate # Pirate English
- es_419 # Spanish (Latin America)
- es_AR # Spanish (Argentina)
- es_EC # Spanish (Ecuador)
- es_ES # Spanish (Spain)
- es_MX # Spanish (Mexico)
- es_PE # Spanish (Peru)
- et_EE # Estonian (Estonia)
- eu_ES # Basque (Spain)
- fa # Persian
- fa_IR # Persian (Iran)
- fi_FI # Finnish (Finland)
- fr # French
- gl # Galician
- gu # Gujarati
- he # Hebrew
- hi # Hindi
- hr # Croatian
- hu # Hungarian
- hy_AM # Armenian (Armenia)
- id # Indonesian
- it_IT # Italian (Italy)
- ja_JP # Japanese (Japan)
- kk_KZ # Kazakh (Kazakhstan)
- km_KH # Khmer (Cambodia)
- kn # Kannada
- ko_KR # Korean (Korea)
- lt_LT # Lithuanian (Lithuania)
- ml # Malayalam
- mn # Mongolian
- ms # Malay
- nb # Norwegian Bokmål
- ne # Nepali
- nl_NL # Dutch (Netherlands)
- or # Oriya
- pl # Polish
- pt_BR # Portuguese (Brazil)
- pt_PT # Portuguese (Portugal)
- ro # Romanian
- ru # Russian
- si # Sinhala
- sk # Slovak
- sl # Slovenian
- th # Thai
- tr_TR # Turkish (Turkey)
- uk # Ukranian
- ur # Urdu
- vi # Vietnamese
- zh_CN # Chinese (China)
- zh_TW # Chinese (Taiwan)
# The locales used for fake-accented English, for testing.
dummy_locales:
- eo
# Directories we don't search for strings.
ignore_dirs:
- docs
- edx-proctoring
- logs
- node_modules
- performance
- requirements
- scripts
- settings
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-28 14:09-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:132
msgid "Required field"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:171
msgid "Practice Exam"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:173
msgid "Proctored Exam"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:181
msgid "Timed Exam"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:200
msgid "Additional Time"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_add_allowance_view.js:204
msgid "Value"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js:13
#: edx_proctoring/static/proctoring/spec/proctored_exam_add_allowance_spec.js:79
msgid "Additional Time (minutes)"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js:14
#: edx_proctoring/static/proctoring/spec/proctored_exam_add_allowance_spec.js:80
msgid "Review Policy Exception"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:9
msgid "Eligible"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:10
msgid "Created"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:11
msgid "Download Software Clicked"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:12
msgid "Ready to start"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:13
msgid "Started"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:14
msgid "Ready to submit"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:15
msgid "Declined"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:16
msgid "Timed out"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:17
msgid "Second Review Required"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:18
msgid "Submitted"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:19
msgid "Verified"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:20
msgid "Rejected"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:21
msgid "Error"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:160
msgid "Practice"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:162
msgid "Proctored"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:165
msgid "Timed"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js:187
msgid "Are you sure you want to remove this student's exam attempt?"
msgstr ""
#: edx_proctoring/static/proctoring/js/views/proctored_exam_view.js:134
msgid ""
"Are you sure you want to leave this page? \n"
"To pass your proctored exam you must also pass the online proctoring session "
"review."
msgstr ""
......@@ -2,6 +2,8 @@
Django management command to manually set the attempt status for a user in a proctored exam
"""
from __future__ import absolute_import
from optparse import make_option
from django.core.management.base import BaseCommand
......
......@@ -2,6 +2,8 @@
Tests for the set_attempt_status management command
"""
from __future__ import absolute_import
from datetime import datetime
import pytz
......
......@@ -2,22 +2,30 @@
"""
Data models for the proctoring subsystem
"""
# pylint: disable=model-missing-unicode
from __future__ import absolute_import
import six
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Q
from django.db.models.base import ObjectDoesNotExist
from django.db.models.signals import pre_save, pre_delete
from django.dispatch import receiver
from django.utils.translation import ugettext as _, ugettext_noop
from model_utils.models import TimeStampedModel
from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from edx_proctoring.exceptions import (
UserNotFoundException,
ProctoredExamNotActiveException,
AllowanceValueNotAllowedException
)
from django.db.models.base import ObjectDoesNotExist
@six.python_2_unicode_compatible
class ProctoredExam(TimeStampedModel):
"""
Information about the Proctored Exam.
......@@ -58,11 +66,8 @@ class ProctoredExam(TimeStampedModel):
unique_together = (('course_id', 'content_id'),)
db_table = 'proctoring_proctoredexam'
def __unicode__(self):
"""
How to serialize myself as a string
"""
def __str__(self):
# pragma: no cover
return u"{course_id}: {exam_name} ({active})".format(
course_id=self.course_id,
exam_name=self.exam_name,
......@@ -173,9 +178,9 @@ class ProctoredExamStudentAttemptStatus(object):
# status alias for sending email
status_alias_mapping = {
submitted: _('pending'),
verified: _('satisfactory'),
rejected: _('unsatisfactory')
submitted: ugettext_noop('pending'),
verified: ugettext_noop('satisfactory'),
rejected: ugettext_noop('unsatisfactory')
}
@classmethod
......@@ -233,8 +238,10 @@ class ProctoredExamStudentAttemptStatus(object):
"""
Returns status alias used in email
"""
status_alias = cls.status_alias_mapping.get(status, None)
return cls.status_alias_mapping.get(status, '')
# Note that the alias is localized here as it is untranslated in the model
return _(status_alias) if status_alias else '' # pylint: disable=translation-of-non-string
@classmethod
def is_valid_status(cls, status):
......@@ -244,6 +251,7 @@ class ProctoredExamStudentAttemptStatus(object):
return cls.is_completed_status(status) or cls.is_incomplete_status(status)
@six.python_2_unicode_compatible
class ProctoredExamReviewPolicy(TimeStampedModel):
"""
This is how an instructor can set review policies for a proctored exam
......@@ -258,6 +266,13 @@ class ProctoredExamReviewPolicy(TimeStampedModel):
# policy that will be passed to reviewers
review_policy = models.TextField()
def __str__(self):
# pragma: no cover
return u"ProctoredExamReviewPolicy: {set_by_user} ({proctored_exam})".format(
set_by_user=self.set_by_user,
proctored_exam=self.proctored_exam,
)
class Meta:
""" Meta class for this Django model """
db_table = 'proctoring_proctoredexamreviewpolicy'
......@@ -463,11 +478,11 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# if the user is attempting this as a proctored exam
# in case there is an option to opt-out
taking_as_proctored = models.BooleanField(default=False, verbose_name=_("Taking as Proctored"))
taking_as_proctored = models.BooleanField(default=False, verbose_name=ugettext_noop("Taking as Proctored"))
# Whether this attempt is considered a sample attempt, e.g. to try out
# the proctoring software
is_sample_attempt = models.BooleanField(default=False, verbose_name=_("Is Sample Attempt"))
is_sample_attempt = models.BooleanField(default=False, verbose_name=ugettext_noop("Is Sample Attempt"))
student_name = models.CharField(max_length=255)
......@@ -682,8 +697,8 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
# DONT EDIT THE KEYS - THE FIRST VALUE OF THE TUPLE - AS ARE THEY ARE STORED IN THE DATABASE
# THE SECOND ELEMENT OF THE TUPLE IS A DISPLAY STRING AND CAN BE EDITED
ADDITIONAL_TIME_GRANTED = ('additional_time_granted', _('Additional Time (minutes)'))
REVIEW_POLICY_EXCEPTION = ('review_policy_exception', _('Review Policy Exception'))
ADDITIONAL_TIME_GRANTED = ('additional_time_granted', ugettext_noop('Additional Time (minutes)'))
REVIEW_POLICY_EXCEPTION = ('review_policy_exception', ugettext_noop('Review Policy Exception'))
all_allowances = [
ADDITIONAL_TIME_GRANTED + REVIEW_POLICY_EXCEPTION
......
"""Defines serializers used by the Proctoring API."""
from __future__ import absolute_import
from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.fields import DateTimeField
from django.contrib.auth.models import User
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAttempt,
......@@ -14,7 +19,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
"""
Serializer for the ProctoredExam Model.
"""
id = serializers.IntegerField(required=False)
id = serializers.IntegerField(required=False) # pylint: disable=invalid-name
course_id = serializers.CharField(required=True)
content_id = serializers.CharField(required=True)
external_id = serializers.CharField(required=True)
......@@ -44,7 +49,7 @@ class UserSerializer(serializers.ModelSerializer):
"""
Serializer for the User Model.
"""
id = serializers.IntegerField(required=False)
id = serializers.IntegerField(required=False) # pylint: disable=invalid-name
username = serializers.CharField(required=True)
email = serializers.CharField(required=True)
......
......@@ -2,6 +2,8 @@
A wrapper class around all methods exposed in api.py
"""
from __future__ import absolute_import
import types
......
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
"""
We need python think this is a python module
"""
......@@ -26,7 +26,7 @@
{% endblocktrans %}
</p>
<p>
<span><a href="#" id="software_download_link" data-action="click_download_software" target="_blank">Start System Check</a></span>
<span><a href="#" id="software_download_link" data-action="click_download_software" target="_blank">{% trans "Start System Check" %}</a></span>
</p>
<p>
{% blocktrans %}
......
"""
We need python think this is a python module
"""
"""
Init.py file for the tests top level directory
"""
......@@ -4,11 +4,14 @@
"""
All tests for the api.py
"""
import ddt
from __future__ import absolute_import
from datetime import datetime, timedelta
import ddt
from freezegun import freeze_time
from mock import patch
import pytz
from freezegun import freeze_time
from edx_proctoring.api import (
create_exam,
......@@ -62,17 +65,14 @@ from edx_proctoring.models import (
ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy,
)
from .utils import (
ProctoredExamTestCase,
)
from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from .test_services import (
MockCreditService,
MockCreditServiceNone,
MockCreditServiceWithCourseEndDate,
)
from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from .utils import ProctoredExamTestCase
@ddt.ddt
......@@ -427,9 +427,8 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
with freeze_time(reset_time):
attempt_id = create_exam_attempt(exam_id, self.user_id)
attempt = get_exam_attempt_by_id(attempt_id)
self.assertTrue(
minutes_before_past_due_date - 1 <= attempt['allowed_time_limit_mins'] <= minutes_before_past_due_date
)
self.assertLessEqual(minutes_before_past_due_date - 1, attempt['allowed_time_limit_mins'])
self.assertLessEqual(attempt['allowed_time_limit_mins'], minutes_before_past_due_date)
def test_create_an_exam_attempt(self):
"""
......
......@@ -3,6 +3,8 @@
All tests for proctored exam emails.
"""
from __future__ import absolute_import
import ddt
from django.core import mail
from mock import patch
......@@ -117,7 +119,7 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
ProctoredExamStudentAttemptStatus.timed_out,
ProctoredExamStudentAttemptStatus.error
)
@patch.dict('settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_not_send_email(self, status):
"""
Assert that email is not sent on the following statuses of proctoring attempt.
......
......@@ -3,6 +3,9 @@
"""
All tests for the models.py
"""
from __future__ import absolute_import
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAllowance,
......@@ -44,6 +47,14 @@ class ProctoredExamModelTests(LoggedInTestCase):
output = unicode(proctored_exam)
self.assertEquals(output, u"test_course: अआईउऊऋऌ अआईउऊऋऌ (inactive)")
policy = ProctoredExamReviewPolicy.objects.create(
set_by_user_id=self.user.id,
proctored_exam=proctored_exam,
review_policy='Foo Policy'
)
output = unicode(policy)
self.assertEquals(output, u"ProctoredExamReviewPolicy: tester (test_course: अआईउऊऋऌ अआईउऊऋऌ (inactive))")
def test_save_proctored_exam_student_allowance_history(self): # pylint: disable=invalid-name
"""
Test to Save and update the proctored Exam Student Allowance object.
......
......@@ -2,7 +2,10 @@
Tests for the custom StrictBooleanField serializer used by the ProctoredExamSerializer
"""
from __future__ import absolute_import
import unittest
from edx_proctoring.serializers import ProctoredExamSerializer
......
......@@ -4,15 +4,18 @@
Test for the xBlock service
"""
from __future__ import absolute_import
from datetime import datetime, timedelta
import types
import unittest
import pytz
from datetime import datetime, timedelta
from edx_proctoring.services import (
ProctoringService
)
from edx_proctoring.exceptions import UserNotFoundException
from edx_proctoring import api as edx_proctoring_api
import types
class MockCreditService(object):
......
......@@ -5,11 +5,13 @@
All tests for the api.py
"""
import ddt
from __future__ import absolute_import
from datetime import datetime, timedelta
import ddt
from freezegun import freeze_time
from mock import patch
import pytz
from freezegun import freeze_time
from edx_proctoring.api import (
update_exam,
......
"""
File that contains tests for the util methods.
"""
from __future__ import absolute_import
import unittest
from edx_proctoring.utils import humanized_time, _emit_event
......
......@@ -2,14 +2,17 @@
"""
All tests for the proctored_exams.py
"""
from __future__ import absolute_import
from datetime import datetime, timedelta
import json
import pytz
import ddt
from mock import Mock, patch
from freezegun import freeze_time
from httmock import HTTMock
from string import Template # pylint: disable=deprecated-module
from datetime import datetime, timedelta
from mock import Mock, patch
import pytz
from django.test.client import Client
from django.core.urlresolvers import reverse, NoReverseMatch
from django.contrib.auth.models import User
......@@ -32,15 +35,13 @@ from edx_proctoring.api import (
_calculate_allowed_mins
)
from .utils import (
LoggedInTestCase
)
from edx_proctoring.urls import urlpatterns
from edx_proctoring.backends.tests.test_review_payload import TEST_REVIEW_PAYLOAD
from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload
from edx_proctoring.backends.tests.test_software_secure import mock_response_content
from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService
from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from edx_proctoring.urls import urlpatterns
from .test_services import MockCreditService, MockInstructorService
from .utils import LoggedInTestCase
class ProctoredExamsApiTests(LoggedInTestCase):
......@@ -493,7 +494,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt = get_exam_attempt_by_id(attempt_id)
self.assertEqual(attempt['status'], "started")
self.assertFalse(attempt['status'] == "ready_to_start")
self.assertNotEqual(attempt['status'], "ready_to_start")
def test_start_exam_create(self):
"""
......@@ -679,7 +680,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertIsNotNone(response_data['started_at'])
self.assertIsNone(response_data['completed_at'])
# check that we get timer around 30 hours minus some seconds
self.assertTrue(107990 <= response_data['time_remaining_seconds'] <= 108000)
self.assertLessEqual(107990, response_data['time_remaining_seconds'])
self.assertLessEqual(response_data['time_remaining_seconds'], 108000)
# check that humanized time
self.assertEqual(response_data['accessibility_time_string'], 'you have 30 hours remaining')
......@@ -701,9 +703,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
total_minutes, __ = _calculate_allowed_mins(proctored_exam.due_date, proctored_exam.time_limit_mins)
# Check that timer has > 24 hours
self.assertTrue(total_minutes / 60 > 24)
self.assertGreater(total_minutes / 60, 24)
# Get total_minutes around 27 hours. We are checking range here because while testing some seconds have passed.
self.assertTrue(expected_total_minutes - 1 <= total_minutes <= expected_total_minutes)
self.assertLessEqual(expected_total_minutes - 1, total_minutes)
self.assertLessEqual(total_minutes, expected_total_minutes)
def test_attempt_ready_to_start(self):
"""
......@@ -1668,7 +1671,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertTrue('exam_attempt_id' in response_data)
self.assertIn('exam_attempt_id', response_data)
attempt_id = response_data['exam_attempt_id']
......@@ -1725,7 +1728,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id']
)
......@@ -1763,7 +1766,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id'].upper()
)
......@@ -1800,7 +1803,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id'].upper()
)
......@@ -1837,7 +1840,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])
test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id='mismatch'
)
......
......@@ -5,6 +5,8 @@
Subclasses Django test client to allow for easy login
"""
from __future__ import absolute_import
from datetime import datetime
from importlib import import_module
import pytz
......@@ -80,7 +82,7 @@ class LoggedInTestCase(TestCase):
"""
Setup for tests
"""
super(LoggedInTestCase, self).setUp()
self.client = TestClient()
self.user = User(username='tester', email='tester@test.com')
self.user.save()
......@@ -200,6 +202,7 @@ class ProctoredExamTestCase(LoggedInTestCase):
"""
Cleanup
"""
super(ProctoredExamTestCase, self).tearDown()
del TRACKERS['default']
def _create_proctored_exam(self):
......
"""
URL mappings for edX Proctoring Server.
"""
from edx_proctoring import views, callbacks
from django.conf import settings
from __future__ import absolute_import
from django.conf import settings
from django.conf.urls import patterns, url, include
from edx_proctoring import views, callbacks
urlpatterns = patterns( # pylint: disable=invalid-name
'',
url(
......
......@@ -2,25 +2,28 @@
Helpers for the HTTP APIs
"""
import pytz
import logging
from __future__ import absolute_import
from datetime import datetime, timedelta
import logging
import pytz
from django.utils.translation import ugettext as _
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from rest_framework.views import APIView
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from eventtracking import tracker
from edx_proctoring.models import (
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptHistory,
)
# import dependent libraries (in local_requirements.txt otherwise pick up from running Open edX LMS runtime)
from eventtracking import tracker
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
log = logging.getLogger(__name__)
......
......@@ -2,16 +2,19 @@
Proctored Exams HTTP-based API endpoints
"""
from __future__ import absolute_import
import logging
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from rest_framework import status
from rest_framework.response import Response
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from edx_proctoring.api import (
create_exam,
update_exam,
......
# Django/Framework Packages
django>=1.8,<1.9a
django-model-utils==2.3.1
djangorestframework>=3.1,<3.2
django-ipware==1.1.0
pytz>=2012h
pycrypto>=2.6
python-dateutil==2.1
# edX packages
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
edx-opaque-keys==0.3.4
#!/usr/bin/env python
"""
Django administration utility.
"""
from __future__ import absolute_import, unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
from django.core.management import execute_from_command_line
PWD = os.path.abspath(os.path.dirname(__file__))
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings')
sys.path.append(PWD)
try:
from django.core.management import execute_from_command_line # pylint: disable=wrong-import-position
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # pylint: disable=unused-import, wrong-import-position
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)
......@@ -2,6 +2,15 @@
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: proc
oeps: {}
owner: edx/teaching-and-learning
tags:
- lms
oeps:
oep-2: True
oep-3:
state: False
reason: TODO - Implement for this application if appropriate
oep-5:
state: False
reason: TODO - Implement for this application if appropriate
track-pulls: true
# pylintrc tweaks for use with edx_lint.
[MASTER]
ignore = migrations
load-plugins = edx_lint.pylint,pylint_django,pylint_celery
# Empty so that we will use the versions of dependencies installed in edx-platform.
# See local_requirements.txt for the requirements needed for local development.
# Core requirements for using this application
# Django/Framework Packages
django>=1.8,<1.9a
django-model-utils>=2.3.1
djangorestframework>=3.1
django-ipware>=1.1.0
pytz>=2012h
pycrypto>=2.6
python-dateutil>=2.1
# edX packages
-e git+https://github.com/edx/event-tracking.git@0.2.2#egg=event-tracking==0.2.2
edx-opaque-keys>=0.4
# Additional requirements for development of this application
diff-cover # Changeset diff test coverage
edx-lint # For updating pylintrc
edx-i18n-tools # For i18n_tool dummy
pip-tools # Requirements file management
tox # virtualenv management for tests
tox-battery # Makes tox aware of requirements file changes
twine # Utility for PyPI package uploads
wheel # For generation of wheels for PyPI
django>=1.8,<1.9a
# Requirements for documentation validation
doc8 # reStructuredText style checker
edx_sphinx_theme # edX theme for Sphinx output
readme_renderer # Validates README.rst for usage on PyPI
Sphinx # Documentation builder
sphinxcontrib-napoleon # Google Style docstring support for sphinx
# If there are any Python packages you want to keep in your virtualenv beyond
# those listed in the official requirements files, create a "private.in" file
# and list them there. Generate the corresponding "private.txt" file pinning
# all of their indirect dependencies to specific versions as follows:
# pip-compile private.in
# This allows you to use "pip-sync" without removing these packages:
# pip-sync requirements/*.txt
# "private.in" and "private.txt" aren't checked into git to avoid merge
# conflicts, and the presence of this file allows "private.*" to be
# included in scripted pip-sync usage without requiring that those files be
# created first.
# Requirements for code quality checks
caniusepython3 # Additional Python 3 compatibility pylint checks
edx-lint # edX pylint rules and plugins
isort # to standardize order of imports
pycodestyle # PEP 8 compliance validation
pydocstyle # PEP 257 compliance validation
# Requirements for test runs.
pytest-catchlog # Show log output for test failures
pytest-cov # pytest extension for code coverage statistics
pytest-django # pytest extension for better Django support
bok-choy>=0.3.1
ddt>=0.8.0
django_nose>=1.4.1
freezegun>=0.3.1
httmock>=1.2.3
httpretty>=0.8.0
logilab-common>=0.63.2
mock==1.0.1
nose>=1.3.3
selenium>=2.45.0
sure==1.2.7
testfixtures>=4.0.0
ECHO 'Beginning Test Run...'
ECHO ''
ECHO 'Removing *.pyc files'
find . -name "*.pyc" -exec rm -rf {} \;
ECHO 'Running test suite'
coverage run manage.py test edx_proctoring --verbosity=3
coverage report -m
coverage html
pep8 edx_proctoring
pylint edx_proctoring --report=no
ECHO ''
ECHO 'View the full coverage report at {CODE_PATH}/edx-proctoring/htmlcov/index.html'
ECHO ''
ECHO 'Testing Complete!'
[pep8]
ignore=E501
max_line_length=119
exclude=settings,*/migrations/*
[isort]
line_length = 120
known_edx =
known_django = django
known_djangoapp = model_utils
known_first_party = edx_proctoring
sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,DJANGOAPP,EDX,FIRSTPARTY,LOCALFOLDER
[wheel]
universal = 1
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=C0111,W6005,W6100
from __future__ import absolute_import, print_function
from setuptools import setup, find_packages
import os
import re
import sys
def is_requirement(line):
"""
Return True if the requirement line is a package requirement;
that is, it is not blank, a comment, or editable.
"""
# Remove whitespace at the start/end of the line
line = line.strip()
# Skip blank lines, comments, and editable installs
return not (
line == '' or
line.startswith('-r') or
line.startswith('#') or
line.startswith('-e') or
line.startswith('git+')
)
def load_requirements(*requirements_paths):
from setuptools import setup
def get_version(*file_paths):
"""
Load all requirements from the specified requirements files.
Returns a list of requirement strings.
Extract the version string from the file at the given relative path fragments.
"""
requirements = set()
for path in requirements_paths:
requirements.update(
line.strip() for line in open(path).readlines()
if is_requirement(line)
)
return list(requirements)
filename = os.path.join(os.path.dirname(__file__), *file_paths)
version_file = open(filename).read()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError('Unable to find version string.')
VERSION = get_version('edx_proctoring', '__init__.py')
if sys.argv[-1] == 'tag':
print("Tagging the version on github:")
os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION))
os.system("git push --tags")
sys.exit()
README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst')).read()
setup(
name='edx-proctoring',
version='0.16.2',
version=VERSION,
description='Proctoring subsystem for Open edX',
long_description=open('README.md').read(),
long_description=README + '\n\n' + CHANGELOG,
author='edX',
author_email='oscm@edx.org',
url='https://github.com/edx/edx-proctoring',
license='AGPL',
license="AGPL 3.0",
zip_safe=False,
keywords='Django edx',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Development Status :: 3 - Alpha',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Natural Language :: English',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
],
packages=[
'edx_proctoring',
],
packages=find_packages(exclude=["tests"]),
package_data={
'': ['*.html', '*.underscore', '*.png', '*.js', '*swf']
},
dependency_links=[
include_package_data=True,
install_requires=[
"Django>=1.8,<1.11"
],
install_requires=load_requirements('requirements.txt'),
tests_require=load_requirements('test_requirements.txt')
)
# Testing Packages
logilab-common==0.63.2
coverage==3.7.1
astroid==1.3.4
django_nose==1.4.1
nose==1.3.3
httpretty==0.8.0
pep8==1.6.2
pylint==1.4.2
pep257==0.3.2
mock==1.0.1
testfixtures==4.0.0
bok-choy>=0.3.1
sure==1.2.7
ddt==0.8.0
selenium>=2.45.0
freezegun==0.3.1
httmock==1.2.3
"""
Django settings file for local development purposes
These settings are here to use during tests, because Django requires them.
In a real-world use case, apps in this project are installed into other
Django applications, so these settings will not be used.
"""
from __future__ import absolute_import, unicode_literals
import sys
......
[tox]
envlist = {py27}-django{18}
[doc8]
max-line-length = 120
[pycodestyle]
exclude = .git,.tox,migrations
max-line-length = 120
[pydocstyle]
; D101 = Missing docstring in public class
; D200 = One-line docstring should fit on one line with quotes
; D203 = 1 blank line required before class docstring
; D212 = Multi-line docstring summary should start at the first line
ignore = D101,D200,D203,D212
match-dir = (?!migrations)
[testenv]
setenv =
DJANGO_SETTINGS_MODULE = test_settings
deps =
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
-rrequirements/test.txt
commands =
coverage run ./manage.py test {posargs}
[testenv:docs]
setenv =
DJANGO_SETTINGS_MODULE = test_settings
PYTHONPATH = {toxinidir}
whitelist_externals =
make
rm
deps =
-r{toxinidir}/requirements/doc.txt
commands =
doc8 --ignore-path docs/_build README.rst docs
rm -f docs/edx_proctoring.rst
rm -f docs/modules.rst
make -C docs clean
make -C docs html
python setup.py check --restructuredtext --strict
[testenv:quality]
whitelist_externals =
make
rm
touch
deps =
-r{toxinidir}/requirements/doc.txt
-r{toxinidir}/requirements/quality.txt
-r{toxinidir}/requirements/test.txt
commands =
pylint edx_proctoring
pycodestyle edx_proctoring
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment