From 7113178f6b3fa10901d0a26294e7e61112f9dc48 Mon Sep 17 00:00:00 2001 From: alangsto <46360176+alangsto@users.noreply.github.com> Date: Thu, 9 Jun 2022 08:32:50 -0400 Subject: [PATCH] feat: add exam models (#10) * feat: add exam models * chore: fix pylint errors * fix: remove regex for path strings --- .annotation_safe_list.yml | 45 ++++++++ .bowerrc | 4 + .coveragerc | 12 ++ .github/workflows/ci.yml | 45 ++++++++ .github/workflows/commitlint.yml | 10 ++ .github/workflows/docker-publish.yml | 22 ++++ .../workflows/upgrade-python-requirements.yml | 27 +++++ .gitignore | 92 ++++++++++++++- .pii_annotations.yml | 35 ++++++ Makefile | 4 + docs/conf.py | 2 +- edx_exams/apps/api/urls.py | 4 +- edx_exams/apps/core/admin.py | 2 +- .../migrations/0002_create_exam_models.py | 96 ++++++++++++++++ edx_exams/apps/core/models.py | 108 +++++++++++++++++- edx_exams/apps/core/views.py | 5 +- edx_exams/apps/lti/admin.py | 4 +- edx_exams/apps/lti/api.py | 21 ++-- edx_exams/apps/lti/apps.py | 4 + edx_exams/apps/lti/models.py | 6 +- edx_exams/apps/lti/tests.py | 4 +- edx_exams/apps/lti/urls.py | 4 + edx_exams/apps/lti/views.py | 64 ++++++----- edx_exams/settings/devstack.py | 5 +- edx_exams/settings/local.py | 4 +- edx_exams/urls.py | 18 +-- requirements/base.in | 1 + requirements/base.txt | 7 +- requirements/ci.txt | 46 ++++++++ requirements/dev.txt | 13 ++- requirements/doc.txt | 13 ++- requirements/pip.txt | 2 +- requirements/production.txt | 7 +- requirements/quality.txt | 13 ++- requirements/test.txt | 9 +- requirements/validation.txt | 15 ++- tox.ini | 30 ++--- 37 files changed, 701 insertions(+), 102 deletions(-) create mode 100644 .annotation_safe_list.yml create mode 100644 .bowerrc create mode 100644 .coveragerc create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .github/workflows/upgrade-python-requirements.yml create mode 100644 .pii_annotations.yml create mode 100644 edx_exams/apps/core/migrations/0002_create_exam_models.py create mode 100644 requirements/ci.txt diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml new file mode 100644 index 00000000..c7c70688 --- /dev/null +++ b/.annotation_safe_list.yml @@ -0,0 +1,45 @@ +# This is a Code Annotations automatically-generated Django model safelist file. +# These models must be annotated as follows in order to be counted in the coverage report. +# See https://code-annotations.readthedocs.io/en/latest/safelist.html for more information. +# +# fake_app_1.FakeModelName: +# ".. no_pii:": "This model has no PII" +# fake_app_2.FakeModel2: +# ".. choice_annotation:": foo, bar, baz + +admin.LogEntry: + ".. no_pii:": "This model has no PII" +auth.Group: + ".. no_pii:": "This model has no PII" +auth.Permission: + ".. no_pii:": "This model has no PII" +contenttypes.ContentType: + ".. no_pii:": "This model has no PII" +django_celery_results.ChordCounter: + ".. no_pii:": "This model has no PII" +django_celery_results.GroupResult: + ".. no_pii:": "This model has no PII" +django_celery_results.TaskResult: + ".. no_pii:": "This model has no PII" +sessions.Session: + ".. no_pii:": "This model has no PII" +social_django.Association: + ".. no_pii:": "This model has no PII" +social_django.Code: + ".. pii:": "Email address" + ".. pii_types:": other + ".. pii_retirement:": local_api +social_django.Nonce: + ".. no_pii:": "This model has no PII" +social_django.Partial: + ".. no_pii:": "This model has no PII" +social_django.UserSocialAuth: + ".. no_pii:": "This model has no PII" +waffle.Flag: + ".. no_pii:": "This model has no PII" +waffle.Sample: + ".. no_pii:": "This model has no PII" +waffle.Switch: + ".. no_pii:": "This model has no PII" +lti_consumer.LtiDlContentItem: + ".. no_pii:": "This model has no PII" diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..c527c5fe --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "edx-exams/static/bower_components", + "interactive": false +} diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..1802e98d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,12 @@ +[run] +branch = True +data_file = .coverage +source=edx_exams +omit = + edx_exams/settings* + edx_exams/conf* + *wsgi.py + *migrations* + *admin.py + *static* + *templates* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3bbd824f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: Python CI + +on: + push: + branches: [main] + pull_request: + branches: + - '**' + + +jobs: + run_tests: + name: tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-20.04] + python-version: ['3.8'] + toxenv: [django32, django40, quality, pii_check] + + steps: + - uses: actions/checkout@v2 + - name: setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install pip + run: pip install -r requirements/pip.txt + + - name: Install Dependencies + run: pip install -r requirements/ci.txt + + - name: Run Tests + env: + TOXENV: ${{ matrix.toxenv }} + run: tox + + - name: Run coverage + if: matrix.python-version == '3.8' && matrix.toxenv == 'django32' + uses: codecov/codecov-action@v1 + with: + flags: unittests + fail_ci_if_error: true diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 00000000..e2b06615 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,10 @@ +# Run commitlint on the commit messages in a pull request. + +name: Lint Commit Messages + +on: + - pull_request + +jobs: + commitlint: + uses: edx/.github/.github/workflows/commitlint.yml@master diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..0452cf22 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,22 @@ +name: Push Docker Images + +on: + push: + branches: + - main +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Build and Push docker image + env: + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + run : make github_docker_push diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml new file mode 100644 index 00000000..2da4874d --- /dev/null +++ b/.github/workflows/upgrade-python-requirements.yml @@ -0,0 +1,27 @@ +name: Upgrade Python Requirements + +on: + schedule: + - cron: "15 15 10/14 * *" + workflow_dispatch: + inputs: + branch: + description: "Target branch against which to create requirements PR" + required: true + default: 'master' + +jobs: + call-upgrade-python-requirements-workflow: + uses: edx/.github/.github/workflows/upgrade-python-requirements.yml@master + with: + branch: ${{ github.event.inputs.branch || 'master' }} + # optional parameters below; fill in if you'd like github or email notifications + # user_reviewers: "" + # team_reviewers: "" + email_address: "masters-requirements-update@2u-internal.opsgenie.net" + send_success_notification: true + secrets: + requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} + requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} + edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} + edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} diff --git a/.gitignore b/.gitignore index 3886ecbf..9cb4742c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,89 @@ -### Python artifacts -*.pyc -*.__pycache__ +*.py[cod] + +*.log + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Django +default.db + +# Static assets +media/cache/ +assets/ + +# Unit test / coverage reports +.coverage +htmlcov +.tox +nosetests.xml +unittests.xml + +# PII annotation reports +pii_report + +### Internationalization artifacts +*.mo +*.po +*.prob +!django.po +!django.mo +!djangojs.po +!djangojs.mo +{{cookiecutter.project_name}}/conf/locale/fake*/LC_MESSAGES/*.po +{{cookiecutter.project_name}}/conf/locale/fake*/LC_MESSAGES/*.mo +{{cookiecutter.project_name}}/conf/locale/messages.mo + + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# QA +coverage.xml +diff_*.html +*.report +report +venv +acceptance_tests.*.log +acceptance_tests.*.png + +# Override config files +override.cfg +private.py + +# JetBrains +.idea + +# Emacs +*~ + +# OS X +.DS_Store + +# Editor Temp Files +*.swp + +*.trace + +docs/_build/ diff --git a/.pii_annotations.yml b/.pii_annotations.yml new file mode 100644 index 00000000..2b710e08 --- /dev/null +++ b/.pii_annotations.yml @@ -0,0 +1,35 @@ +source_path: ./ +report_path: pii_report +safelist_path: .annotation_safe_list.yml +coverage_target: 100.0 +annotations: + ".. no_pii:": + "pii_group": + - ".. pii:": + - ".. pii_types:": + choices: + - id # Unique identifier for the user which is shared across systems + - name # Used for any part of the user’s name + - username + - password + - location # Used for any part of any type address or country stored + - phone_number # Used for phone or fax numbers + - email_address + - birth_date # Used for any part of a stored birth date + - ip # IP address + - external_service # Used for external service ids or links such as social media links or usernames, website links, etc. + - biography # Any type of free-form biography field + - gender + - sex + - image + - video + - other + - ".. pii_retirement:": + choices: + - retained # Intentionally kept for legal reasons + - local_api # An API exists in this repository for retiring this information + - consumer_api # The data's consumer must implement an API for retiring this information + - third_party # A third party API exists to retire this data +extensions: + python: + - py diff --git a/Makefile b/Makefile index 4f3c240e..96777968 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,9 @@ clean_pycrypto: ## temporary (?) hack to deal with the pycrypto dep that's insta requirements: clean_pycrypto piptools dev_requirements ## sync to default requirements +test_requirements: + pip-sync -q requirements/test.txt + ci_requirements: validation_requirements ## sync to requirements needed for CI checks dev_requirements: ## sync to requirements for local development @@ -105,6 +108,7 @@ upgrade: piptools ## update the requirements/*.txt files with the latest package pip install -qr requirements/pip-tools.txt pip-compile --upgrade -o requirements/base.txt requirements/base.in pip-compile --upgrade -o requirements/test.txt requirements/test.in + pip-compile --upgrade -o requirements/ci.txt requirements/ci.in pip-compile --upgrade -o requirements/doc.txt requirements/doc.in pip-compile --upgrade -o requirements/quality.txt requirements/quality.in pip-compile --upgrade -o requirements/validation.txt requirements/validation.in diff --git a/docs/conf.py b/docs/conf.py index bdc5c44e..e9ec72fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -119,7 +119,7 @@ def get_version(*file_paths): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/edx_exams/apps/api/urls.py b/edx_exams/apps/api/urls.py index 9d2b2c5d..68a58223 100644 --- a/edx_exams/apps/api/urls.py +++ b/edx_exams/apps/api/urls.py @@ -4,11 +4,11 @@ All API URLs should be versioned, so urlpatterns should only contain namespaces for the active versions of the API. """ -from django.conf.urls import include, url +from django.urls import include, path from edx_exams.apps.api.v1 import urls as v1_urls app_name = 'api' urlpatterns = [ - url(r'^v1/', include(v1_urls)), + path(r'^v1/', include(v1_urls)), ] diff --git a/edx_exams/apps/core/admin.py b/edx_exams/apps/core/admin.py index bde11c50..a4d98028 100644 --- a/edx_exams/apps/core/admin.py +++ b/edx_exams/apps/core/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from edx_exams.apps.core.models import User diff --git a/edx_exams/apps/core/migrations/0002_create_exam_models.py b/edx_exams/apps/core/migrations/0002_create_exam_models.py new file mode 100644 index 00000000..4ef55bc0 --- /dev/null +++ b/edx_exams/apps/core/migrations/0002_create_exam_models.py @@ -0,0 +1,96 @@ +# Generated by Django 3.2.13 on 2022-06-08 15:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Exam', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('resource_id', models.CharField(db_index=True, max_length=255)), + ('course_id', models.CharField(db_index=True, max_length=255)), + ('content_id', models.CharField(db_index=True, max_length=255)), + ('exam_name', models.TextField()), + ('exam_type', models.CharField(db_index=True, max_length=255)), + ('time_limit_mins', models.PositiveIntegerField()), + ('due_date', models.DateTimeField(null=True)), + ('hide_after_due', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'exam', + 'db_table': 'exams_exam', + }, + ), + migrations.CreateModel( + name='ProctoringProvider', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(db_index=True, max_length=255)), + ('verbose_name', models.CharField(db_index=True, max_length=255)), + ('lti_configuration_id', models.CharField(db_index=True, max_length=255)), + ], + options={ + 'verbose_name': 'proctoring provider', + 'db_table': 'exams_proctoringprovider', + }, + ), + migrations.AddField( + model_name='user', + name='anonymous_user_id', + field=models.IntegerField(db_index=True, null=True), + ), + migrations.CreateModel( + name='ExamAttempt', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('attempt_number', models.PositiveIntegerField()), + ('status', models.CharField(max_length=64)), + ('start_time', models.DateTimeField(null=True)), + ('allowed_time_limit_mins', models.IntegerField(null=True)), + ('exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.exam')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'exam attempt', + 'db_table': 'exams_examattempt', + }, + ), + migrations.AddField( + model_name='exam', + name='provider', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.proctoringprovider'), + ), + migrations.CreateModel( + name='CourseExamConfiguration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('course_id', models.CharField(db_index=True, max_length=255)), + ('allow_opt_out', models.BooleanField(default=False)), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.proctoringprovider')), + ], + options={ + 'verbose_name': 'course exam configuration', + 'db_table': 'exams_courseexamconfiguration', + }, + ), + ] diff --git a/edx_exams/apps/core/models.py b/edx_exams/apps/core/models.py index 649eab8e..da731406 100644 --- a/edx_exams/apps/core/models.py +++ b/edx_exams/apps/core/models.py @@ -2,7 +2,8 @@ from django.contrib.auth.models import AbstractUser from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from model_utils.models import TimeStampedModel class User(AbstractUser): @@ -16,6 +17,8 @@ class User(AbstractUser): """ full_name = models.CharField(_('Full Name'), max_length=255, blank=True, null=True) + anonymous_user_id = models.IntegerField(null=True, db_index=True) + @property def access_token(self): """ @@ -35,3 +38,106 @@ def get_full_name(self): def __str__(self): return str(self.get_full_name()) + + +class ProctoringProvider(TimeStampedModel): + """ + Information about the Proctoring Provider + + .. no_pii: + """ + + name = models.CharField(max_length=255, db_index=True) + + verbose_name = models.CharField(max_length=255, db_index=True) + + lti_configuration_id = models.CharField(max_length=255, db_index=True) + + class Meta: + """ Meta class for this Django model """ + db_table = 'exams_proctoringprovider' + verbose_name = 'proctoring provider' + + +class Exam(TimeStampedModel): + """ + Information about the Exam. + + .. no_pii: + """ + + resource_id = models.CharField(max_length=255, db_index=True) + + course_id = models.CharField(max_length=255, db_index=True) + + provider = models.ForeignKey(ProctoringProvider, on_delete=models.CASCADE) + + # pointer to the id of the piece of course_ware that is the proctored exam. + content_id = models.CharField(max_length=255, db_index=True) + + # display name of the Exam (Midterm etc). + exam_name = models.TextField() + + # type of Exam (proctored, practice, etc). + exam_type = models.CharField(max_length=255, db_index=True) + + # Time limit (in minutes) that a student can finish this exam. + time_limit_mins = models.PositiveIntegerField() + + # Due date is a deadline to finish the exam + due_date = models.DateTimeField(null=True) + + # Whether to hide this exam after the due date + hide_after_due = models.BooleanField(default=False) + + # Whether this exam will be active. + is_active = models.BooleanField(default=False) + + class Meta: + """ Meta class for this Django model """ + db_table = 'exams_exam' + verbose_name = 'exam' + + +class ExamAttempt(TimeStampedModel): + """ + Information about the Exam Attempt + + .. no_pii: + """ + + user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE) + + exam = models.ForeignKey(Exam, on_delete=models.CASCADE) + + attempt_number = models.PositiveIntegerField() + + status = models.CharField(max_length=64) + + start_time = models.DateTimeField(null=True) + + allowed_time_limit_mins = models.IntegerField(null=True) + + class Meta: + """ Meta class for this Django model """ + db_table = 'exams_examattempt' + verbose_name = 'exam attempt' + + +class CourseExamConfiguration(TimeStampedModel): + """ + Information about the Course Exam Configuration + + .. no_pii: + """ + + course_id = models.CharField(max_length=255, db_index=True) + + provider = models.ForeignKey(ProctoringProvider, on_delete=models.CASCADE) + + allow_opt_out = models.BooleanField(default=False) + + class Meta: + """ Meta class for this Django model """ + db_table = 'exams_courseexamconfiguration' + verbose_name = 'course exam configuration' diff --git a/edx_exams/apps/core/views.py b/edx_exams/apps/core/views.py index 04ef8fe6..39fa2b33 100644 --- a/edx_exams/apps/core/views.py +++ b/edx_exams/apps/core/views.py @@ -46,7 +46,10 @@ class Health(APIView): description='Checks the status of the database connection on which this service relies.' ) def get(self, request): - # Ignores health check in performance monitoring so as to not artifically inflate our response time metrics + """ + Get health status of service + """ + # Ignores health check in performance monitoring so as to not artificially inflate our response time metrics ignore_transaction() try: diff --git a/edx_exams/apps/lti/admin.py b/edx_exams/apps/lti/admin.py index 8c38f3f3..3877db70 100644 --- a/edx_exams/apps/lti/admin.py +++ b/edx_exams/apps/lti/admin.py @@ -1,3 +1,5 @@ -from django.contrib import admin +""" Admin configuration for LTI """ + +from django.contrib import admin # pylint: disable=unused-import # Register your models here. diff --git a/edx_exams/apps/lti/api.py b/edx_exams/apps/lti/api.py index 9f74c2cd..57cb8a95 100644 --- a/edx_exams/apps/lti/api.py +++ b/edx_exams/apps/lti/api.py @@ -1,8 +1,12 @@ -from django.conf import settings +""" +LTI API +""" +from django.conf import settings from lti_consumer.lti_1p3.consumer import LtiAdvantageConsumer from lti_consumer.models import LtiConfiguration + def get_lti1p3_consumer(): """ Returns an configured instance of LTI consumer. @@ -26,17 +30,20 @@ def get_lti1p3_consumer(): tool_keyset_url=lti_config.lti_1p3_tool_keyset_url ) + def get_lti_preflight_url(lti_message_hint): lti_consumer = get_lti1p3_consumer() context = lti_consumer.prepare_preflight_url(lti_hint=lti_message_hint) return context -def get_resource_link(): - # TODO: The resource link should uniquely represent the assessment in the Assessment Platform. - # TODO: We SHOULD provide a value for the title attribute. - # TODO: It's RECOMMENDED to provide a value for the description attribute. - # TODO: The xblock-lti-consumer library does not currently support setting these attributes. - return 'edx:proctored_exam:12345' + +def get_resource_link(): # pylint: disable=missing-function-docstring + # TODO: The resource link should uniquely represent the assessment in the Assessment Platform. + # TODO: We SHOULD provide a value for the title attribute. + # TODO: It's RECOMMENDED to provide a value for the description attribute. + # TODO: The xblock-lti-consumer library does not currently support setting these attributes. + return 'edx:proctored_exam:12345' + def get_optional_user_identity_claims(): # These claims are optional. diff --git a/edx_exams/apps/lti/apps.py b/edx_exams/apps/lti/apps.py index 34eae637..8343ada2 100644 --- a/edx_exams/apps/lti/apps.py +++ b/edx_exams/apps/lti/apps.py @@ -1,3 +1,7 @@ +""" +LTI Apps +""" + from django.apps import AppConfig diff --git a/edx_exams/apps/lti/models.py b/edx_exams/apps/lti/models.py index 71a83623..ed94cd2e 100644 --- a/edx_exams/apps/lti/models.py +++ b/edx_exams/apps/lti/models.py @@ -1,3 +1,7 @@ -from django.db import models +""" +LTI models +""" + +from django.db import models # pylint: disable=unused-import # Create your models here. diff --git a/edx_exams/apps/lti/tests.py b/edx_exams/apps/lti/tests.py index 7ce503c2..34971c1b 100644 --- a/edx_exams/apps/lti/tests.py +++ b/edx_exams/apps/lti/tests.py @@ -1,3 +1,5 @@ -from django.test import TestCase +""" Tests for LTI """ + +from django.test import TestCase # pylint: disable=unused-import # Create your tests here. diff --git a/edx_exams/apps/lti/urls.py b/edx_exams/apps/lti/urls.py index ffc8d883..07bbbd70 100644 --- a/edx_exams/apps/lti/urls.py +++ b/edx_exams/apps/lti/urls.py @@ -1,3 +1,7 @@ +""" +LTI URLs +""" + from django.urls import path from . import views diff --git a/edx_exams/apps/lti/views.py b/edx_exams/apps/lti/views.py index 11680ab8..a64525eb 100644 --- a/edx_exams/apps/lti/views.py +++ b/edx_exams/apps/lti/views.py @@ -1,3 +1,7 @@ +""" +LTI Views +""" + from urllib.parse import urljoin from django.conf import settings @@ -7,7 +11,6 @@ from django.utils.crypto import get_random_string from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods - from lti_consumer.lti_1p3.exceptions import ( BadJwtSignature, InvalidClaimValue, @@ -15,19 +18,20 @@ MissingRequiredClaim, NoSuitableKeys, TokenSignatureExpired, - UnauthorizedToken, + UnauthorizedToken ) from edx_exams.apps.lti.api import ( get_lti1p3_consumer, get_lti_preflight_url, - get_resource_link, get_optional_user_identity_claims, + get_resource_link ) + def start_proctoring(request): """ - This view represents a "Platform-Originating Message"; the Assessment Platform is directing the browser to send a + This view represents a "Platform-Originating Message"; the Assessment Platform is directing the browser to send a "start proctoring" message to the Proctoring Tool. Because the Assessment Platform acts as the identity provider (IdP), it must follow the "OpenID Connect Launch Flow". The first step is the third-party initiated login; it is a "third-party" initiated login to protect against login CSRF attacks. @@ -36,7 +40,7 @@ def start_proctoring(request): Relying Party. In this case, the initiator redirects to the Relying Party at its login initiation endpoint, which requests that the Relying Party send an Authentication Request to a specified OpenID Provider." https://www.imsglobal.org/spec/security/v1p0/#openid_connect_launch_flow - + This view redirects the learner's browser to the Proctoring Tool's initial OIDC login initiation URL, which acts as the first step of third-party initiated login. The Proctoring Tool should redirect the learner's browser to the Assessment Platform's "OIDC Authorization end-point", which starts the OpenID Connect authentication flow, @@ -53,9 +57,10 @@ def start_proctoring(request): return redirect(preflight_url) + def end_assessment(request): """ - This view represents a "Platform-Originating Message"; the Assessment Platform is directing the browser to send a + This view represents a "Platform-Originating Message"; the Assessment Platform is directing the browser to send a "end assessment" message to the Proctoring Tool. Because the Assessment Platform acts as the identity provider (IdP), it must follow the "OpenID Connect Launch Flow". The first step is the third-party initiated login; it is a "third-party" initiated login to protect against login CSRF attacks. @@ -64,9 +69,9 @@ def end_assessment(request): Relying Party. In this case, the initiator redirects to the Relying Party at its login initiation endpoint, which requests that the Relying Party send an Authentication Request to a specified OpenID Provider." https://www.imsglobal.org/spec/security/v1p0/#openid_connect_launch_flow - + This view redirects the learner's browser to the Proctoring Tool's initial OIDC login initiation URL, which acts as - the first step of third-party initiated login. The Proctoring Tool should redirect the learner's browser to the + the first step of third-party initiated login. The Proctoring Tool should redirect the learner's browser to the Assessment Platform's "OIDC Authorization end-point", which starts the OpenID Connect authentication flow, implemented by the authenticate view. @@ -86,12 +91,13 @@ def end_assessment(request): # We remove the end_assessment_return session data, since the learner has completed the proctoring flow. end_assessment_return = request.session.pop('end_assessment_return') if end_assessment_return: - lti_message_hint = 'LtiEndAssessment' - preflight_url = get_lti_preflight_url(lti_message_hint) + lti_message_hint = 'LtiEndAssessment' + preflight_url = get_lti_preflight_url(lti_message_hint) + + return redirect(preflight_url) - return redirect(preflight_url) + return JsonResponse() # pylint: disable=no-value-for-parameter - return JsonResponse() def public_keyset(request): """ @@ -104,13 +110,14 @@ def public_keyset(request): get_lti1p3_consumer().get_public_keyset(), ) + # We do not want Django's CSRF protection enabled for POSTs made by external services to this endpoint. # This is because Django uses the double-submit cookie method of CSRF protection, but the Proctoring Specification # lends itself better to the synchronizer token method of CSRF protection. # Django's method requires an anti-CSRF token to be included in both a cookie and a hidden from value in the request # to CSRF procted endpoints. # In the Proctoring Specification, there are a number of issues supporting the double-submit cookie method. -# 1. Django requires that a cookie is sent with the request to the Assessment Platform that contains the anti-CSRF +# 1. Django requires that a cookie is sent with the request to the Assessment Platform that contains the anti-CSRF # token. When the learner's browser makes a request to the start_proctoring view, an anti-CSRF token is set in the # cookie. # The default SameSite attribute for cookies is "Lax" (stored in the Django setting CSRF_COOKIE_SAMESITE), @@ -143,7 +150,7 @@ def authenticate(request): The Assessment Platform directs the learner's browser to make a request to the Proctoring Tool, acting as the "authentication response". This request must be made to the URL specified by the "redirect_uri" claim in the request. - + This signifies the second leg of the LTI launch workflow - otherwise know as the LTI launch or "OpenID Connect authorization flow". @@ -179,7 +186,7 @@ def authenticate(request): lti_consumer.enable_proctoring( # NOTE TO SELF: attempt_number is an auto-incrementing integer from 1 per learner, per assessment. - 29, # attempt_number + 29, # attempt_number session_data, resource_link, start_assessment_url=start_assessment_url, @@ -215,17 +222,18 @@ def authenticate(request): preflight_response = request.GET if preflight_response_method == 'GET' else request.POST context.update({ - 'preflight_response': preflight_response.dict(), - 'launch_request': lti_consumer.generate_launch_request( - preflight_response, - resource_link, - ) + 'preflight_response': preflight_response.dict(), + 'launch_request': lti_consumer.generate_launch_request( + preflight_response, + resource_link, + ) }) # This template renders an auto-submitting form, which makes a POST request to the redirect_uri, specified in the # Tool's response to the request the Assessment Platform made to the Tool's OIDC login initiation URL. return render(request, 'lti/lti_launch_request_form.html', context) + # We do not want Django's CSRF protection enabled for POSTs made by external services to this endpoint. # Please see the comment for the authenticate view for a more detailed justification. @csrf_exempt @@ -259,7 +267,7 @@ def start_assessment(request): session_data = request.session.get('lti_proctoring_session_data') start_assessment_url = urljoin(settings.ROOT_URL, reverse('lti:start-assessment')) - + # TODO: The resource link should uniquely represent the assessment in the Assessment Platform. # TODO: We SHOULD provide a value for the title attribute. # TODO: It's RECOMMENDED to provide a value for the description attribute. @@ -268,7 +276,7 @@ def start_assessment(request): lti_consumer.enable_proctoring( # NOTE TO SELF: attempt_number is an auto-incrementing integer from 1 per learner, per assessment. - 29, # attempt_number, + 29, # attempt_number, session_data, resource_link, start_assessment_url=start_assessment_url, @@ -281,13 +289,13 @@ def start_assessment(request): # Required user claim data lti_consumer.set_user_data( - user_id=user_id, - # Pass Django user role to library - # TODO: A role of 'student' is not correctly mapped to the corresponding LTI claim for the Proctoring - # Specification. - role='student' + user_id=user_id, + # Pass Django user role to library + # TODO: A role of 'student' is not correctly mapped to the corresponding LTI claim for the Proctoring + # Specification. + role='student' ) - + # These claims are optional. They are necessary to set in order to properly verify the verified_user claim, # if the Proctoring Tool includes it in the JWT. # TODO: This will need to have additional consideration for PII. diff --git a/edx_exams/settings/devstack.py b/edx_exams/settings/devstack.py index b7be9735..a0975854 100644 --- a/edx_exams/settings/devstack.py +++ b/edx_exams/settings/devstack.py @@ -28,7 +28,10 @@ # OAuth2 variables specific to backend service API calls. BACKEND_SERVICE_EDX_OAUTH2_KEY = os.environ.get('BACKEND_SERVICE_EDX_OAUTH2_KEY', 'edx_exams-backend-service-key') -BACKEND_SERVICE_EDX_OAUTH2_SECRET = os.environ.get('BACKEND_SERVICE_EDX_OAUTH2_SECRET', 'edx_exams-backend-service-secret') +BACKEND_SERVICE_EDX_OAUTH2_SECRET = os.environ.get( + 'BACKEND_SERVICE_EDX_OAUTH2_SECRET', + 'edx_exams-backend-service-secret' +) JWT_AUTH.update({ 'JWT_SECRET_KEY': 'lms-secret', diff --git a/edx_exams/settings/local.py b/edx_exams/settings/local.py index a29d7dbb..9ca11a15 100644 --- a/edx_exams/settings/local.py +++ b/edx_exams/settings/local.py @@ -78,5 +78,5 @@ # TODO: What security considerations are there for settings these settings to these values? Should we store the # anti-CSRF token on the attempt model instead and forego the need for the session cookie? # TODO: These settings should be moved to base.py if we feel comfortable with them. -SESSION_COOKIE_SAMESITE='None' -SESSION_COOKIE_SECURE=True +SESSION_COOKIE_SAMESITE = 'None' +SESSION_COOKIE_SECURE = True diff --git a/edx_exams/urls.py b/edx_exams/urls.py index b6117efa..48b2c876 100644 --- a/edx_exams/urls.py +++ b/edx_exams/urls.py @@ -19,8 +19,8 @@ from auth_backends.urls import oauth2_urlpatterns from django.conf import settings -from django.conf.urls import include, url from django.contrib import admin +from django.urls import include, path from edx_api_doc_tools import make_api_info, make_docs_urls from edx_exams.apps.api import urls as api_urls @@ -30,19 +30,19 @@ admin.autodiscover() urlpatterns = oauth2_urlpatterns + [ - url(r'^admin/', admin.site.urls), - url(r'^api/', include(api_urls)), - url(r'^auto_auth/$', core_views.AutoAuth.as_view(), name='auto_auth'), - url(r'', include('csrf.urls')), # Include csrf urls from edx-drf-extensions - url(r'^health/$', core_views.Health.as_view(), name='health'), - url(r'^lti/', include(lti_urls)), + path('admin/', admin.site.urls), + path('api/', include(api_urls)), + path('auto_auth/', core_views.AutoAuth.as_view(), name='auto_auth'), + path('', include('csrf.urls')), # Include csrf urls from edx-drf-extensions + path('health/', core_views.Health.as_view(), name='health'), + path('lti/', include(lti_urls)), ] if settings.DEBUG and os.environ.get('ENABLE_DJANGO_TOOLBAR', False): # pragma: no cover # Disable pylint import error because we don't install django-debug-toolbar # for CI build import debug_toolbar # pylint: disable=import-error - urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls))) + urlpatterns.append(path('__debug__/', include(debug_toolbar.urls))) api_info = make_api_info( title="edX Exams API", @@ -52,5 +52,5 @@ urlpatterns += make_docs_urls( api_info, - api_url_patterns=[url(r'^health/$', core_views.Health.as_view(), name='health'), url(r'^api/', include(api_urls))] + api_url_patterns=[path('health/', core_views.Health.as_view(), name='health'), path('api/', include(api_urls))] ) diff --git a/requirements/base.in b/requirements/base.in index 191a8e38..f15dcc53 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,6 +4,7 @@ Django # Web application framework django-cors-headers django-extensions +django-model-utils django-rest-swagger django-waffle djangorestframework diff --git a/requirements/base.txt b/requirements/base.txt index da2d5376..ea8883a2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -46,6 +46,7 @@ django==3.2.13 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -59,7 +60,7 @@ django==3.2.13 # openedx-filters django-config-models==2.3.0 # via lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/base.in django-crum==0.7.9 # via edx-django-utils @@ -67,9 +68,11 @@ django-extensions==3.1.5 # via -r requirements/base.in django-filter==21.1 # via lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/base.in django-rest-swagger==2.2.0 # via -r requirements/base.in -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/base.in # edx-django-utils diff --git a/requirements/ci.txt b/requirements/ci.txt new file mode 100644 index 00000000..2d11ff0c --- /dev/null +++ b/requirements/ci.txt @@ -0,0 +1,46 @@ +# +# This file is autogenerated by pip-compile with python 3.8 +# To update, run: +# +# make upgrade +# +certifi==2022.5.18.1 + # via requests +charset-normalizer==2.0.12 + # via requests +codecov==2.1.12 + # via -r requirements/ci.in +coverage==6.4.1 + # via codecov +distlib==0.3.4 + # via virtualenv +filelock==3.7.1 + # via + # tox + # virtualenv +idna==3.3 + # via requests +packaging==21.3 + # via tox +platformdirs==2.5.2 + # via virtualenv +pluggy==1.0.0 + # via tox +py==1.11.0 + # via tox +pyparsing==3.0.9 + # via packaging +requests==2.27.1 + # via codecov +six==1.16.0 + # via + # tox + # virtualenv +toml==0.10.2 + # via tox +tox==3.25.0 + # via -r requirements/ci.in +urllib3==1.26.9 + # via requests +virtualenv==20.14.1 + # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 668c7444..a9f15a15 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -106,6 +106,7 @@ django==3.2.13 # django-debug-toolbar # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -122,7 +123,7 @@ django-config-models==2.3.0 # via # -r requirements/validation.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/validation.txt django-crum==0.7.9 # via @@ -138,9 +139,11 @@ django-filter==21.1 # via # -r requirements/validation.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/validation.txt django-rest-swagger==2.2.0 # via -r requirements/validation.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/validation.txt # edx-django-utils @@ -239,7 +242,7 @@ jsonfield==3.1.0 # via # -r requirements/validation.txt # lti-consumer-xblock -keyring==23.5.1 +keyring==23.6.0 # via # -r requirements/validation.txt # twine @@ -313,7 +316,7 @@ pep517==0.12.0 # pip-tools pip-tools==6.6.2 # via -r requirements/pip-tools.txt -pkginfo==1.8.2 +pkginfo==1.8.3 # via # -r requirements/validation.txt # twine @@ -371,7 +374,7 @@ pyjwt[crypto]==2.4.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==2.14.0 +pylint==2.14.1 # via # -r requirements/validation.txt # edx-lint diff --git a/requirements/doc.txt b/requirements/doc.txt index f3dfe60c..f81f33e0 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -104,6 +104,7 @@ django==3.2.13 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -119,7 +120,7 @@ django-config-models==2.3.0 # via # -r requirements/test.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/test.txt django-crum==0.7.9 # via @@ -133,9 +134,11 @@ django-filter==21.1 # via # -r requirements/test.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/test.txt django-rest-swagger==2.2.0 # via -r requirements/test.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/test.txt # edx-django-utils @@ -240,7 +243,7 @@ jsonfield==3.1.0 # via # -r requirements/test.txt # lti-consumer-xblock -keyring==23.5.1 +keyring==23.6.0 # via twine lazy==1.4 # via @@ -308,7 +311,7 @@ pbr==5.9.0 # stevedore pep517==0.12.0 # via build -pkginfo==1.8.2 +pkginfo==1.8.3 # via twine platformdirs==2.5.2 # via @@ -357,7 +360,7 @@ pyjwt[crypto]==2.4.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==2.14.0 +pylint==2.14.1 # via # -r requirements/test.txt # edx-lint diff --git a/requirements/pip.txt b/requirements/pip.txt index 8510da19..caa4a5b9 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -10,5 +10,5 @@ wheel==0.37.1 # The following packages are considered to be unsafe in a requirements file: pip==22.1.2 # via -r requirements/pip.in -setuptools==62.3.2 +setuptools==62.3.3 # via -r requirements/pip.in diff --git a/requirements/production.txt b/requirements/production.txt index c0721d42..ad6c4bb6 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -62,6 +62,7 @@ django==3.2.13 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -77,7 +78,7 @@ django-config-models==2.3.0 # via # -r requirements/base.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/base.txt django-crum==0.7.9 # via @@ -89,9 +90,11 @@ django-filter==21.1 # via # -r requirements/base.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/base.txt django-rest-swagger==2.2.0 # via -r requirements/base.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/base.txt # edx-django-utils diff --git a/requirements/quality.txt b/requirements/quality.txt index 2b2ffd51..949b14d6 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -98,6 +98,7 @@ django==3.2.13 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -113,7 +114,7 @@ django-config-models==2.3.0 # via # -r requirements/test.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/test.txt django-crum==0.7.9 # via @@ -127,9 +128,11 @@ django-filter==21.1 # via # -r requirements/test.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/test.txt django-rest-swagger==2.2.0 # via -r requirements/test.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/test.txt # edx-django-utils @@ -225,7 +228,7 @@ jsonfield==3.1.0 # via # -r requirements/test.txt # lti-consumer-xblock -keyring==23.5.1 +keyring==23.6.0 # via twine lazy==1.4 # via @@ -289,7 +292,7 @@ pbr==5.9.0 # via # -r requirements/test.txt # stevedore -pkginfo==1.8.2 +pkginfo==1.8.3 # via twine platformdirs==2.5.2 # via @@ -340,7 +343,7 @@ pyjwt[crypto]==2.4.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==2.14.0 +pylint==2.14.1 # via # -r requirements/test.txt # edx-lint diff --git a/requirements/test.txt b/requirements/test.txt index 15fa629a..66953d73 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -84,6 +84,7 @@ distlib==0.3.4 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -99,7 +100,7 @@ django-config-models==2.3.0 # via # -r requirements/base.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via -r requirements/base.txt django-crum==0.7.9 # via @@ -113,9 +114,11 @@ django-filter==21.1 # via # -r requirements/base.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via -r requirements/base.txt django-rest-swagger==2.2.0 # via -r requirements/base.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/base.txt # edx-django-utils @@ -293,7 +296,7 @@ pyjwt[crypto]==2.4.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==2.14.0 +pylint==2.14.1 # via # edx-lint # pylint-celery diff --git a/requirements/validation.txt b/requirements/validation.txt index 49a8fcc1..513000ee 100644 --- a/requirements/validation.txt +++ b/requirements/validation.txt @@ -118,6 +118,7 @@ django==3.2.13 # django-crum # django-extensions # django-filter + # django-model-utils # djangorestframework # drf-jwt # drf-yasg @@ -134,7 +135,7 @@ django-config-models==2.3.0 # -r requirements/quality.txt # -r requirements/test.txt # lti-consumer-xblock -django-cors-headers==3.12.0 +django-cors-headers==3.13.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -156,11 +157,15 @@ django-filter==21.1 # -r requirements/quality.txt # -r requirements/test.txt # lti-consumer-xblock +django-model-utils==4.2.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt django-rest-swagger==2.2.0 # via # -r requirements/quality.txt # -r requirements/test.txt -django-waffle==2.4.1 +django-waffle==2.5.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -284,7 +289,7 @@ jsonfield==3.1.0 # -r requirements/quality.txt # -r requirements/test.txt # lti-consumer-xblock -keyring==23.5.1 +keyring==23.6.0 # via # -r requirements/quality.txt # twine @@ -368,7 +373,7 @@ pbr==5.9.0 # -r requirements/quality.txt # -r requirements/test.txt # stevedore -pkginfo==1.8.2 +pkginfo==1.8.3 # via # -r requirements/quality.txt # twine @@ -430,7 +435,7 @@ pyjwt[crypto]==2.4.0 # edx-drf-extensions # edx-rest-api-client # social-auth-core -pylint==2.14.0 +pylint==2.14.1 # via # -r requirements/quality.txt # -r requirements/test.txt diff --git a/tox.ini b/tox.ini index 95fd34d2..7b3e9294 100644 --- a/tox.ini +++ b/tox.ini @@ -36,25 +36,25 @@ addopts = --cov edx_exams --cov-report term-missing --cov-report xml norecursedirs = .* docs requirements site-packages [testenv] -deps = +deps = django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 -r{toxinidir}/requirements/test.txt -commands = +commands = pytest {posargs} [testenv:docs] -setenv = +setenv = DJANGO_SETTINGS_MODULE = edx_exams.settings.test PYTHONPATH = {toxinidir} # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx. SPHINXOPTS = -W -whitelist_externals = +whitelist_externals = make rm -deps = +deps = -r{toxinidir}/requirements/doc.txt -commands = +commands = doc8 --ignore-path docs/_build README.rst docs rm -f docs/edx_exams.rst rm -f docs/modules.rst @@ -62,19 +62,19 @@ commands = make -e -C docs html [testenv:translations] -whitelist_externals = +whitelist_externals = make -deps = +deps = -r{toxinidir}/requirements/dev.txt -commands = +commands = make validate_translations [testenv:quality] -whitelist_externals = +whitelist_externals = make -deps = +deps = -r{toxinidir}/requirements/quality.txt -commands = +commands = pylint edx_exams test_utils manage.py pycodestyle edx_exams manage.py pydocstyle edx_exams manage.py @@ -82,10 +82,10 @@ commands = make selfcheck [testenv:pii_check] -setenv = +setenv = DJANGO_SETTINGS_MODULE = edx_exams.settings.test -deps = +deps = -r{toxinidir}/requirements/test.txt -commands = +commands = code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage