diff --git a/.github/workflows/pylint-checks.yml b/.github/workflows/pylint-checks.yml deleted file mode 100644 index 91141c0c29ed..000000000000 --- a/.github/workflows/pylint-checks.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Pylint Checks - -on: - pull_request: - merge_group: - push: - branches: - - master - -jobs: - run-pylint: - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - include: - - module-name: lms-1 - path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/" - - module-name: lms-2 - path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/learner_home/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py" - - module-name: openedx-1 - path: "openedx/core/types/ openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/content_staging/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/djangoapps/course_live/" - - module-name: openedx-2 - path: "openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/ openedx/envs/ openedx/core/djangoapps/notifications/ openedx/core/djangoapps/staticfiles/ openedx/core/djangoapps/content_tagging/ openedx/core/djangoapps/authz/" - - module-name: common - path: "common" - - module-name: cms - path: "cms" - - module-name: xmodule - path: "xmodule" - - name: pylint ${{ matrix.module-name }} - steps: - - name: Check out repo - uses: actions/checkout@v6 - - - name: Install required system packages - run: sudo apt-get update && sudo apt-get install libxmlsec1-dev - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: 3.12 - - - name: Get pip cache dir - id: pip-cache-dir - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - - name: Cache pip dependencies - id: cache-dependencies - uses: actions/cache@v5 - with: - path: ${{ steps.pip-cache-dir.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('requirements/edx/development.txt') }} - restore-keys: ${{ runner.os }}-pip- - - - name: Install required Python dependencies - run: | - # dev-requirements is needed because the linter will otherwise - # trip over some dev-only things like django-debug-toolbar - # (import debug_toolbar) that aren't in testing.txt. - make dev-requirements - # After all requirements are installed, check that they're consistent with each other - pip check - - - name: Run quality tests - run: | - pylint ${{ matrix.path }} - - # This job aggregates test results. It's the required check for branch protection. - # https://github.com/marketplace/actions/alls-green#why - # https://github.com/orgs/community/discussions/33579 - success: - name: Pylint checks successful - if: always() - needs: - - run-pylint - runs-on: ubuntu-24.04 - steps: - - name: Decide whether the needed jobs succeeded or failed - # uses: re-actors/alls-green@v1.2.1 - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe - with: - jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index b57b713eb93a..213bd860ce2a 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -6,11 +6,40 @@ on: push: branches: - master - - open-release/lilac.master + - release/** jobs: - run_tests: - name: Quality Others + ruff: + name: Ruff + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Get pip cache dir + id: pip-cache-dir + run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + + - name: Cache pip dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.pip-cache-dir.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('requirements/edx/testing.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install quality requirements + run: pip install -r requirements/edx/testing.txt + + - name: Run ruff + run: ruff check --output-format=github . + + policy-checks: + name: Policy & Security Checks + needs: [ruff] runs-on: ${{ matrix.os }} strategy: matrix: @@ -73,12 +102,11 @@ jobs: run: | pip install -e . - - name: Run Quality Tests + - name: Run Policy Checks env: PIP_SRC: ${{ runner.temp }} TARGET_BRANCH: ${{ github.base_ref }} run: | - ruff check --output-format=github . make xsslint make pii_check make check_keywords @@ -93,3 +121,82 @@ jobs: test_root/log/**/*.log *.log overwrite: true + + run-pylint: + needs: [ruff] + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - module-name: lms-1 + path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/" + - module-name: lms-2 + path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/learner_home/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py" + - module-name: openedx-1 + path: "openedx/core/types/ openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/content_staging/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/djangoapps/course_live/" + - module-name: openedx-2 + path: "openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/ openedx/envs/ openedx/core/djangoapps/notifications/ openedx/core/djangoapps/staticfiles/ openedx/core/djangoapps/content_tagging/ openedx/core/djangoapps/authz/" + - module-name: common + path: "common" + - module-name: cms + path: "cms" + - module-name: xmodule + path: "xmodule" + + name: pylint ${{ matrix.module-name }} + steps: + - name: Check out repo + uses: actions/checkout@v6 + + - name: Install required system packages + run: sudo apt-get update && sudo apt-get install libxmlsec1-dev + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Get pip cache dir + id: pip-cache-dir + run: | + echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + + - name: Cache pip dependencies + id: cache-dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.pip-cache-dir.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('requirements/edx/development.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install required Python dependencies + run: | + # dev-requirements is needed because the linter will otherwise + # trip over some dev-only things like django-debug-toolbar + # (import debug_toolbar) that aren't in testing.txt. + make dev-requirements + # After all requirements are installed, check that they're consistent with each other + pip check + + - name: Run quality tests + run: | + pylint ${{ matrix.path }} + + # This job aggregates test results. It's the required check for branch protection. + # https://github.com/marketplace/actions/alls-green#why + # https://github.com/orgs/community/discussions/33579 + success: + name: Quality checks successful + if: always() + needs: + - ruff + - policy-checks + - run-pylint + runs-on: ubuntu-24.04 + steps: + - name: Decide whether the needed jobs succeeded or failed + # uses: re-actors/alls-green@v1.2.1 + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe + with: + jobs: ${{ toJSON(needs) }} diff --git a/cms/djangoapps/api/v1/serializers/course_runs.py b/cms/djangoapps/api/v1/serializers/course_runs.py index ab56ea40d341..84fca974730d 100644 --- a/cms/djangoapps/api/v1/serializers/course_runs.py +++ b/cms/djangoapps/api/v1/serializers/course_runs.py @@ -38,7 +38,7 @@ class CourseRunScheduleSerializer(serializers.Serializer): # lint-amnesty, pyli enrollment_end = serializers.DateTimeField(allow_null=True, required=False) -class CourseRunTeamSerializer(serializers.Serializer): # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseRunTeamSerializer(serializers.Serializer): # pylint: disable=abstract-method, missing-class-docstring def to_internal_value(self, data): """Overriding this to support deserialization, for write operations.""" for member in data: @@ -61,7 +61,7 @@ def get_attribute(self, instance): return instance -class CourseRunTeamSerializerMixin(serializers.Serializer): # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseRunTeamSerializerMixin(serializers.Serializer): # pylint: disable=abstract-method, missing-class-docstring team = CourseRunTeamSerializer(required=False) def update_team(self, instance, team): # lint-amnesty, pylint: disable=missing-function-docstring @@ -100,7 +100,7 @@ def to_internal_value(self, data): return data == 'self_paced' -class CourseRunImageSerializer(serializers.Serializer): # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseRunImageSerializer(serializers.Serializer): # pylint: disable=abstract-method, missing-class-docstring # We set an empty default to prevent the parent serializer from attempting # to save this value to the Course object. card_image = CourseRunImageField(source='course_image', default=empty) @@ -121,7 +121,7 @@ class CourseRunSerializerCommonFieldsMixin(serializers.Serializer): # lint-amne choices=((False, 'instructor_paced'), (True, 'self_paced'),)) -class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer): # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer): # pylint: disable=abstract-method, missing-class-docstring id = serializers.CharField(read_only=True) title = serializers.CharField(source='display_name') images = CourseRunImageSerializer(source='*', required=False) @@ -155,7 +155,7 @@ def create(self, validated_data): return instance -class CourseRunRerunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseRunRerunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, # pylint: disable=abstract-method, missing-class-docstring serializers.Serializer): title = serializers.CharField(source='display_name', required=False) number = serializers.CharField(source='id.course', required=False) @@ -200,7 +200,7 @@ def update(self, instance, validated_data): return course_run -class CourseCloneSerializer(serializers.Serializer): # lint-amnesty, pylint: disable=abstract-method, missing-class-docstring +class CourseCloneSerializer(serializers.Serializer): # pylint: disable=abstract-method, missing-class-docstring source_course_id = serializers.CharField() destination_course_id = serializers.CharField() diff --git a/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py b/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py index 4de6209e3729..eaeb9732340d 100644 --- a/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py +++ b/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py @@ -344,7 +344,7 @@ def test_rerun(self, pacing_type, expected_self_paced_value, number): user = UserFactory() role = 'instructor' run = '3T2017' - url = reverse('api:v1:course_run-rerun', kwargs={'pk': str(original_course_run.id)}) # lint-amnesty, pylint: disable=no-member + url = reverse('api:v1:course_run-rerun', kwargs={'pk': str(original_course_run.id)}) # pylint: disable=no-member data = { 'run': run, 'schedule': { @@ -383,7 +383,7 @@ def test_rerun(self, pacing_type, expected_self_paced_value, number): self.assert_course_access_role_count(course_run, 1) course_orgs = get_course_organizations(course_run_key) self.assertEqual(len(course_orgs), 1) # noqa: PT009 - self.assertEqual(course_orgs[0]['short_name'], original_course_run.id.org) # lint-amnesty, pylint: disable=no-member # noqa: PT009 + self.assertEqual(course_orgs[0]['short_name'], original_course_run.id.org) # pylint: disable=no-member # noqa: PT009 def test_rerun_duplicate_run(self): course_run = ToyCourseFactory() diff --git a/cms/djangoapps/api/v1/views/course_runs.py b/cms/djangoapps/api/v1/views/course_runs.py index 065714a51218..f9f2f4cbabf2 100644 --- a/cms/djangoapps/api/v1/views/course_runs.py +++ b/cms/djangoapps/api/v1/views/course_runs.py @@ -53,7 +53,7 @@ def retrieve(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=u serializer = self.get_serializer(course_run) return Response(serializer.data) - def update(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument + def update(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument course_run = self.get_object() partial = kwargs.pop('partial', False) @@ -77,7 +77,7 @@ def create(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unu methods=['post', 'put'], parser_classes=(parsers.FormParser, parsers.MultiPartParser,), serializer_class=CourseRunImageSerializer) - def images(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument + def images(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument course_run = self.get_object() serializer = CourseRunImageSerializer(course_run, data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) @@ -85,7 +85,7 @@ def images(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=mis return Response(serializer.data) @action(detail=True, methods=['post']) - def rerun(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument + def rerun(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument course_run = self.get_object() serializer = CourseRunRerunSerializer(course_run, data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) diff --git a/cms/djangoapps/contentstore/api/views/course_quality.py b/cms/djangoapps/contentstore/api/views/course_quality.py index 1f51622cb4ab..acc276c018dd 100644 --- a/cms/djangoapps/contentstore/api/views/course_quality.py +++ b/cms/djangoapps/contentstore/api/views/course_quality.py @@ -135,7 +135,7 @@ def _execute_method_and_log_time(log_time, func, *args): return Response(response) - def _required_course_depth(self, request, all_requested): # lint-amnesty, pylint: disable=missing-function-docstring + def _required_course_depth(self, request, all_requested): # pylint: disable=missing-function-docstring if get_bool_param(request, 'units', all_requested): # The num_blocks metric for "units" requires retrieving all blocks in the graph. return None diff --git a/cms/djangoapps/contentstore/asset_storage_handlers.py b/cms/djangoapps/contentstore/asset_storage_handlers.py index 06a76a904fcf..35b4061ded50 100644 --- a/cms/djangoapps/contentstore/asset_storage_handlers.py +++ b/cms/djangoapps/contentstore/asset_storage_handlers.py @@ -269,7 +269,7 @@ def _assets_json(request, course_key): assets_usage_locations_map = _get_asset_usage_path(course_key, assets) - if request_options['requested_page'] > 0 and first_asset_to_display_index >= total_count and total_count > 0: # lint-amnesty, pylint: disable=chained-comparison + if request_options['requested_page'] > 0 and first_asset_to_display_index >= total_count and total_count > 0: # pylint: disable=chained-comparison _update_options_to_requery_final_page(query_options, total_count) current_page = query_options['current_page'] first_asset_to_display_index = _get_first_asset_index(current_page, requested_page_size) @@ -735,7 +735,7 @@ def _check_existence_and_get_asset_content(asset_key): # lint-amnesty, pylint: raise AssetNotFoundException # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 -def _delete_thumbnail(thumbnail_location, course_key, asset_key): # lint-amnesty, pylint: disable=missing-function-docstring +def _delete_thumbnail(thumbnail_location, course_key, asset_key): # pylint: disable=missing-function-docstring if thumbnail_location is not None: # We are ignoring the value of the thumbnail_location-- we only care whether diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index 40d11817667e..9169e7f11472 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -32,6 +32,6 @@ # .. toggle_use_cases: temporary # .. toggle_creation_date: 2021-07-12 # .. toggle_target_removal_date: 2021-12-31 -# .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work. +# .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work. # noqa: E501 # .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844 CUSTOM_RELATIVE_DATES = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.custom_relative_dates', __name__) diff --git a/cms/djangoapps/contentstore/course_group_config.py b/cms/djangoapps/contentstore/course_group_config.py index 2cbab53a6e6e..6126b6785805 100644 --- a/cms/djangoapps/contentstore/course_group_config.py +++ b/cms/djangoapps/contentstore/course_group_config.py @@ -67,7 +67,7 @@ def parse(json_string): try: configuration = json.loads(json_string.decode("utf-8")) except ValueError: - raise GroupConfigurationsValidationError(_("invalid JSON")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise GroupConfigurationsValidationError(_("invalid JSON")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 configuration["version"] = UserPartition.VERSION return configuration @@ -116,7 +116,7 @@ def get_user_partition(self): try: return UserPartition.from_json(self.configuration) except ReadOnlyUserPartitionError: - raise GroupConfigurationsValidationError(_("unable to load this type of group configuration")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise GroupConfigurationsValidationError(_("unable to load this type of group configuration")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 @staticmethod def _get_usage_dict(course, unit, block, scheme_name=None): diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index 3061907a847d..0d4e5b08f19f 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -114,7 +114,7 @@ def remove_deleted_items(cls, searcher, structure_key, exclude_items): searcher.remove(result_ids) @classmethod - def index(cls, modulestore, structure_key, triggered_at=None, reindex_age=REINDEX_AGE, timeout=INDEXING_REQUEST_TIMEOUT): # lint-amnesty, pylint: disable=line-too-long, too-many-statements + def index(cls, modulestore, structure_key, triggered_at=None, reindex_age=REINDEX_AGE, timeout=INDEXING_REQUEST_TIMEOUT): # pylint: disable=too-many-statements # noqa: E501 """ Process course for indexing @@ -630,7 +630,7 @@ def index_about_information(cls, modulestore, course): # Broad exception handler so that a single bad property does not scupper the collection of others try: section_content = about_information.get_value(**about_context) - except: # pylint: disable=bare-except + except: # noqa: E722 section_content = None log.warning( "Course discovery could not collect property %s for course %s", diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py index 5a35d3fbe9f8..384dd1041e7a 100644 --- a/cms/djangoapps/contentstore/exams.py +++ b/cms/djangoapps/contentstore/exams.py @@ -33,7 +33,7 @@ def register_exams(course_key): course = modulestore().get_course(course_key) if course is None: - raise ItemNotFoundError("Course {} does not exist", str(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple + raise ItemNotFoundError("Course {} does not exist", str(course_key)) # pylint: disable=raising-format-tuple # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items( diff --git a/cms/djangoapps/contentstore/git_export_utils.py b/cms/djangoapps/contentstore/git_export_utils.py index 5f1c7877c4b2..1e63dd0ced7d 100644 --- a/cms/djangoapps/contentstore/git_export_utils.py +++ b/cms/djangoapps/contentstore/git_export_utils.py @@ -136,7 +136,7 @@ def export_to_git(course_id, repo, user='', rdir=None): root_dir, course_dir) except (OSError, AttributeError): log.exception('Failed export to xml') - raise GitExportError(GitExportError.XML_EXPORT_FAIL) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise GitExportError(GitExportError.XML_EXPORT_FAIL) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 # Get current branch if not already set if not branch: diff --git a/cms/djangoapps/contentstore/management/commands/create_course.py b/cms/djangoapps/contentstore/management/commands/create_course.py index 86e88beef774..1940c0dfc334 100644 --- a/cms/djangoapps/contentstore/management/commands/create_course.py +++ b/cms/djangoapps/contentstore/management/commands/create_course.py @@ -52,7 +52,7 @@ def get_user(self, user): try: user_object = user_from_str(user) except User.DoesNotExist: - raise CommandError(f"No user {user} found.") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError(f"No user {user} found.") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 return user_object def handle(self, *args, **options): diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index 8a64c64e26fe..729159278511 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -67,7 +67,7 @@ def handle(self, *args, **options): course_key = str(options['course_key']) course_key = CourseKey.from_string(course_key) except InvalidKeyError: - raise CommandError('Invalid course_key: {}'.format(options['course_key'])) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError('Invalid course_key: {}'.format(options['course_key'])) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if not modulestore().get_course(course_key): raise CommandError('Course not found: {}'.format(options['course_key'])) @@ -80,6 +80,6 @@ def handle(self, *args, **options): if options['remove_assets']: contentstore().delete_all_course_assets(course_key) - print(f'Deleted assets for course {course_key}') # lint-amnesty, pylint: disable=too-many-format-args + print(f'Deleted assets for course {course_key}') # pylint: disable=too-many-format-args print(f'Deleted course {course_key}') diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 76644d2d650b..e7f592e3b8b0 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -32,7 +32,7 @@ def handle(self, *args, **options): try: course_key = CourseKey.from_string(options['course_id']) except InvalidKeyError: - raise CommandError("Invalid course_key: '%s'." % options['course_id']) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, UP031 + raise CommandError("Invalid course_key: '%s'." % options['course_id']) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501, UP031 if not modulestore().get_course(course_key): raise CommandError("Course with %s key not found." % options['course_id']) # noqa: UP031 diff --git a/cms/djangoapps/contentstore/management/commands/export_content_library.py b/cms/djangoapps/contentstore/management/commands/export_content_library.py index 7a6d4cfd2599..bbb21f667449 100644 --- a/cms/djangoapps/contentstore/management/commands/export_content_library.py +++ b/cms/djangoapps/contentstore/management/commands/export_content_library.py @@ -34,7 +34,7 @@ def handle(self, *args, **options): try: library_key = CourseKey.from_string(options['library_id']) except InvalidKeyError: - raise CommandError('Invalid library ID: "{}".'.format(options['library_id'])) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError('Invalid library ID: "{}".'.format(options['library_id'])) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if not isinstance(library_key, LibraryLocator): raise CommandError('Argument "{}" is not a library key'.format(options['library_id'])) @@ -50,7 +50,7 @@ def handle(self, *args, **options): # Generate archive using the handy tasks implementation tarball = tasks.create_export_tarball(library, library_key, {}, None) except Exception as e: - raise CommandError(f'Failed to export "{library_key}" with "{e}"') # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError(f'Failed to export "{library_key}" with "{e}"') # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 with tarball: # Save generated archive with keyed filename prefix, suffix, n = str(library_key).replace(':', '+'), '.tar.gz', 0 diff --git a/cms/djangoapps/contentstore/management/commands/export_olx.py b/cms/djangoapps/contentstore/management/commands/export_olx.py index 6abed6bd043a..5064737eb7ce 100644 --- a/cms/djangoapps/contentstore/management/commands/export_olx.py +++ b/cms/djangoapps/contentstore/management/commands/export_olx.py @@ -49,7 +49,7 @@ def handle(self, *args, **options): except InvalidKeyError: raise CommandError("Unparsable course_id") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 except IndexError: - raise CommandError("Insufficient arguments") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError("Insufficient arguments") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 filename = options['output'] pipe_results = False diff --git a/cms/djangoapps/contentstore/management/commands/generate_courses.py b/cms/djangoapps/contentstore/management/commands/generate_courses.py index b1a2889a31a5..b87d2de4b6c3 100644 --- a/cms/djangoapps/contentstore/management/commands/generate_courses.py +++ b/cms/djangoapps/contentstore/management/commands/generate_courses.py @@ -35,7 +35,7 @@ def handle(self, *args, **options): except ValueError: raise CommandError("Invalid JSON object") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 except KeyError: - raise CommandError("JSON object is missing courses list") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError("JSON object is missing courses list") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 for course_settings in courses: # Validate course @@ -112,7 +112,7 @@ def _process_course_fields(self, fields): fields[field] = Date().from_json(date_json) logger.info(field + " has been set to " + date_json) except Exception: # pylint: disable=broad-except - logger.info("The date string could not be parsed for " + field) # lint-amnesty, pylint: disable=logging-not-lazy + logger.info("The date string could not be parsed for " + field) # pylint: disable=logging-not-lazy del fields[field] elif field in course_tab_list_fields: # Generate CourseTabList object from the json value @@ -121,11 +121,11 @@ def _process_course_fields(self, fields): fields[field] = CourseTabList().from_json(course_tab_list_json) logger.info(field + " has been set to " + course_tab_list_json) except Exception: # pylint: disable=broad-except - logger.info("The course tab list string could not be parsed for " + field) # lint-amnesty, pylint: disable=logging-not-lazy + logger.info("The course tab list string could not be parsed for " + field) # pylint: disable=logging-not-lazy del fields[field] else: # CourseField is valid and has been set - logger.info(field + " has been set to " + str(fields[field])) # lint-amnesty, pylint: disable=logging-not-lazy + logger.info(field + " has been set to " + str(fields[field])) # pylint: disable=logging-not-lazy for field in all_fields: if field not in fields: @@ -156,7 +156,7 @@ def _course_is_valid(self, course): if "fields" in course: for setting in required_field_settings: if setting not in course["fields"]: - logger.warning("Fields json is missing " + setting) # lint-amnesty, pylint: disable=logging-not-lazy + logger.warning("Fields json is missing " + setting) # pylint: disable=logging-not-lazy is_valid = False return is_valid diff --git a/cms/djangoapps/contentstore/management/commands/git_export.py b/cms/djangoapps/contentstore/management/commands/git_export.py index a8193f8e3409..1c2a21535603 100644 --- a/cms/djangoapps/contentstore/management/commands/git_export.py +++ b/cms/djangoapps/contentstore/management/commands/git_export.py @@ -50,7 +50,7 @@ def handle(self, *args, **options): try: course_key = CourseKey.from_string(options['course_loc']) except InvalidKeyError: - raise CommandError(str(git_export_utils.GitExportError.BAD_COURSE)) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError(str(git_export_utils.GitExportError.BAD_COURSE)) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 try: git_export_utils.export_to_git( diff --git a/cms/djangoapps/contentstore/management/commands/reindex_course.py b/cms/djangoapps/contentstore/management/commands/reindex_course.py index e9d97ab91c33..826d9ced9d32 100644 --- a/cms/djangoapps/contentstore/management/commands/reindex_course.py +++ b/cms/djangoapps/contentstore/management/commands/reindex_course.py @@ -60,7 +60,7 @@ def _parse_course_key(self, raw_value): try: result = CourseKey.from_string(raw_value) except InvalidKeyError: - raise CommandError("Invalid course_key: '%s'." % raw_value) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, UP031 + raise CommandError("Invalid course_key: '%s'." % raw_value) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501, UP031 if not isinstance(result, CourseLocator): raise CommandError(f"Argument {raw_value} is not a course key") diff --git a/cms/djangoapps/contentstore/management/commands/replace_v1_lib_refs_with_v2_in_courses.py b/cms/djangoapps/contentstore/management/commands/replace_v1_lib_refs_with_v2_in_courses.py index 20d9e11bb14e..85a6f8f9a617 100644 --- a/cms/djangoapps/contentstore/management/commands/replace_v1_lib_refs_with_v2_in_courses.py +++ b/cms/djangoapps/contentstore/management/commands/replace_v1_lib_refs_with_v2_in_courses.py @@ -62,7 +62,7 @@ def replace_all_library_source_blocks_ids(self, v1_to_v2_lib_map): def validate(self, v1_to_v2_lib_map): """ Validate that replace_all_library_source_blocks_ids was successful""" course_id_strings = list(CourseOverview.get_all_course_keys()) - tasks = group(validate_all_library_source_blocks_ids_for_course.s(course_id, v1_to_v2_lib_map) for course_id in course_id_strings) # lint-amnesty, pylint: disable=line-too-long + tasks = group(validate_all_library_source_blocks_ids_for_course.s(course_id, v1_to_v2_lib_map) for course_id in course_id_strings) # noqa: E501 results = tasks.apply_async() validation = set() diff --git a/cms/djangoapps/contentstore/management/commands/sync_courses.py b/cms/djangoapps/contentstore/management/commands/sync_courses.py index 75b4e4988a92..be24260a5618 100644 --- a/cms/djangoapps/contentstore/management/commands/sync_courses.py +++ b/cms/djangoapps/contentstore/management/commands/sync_courses.py @@ -36,7 +36,7 @@ def get_user(self, user): try: user_object = user_from_str(user) except User.DoesNotExist: - raise CommandError(f"No user {user} found.") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CommandError(f"No user {user} found.") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 return user_object def handle(self, *args, **options): diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py index 71904e997c15..84fcfbcd67fe 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py @@ -27,7 +27,7 @@ class ExportAllCourses(ModuleStoreTestCase): def setUp(self): """ Common setup. """ super().setUp() - self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # lint-amnesty, pylint: disable=protected-access + self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # pylint: disable=protected-access self.temp_dir = mkdtemp() self.addCleanup(shutil.rmtree, self.temp_dir) self.first_course = CourseFactory.create( diff --git a/cms/djangoapps/contentstore/proctoring.py b/cms/djangoapps/contentstore/proctoring.py index 7a6e120c9721..7e00327eeea7 100644 --- a/cms/djangoapps/contentstore/proctoring.py +++ b/cms/djangoapps/contentstore/proctoring.py @@ -41,7 +41,7 @@ def register_special_exams(course_key): course = modulestore().get_course(course_key) if course is None: - raise ItemNotFoundError("Course {} does not exist", str(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple + raise ItemNotFoundError("Course {} does not exist", str(course_key)) # pylint: disable=raising-format-tuple if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py index 496a50437104..094e545e3e12 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py @@ -38,7 +38,7 @@ def test_success_response(self): "First name", "First description", [Group(0, "Group A"), Group(1, "Group B"), Group(2, "Group C")], - ), # lint-amnesty, pylint: disable=line-too-long + ), ] self.save_course() diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py index 9da850244ccc..00edb50aad3b 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py @@ -120,14 +120,14 @@ def test_VideoTranscriptEnabledFlag_enabled(self): str(self.course.id) ) self.assertIn("transcript_preferences_handler_url", transcript_settings) # noqa: PT009 - self.assertEqual(expected_preference_handler, transcript_settings["transcript_preferences_handler_url"]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(expected_preference_handler, transcript_settings["transcript_preferences_handler_url"]) # noqa: PT009 expected_credentials_handler = reverse_course_url( 'transcript_credentials_handler', str(self.course.id) ) self.assertIn("transcript_credentials_handler_url", transcript_settings) # noqa: PT009 - self.assertEqual(expected_credentials_handler, transcript_settings["transcript_credentials_handler_url"]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(expected_credentials_handler, transcript_settings["transcript_credentials_handler_url"]) # noqa: PT009 with patch( 'openedx.core.djangoapps.video_config.toggles.XPERT_TRANSLATIONS_UI.is_enabled' ) as xpertTranslationfeature: diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py index 149732437215..18a0fafa658e 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py @@ -127,7 +127,7 @@ def setUp(self): self._publish_container(self.top_level_unit_id_2) self._publish_container(self.top_level_subsection_id) self._publish_container(self.top_level_section_id) - self.mock_upstream_link = f"{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/library/{self.library_id}/components?usageKey={self.video_lib_id}" # pylint: disable=line-too-long # noqa: E501 + self.mock_upstream_link = f"{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/library/{self.library_id}/components?usageKey={self.video_lib_id}" # noqa: E501 self.course = CourseFactory.create() add_users(self.superuser, CourseStaffRole(self.course.id), self.course_user) chapter = BlockFactory.create(category='chapter', parent=self.course) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 53476d0eb43a..8c9c6df4ae1c 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -75,7 +75,7 @@ def wrapper(*args, **kwargs): return task_decorator -def _create_catalog_data_for_signal(course_key: CourseKey) -> (Optional[datetime], Optional[CourseCatalogData]): # noqa: UP045 # pylint: disable=line-too-long +def _create_catalog_data_for_signal(course_key: CourseKey) -> (Optional[datetime], Optional[CourseCatalogData]): # noqa: UP045 """ Creates data for catalog-info-changed signal when course is published. diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index b73800053288..746b5e0eb227 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -437,7 +437,7 @@ def create_export_tarball(course_block, course_key, context, status=None): if parent_loc is not None: parent = modulestore().get_item(parent_loc) - except: # pylint: disable=bare-except + except: # noqa: E722 # if we have a nested exception, then we'll show the more generic error message pass @@ -525,7 +525,7 @@ def sync_discussion_settings(course_key, user): fields = ["enable_graded_units", "unit_level_visibility", "enable_in_context", "posting_restrictions"] # Plugin configuration is stored in the course settings under the provider name. - field_mappings = dict(zip(fields, fields)) | {"plugin_configuration": discussion_config.provider_type} # noqa: B905 # pylint: disable=line-too-long + field_mappings = dict(zip(fields, fields)) | {"plugin_configuration": discussion_config.provider_type} # noqa: B905 for attr_name, settings_key in field_mappings.items(): if settings_key in discussion_settings: @@ -1007,7 +1007,7 @@ def validate_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v @shared_task(time_limit=30) @set_code_owner_attribute -def replace_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v2_lib_map): # lint-amnesty, pylint: disable=useless-return +def replace_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v2_lib_map): # pylint: disable=useless-return """Search a Modulestore for all library source blocks in a course by querying mongo. replace all source_library_ids with the corresponding v2 value from the map. @@ -1066,7 +1066,7 @@ def replace_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v2 @shared_task(time_limit=30) @set_code_owner_attribute -def undo_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v2_lib_map): # lint-amnesty, pylint: disable=useless-return +def undo_all_library_source_blocks_ids_for_course(course_key_string, v1_to_v2_lib_map): # pylint: disable=useless-return """Search a Modulestore for all library source blocks in a course by querying mongo. replace all source_library_ids with the corresponding v1 value from the inverted map. This is exists to undo changes made previously. diff --git a/cms/djangoapps/contentstore/tests/test_clone_course.py b/cms/djangoapps/contentstore/tests/test_clone_course.py index 2449c9269288..2a4b3134681c 100644 --- a/cms/djangoapps/contentstore/tests/test_clone_course.py +++ b/cms/djangoapps/contentstore/tests/test_clone_course.py @@ -70,7 +70,7 @@ def test_space_in_asset_name_for_rerun_course(self): # Get & verify all assets of the course assets, count = contentstore().get_all_content_for_course(course.id) self.assertEqual(count, 1) # noqa: PT009 - self.assertEqual({asset['asset_key'].block_id for asset in assets}, course_assets) # lint-amnesty, pylint: disable=consider-using-set-comprehension # noqa: PT009 + self.assertEqual({asset['asset_key'].block_id for asset in assets}, course_assets) # pylint: disable=consider-using-set-comprehension # noqa: PT009 # rerun from split into split split_rerun_id = CourseLocator(org=org, course=course_number, run="2012_Q2") @@ -129,14 +129,14 @@ def test_rerun_course(self): ) # try to hit the generic exception catch - with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoPersistenceBackend.insert_course_index', Mock(side_effect=Exception)): # lint-amnesty, pylint: disable=line-too-long + with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoPersistenceBackend.insert_course_index', Mock(side_effect=Exception)): # noqa: E501 split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail") fields = {'display_name': 'total failure'} CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name']) result = rerun_course.delay(str(split_course3_id), str(split_course4_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertIn("exception: ", result.get()) # noqa: PT009 - self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error") # noqa: PT009 # pylint: disable=line-too-long + self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error") # noqa: PT009 CourseRerunState.objects.find_first( course_key=split_course4_id, state=CourseRerunUIStateManager.State.FAILED diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 5c33e156721b..b87d14c9a3fd 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -79,7 +79,7 @@ def decorated_func(*args, **kwargs): try: from PIL import Image except ImportError: - raise SkipTest("Pillow is not installed (or not found)") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise SkipTest("Pillow is not installed (or not found)") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if not getattr(Image.core, "jpeg_decoder", False): raise SkipTest("Pillow cannot open JPEG files") return func(*args, **kwargs) @@ -301,7 +301,7 @@ def test_rewrite_nonportable_links_on_import(self): html_block = self.store.get_item(html_block_location) self.assertIn('/jump_to_id/nonportable_link', html_block.data) # noqa: PT009 - def verify_content_existence(self, store, root_dir, course_id, dirname, category_name, filename_suffix=''): # lint-amnesty, pylint: disable=missing-function-docstring + def verify_content_existence(self, store, root_dir, course_id, dirname, category_name, filename_suffix=''): # pylint: disable=missing-function-docstring filesystem = OSFS(root_dir / 'test_export') self.assertTrue(filesystem.exists(dirname)) # noqa: PT009 @@ -441,7 +441,7 @@ def test_html_export_roundtrip(self): # get the sample HTML with styling information html_block = self.store.get_item(course_id.make_usage_key('html', 'with_styling')) - self.assertIn('

', html_block.data) # noqa: PT009 # pylint: disable=line-too-long + self.assertIn('

', html_block.data) # noqa: PT009 # get the sample HTML with just a simple tag information html_block = self.store.get_item(course_id.make_usage_key('html', 'just_img')) @@ -1447,7 +1447,7 @@ def test_capa_block(self): problem_loc = UsageKey.from_string(payload['locator']) problem = self.store.get_item(problem_loc) self.assertIsInstance(problem, ProblemBlock, "New problem is not a ProblemBlock") # noqa: PT009 - self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") # lint-amnesty, pylint: disable=line-too-long # noqa: PT009 + self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") # noqa: E501, PT009 def test_cms_imported_course_walkthrough(self): """ @@ -1921,7 +1921,7 @@ def test_rerun_course_video_upload_token(self): # Verify video upload pipeline is empty. source_course = self.store.get_course(source_course.id) new_course = self.store.get_course(destination_course_key) - self.assertDictEqual(source_course.video_upload_pipeline, {"course_video_upload_token": 'test-token'}) # noqa: PT009 # pylint: disable=line-too-long + self.assertDictEqual(source_course.video_upload_pipeline, {"course_video_upload_token": 'test-token'}) # noqa: PT009 self.assertEqual(new_course.video_upload_pipeline, {}) # noqa: PT009 def test_rerun_course_success(self): diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py index 31c9be4c2741..76bbd98bfc99 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -408,7 +408,7 @@ def _set_of_course_keys(course_list, key_attribute_name='id'): return {getattr(c, key_attribute_name) for c in course_list} found_courses, unsucceeded_course_actions = _accessible_courses_iter_for_tests(self.request) - self.assertSetEqual(_set_of_course_keys(courses + courses_in_progress), _set_of_course_keys(found_courses)) # noqa: PT009 # pylint: disable=line-too-long + self.assertSetEqual(_set_of_course_keys(courses + courses_in_progress), _set_of_course_keys(found_courses)) # noqa: PT009 self.assertSetEqual( # noqa: PT009 _set_of_course_keys(courses_in_progress), _set_of_course_keys(unsucceeded_course_actions, 'course_key') ) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 5d726627f27e..41ce4d3fd1cd 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -174,7 +174,7 @@ def test_disable_advanced_settings_feature(self, disable_advanced_settings): If this feature is enabled, only Django Staff/Superuser should be able to access the "Advanced Settings" page. For non-staff users the "Advanced Settings" tab link should not be visible. """ - advanced_settings_link_html = f"Advanced Settings".encode('utf-8') # noqa: UP012 # pylint: disable=line-too-long + advanced_settings_link_html = f"Advanced Settings".encode('utf-8') # noqa: UP012 with override_settings(FEATURES={ 'DISABLE_ADVANCED_SETTINGS': disable_advanced_settings, @@ -280,9 +280,9 @@ def compare_details_with_encoding(self, encoded, details, context): details['about_sidebar_html'], encoded['about_sidebar_html'], context + " about_sidebar_html not ==" ) self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==") # noqa: PT009 - self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") # noqa: PT009 self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") # noqa: PT009 - self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==") # noqa: PT009 self.assertEqual(details['language'], encoded['language'], context + " languages not ==") # noqa: PT009 def compare_date_fields(self, details, encoded, context, field): @@ -323,7 +323,7 @@ def test_upgrade_deadline(self, has_verified_mode, has_expiration_date): settings_details_url = get_url(self.course.id) response = self.client.get_html(settings_details_url) - self.assertEqual(b"Upgrade Deadline Date" in response.content, has_expiration_date and has_verified_mode) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(b"Upgrade Deadline Date" in response.content, has_expiration_date and has_verified_mode) # noqa: PT009 @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True}) @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True) @@ -743,11 +743,9 @@ def test_update_from_json(self, send_signal, tracker, uuid): # one for each of the calls to update_from_json() send_signal.assert_has_calls([ - # pylint: disable=line-too-long - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_3), - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_4), - # pylint: enable=line-too-long + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), # noqa: E501 + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_3), # noqa: E501 + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_4), # noqa: E501 ]) # one for each of the calls to update_from_json(); the last update doesn't actually change the parts of the @@ -865,10 +863,8 @@ def test_update_grader_from_json(self, send_signal, tracker, uuid): # one for each of the calls to update_grader_from_json() send_signal.assert_has_calls([ - # pylint: disable=line-too-long - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_3), - # pylint: enable=line-too-long + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), # noqa: E501 + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_3), # noqa: E501 ]) # one for each of the calls to update_grader_from_json() @@ -906,7 +902,7 @@ def test_update_cutoffs_from_json(self, tracker, uuid): test_grader.grade_cutoffs['Pass'] = 0.75 CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user) altered_grader = CourseGradingModel.fetch(self.course.id) - self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'") # noqa: PT009 # pylint: disable=line-too-long + self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'") # noqa: PT009 grading_policy_3 = self._grading_policy_hash_for_course() # one for each of the calls to update_cutoffs_from_json() @@ -936,7 +932,7 @@ def test_delete_grace_period(self): CourseGradingModel.update_grace_period_from_json( self.course.id, test_grader.grace_period, self.user) altered_grader = CourseGradingModel.fetch(self.course.id) - self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period") # noqa: PT009 # pylint: disable=line-too-long + self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period") # noqa: PT009 test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0} # Now delete the grace period @@ -981,10 +977,8 @@ def test_update_section_grader_type(self, send_signal, tracker, uuid): # one for each call to update_section_grader_type() send_signal.assert_has_calls([ - # pylint: disable=line-too-long - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_1), - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), - # pylint: enable=line-too-long + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_1), # noqa: E501 + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_2), # noqa: E501 ]) tracker.emit.assert_has_calls([ @@ -1061,11 +1055,9 @@ def test_add_delete_grader(self, send_signal): self.assertNotIn(original_model['graders'][1], updated_model['graders']) # noqa: PT009 send_signal.assert_has_calls([ # once for the POST - # pylint: disable=line-too-long - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_hash1), + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_hash1), # noqa: E501 # once for the DELETE - mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_hash2), - # pylint: enable=line-too-long + mock.call(sender=CourseGradingModel, user_id=self.user.id, course_key=self.course.id, grading_policy_hash=grading_policy_hash2), # noqa: E501 ]) def setup_test_set_get_section_grader_ajax(self): @@ -1346,7 +1338,7 @@ def test_validate_from_json_correct_inputs(self): # Tab gets tested in test_advanced_settings_munge_tabs self.assertIn('advanced_modules', test_model, 'Missing advanced_modules') # noqa: PT009 - self.assertEqual(test_model['advanced_modules']['value'], ['notes'], 'advanced_module is not updated') # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(test_model['advanced_modules']['value'], ['notes'], 'advanced_module is not updated') # noqa: PT009 def test_validate_from_json_wrong_inputs(self): # input incorrectly formatted data @@ -1417,7 +1409,7 @@ def test_update_from_json(self): self.assertIn('display_name', test_model, 'Missing editable metadata field') # noqa: PT009 self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value") # noqa: PT009 self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') # noqa: PT009 - self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") # noqa: PT009 def update_check(self, test_model): """ @@ -1426,9 +1418,9 @@ def update_check(self, test_model): self.assertIn('display_name', test_model, 'Missing editable metadata field') # noqa: PT009 self.assertEqual(test_model['display_name']['value'], self.course.display_name) # noqa: PT009 self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field') # noqa: PT009 - self.assertEqual(test_model['advertised_start']['value'], 'start A', "advertised_start not expected value") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(test_model['advertised_start']['value'], 'start A', "advertised_start not expected value") # noqa: PT009 self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field') # noqa: PT009 - self.assertEqual(test_model['days_early_for_beta']['value'], 2, "days_early_for_beta not expected value") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(test_model['days_early_for_beta']['value'], 2, "days_early_for_beta not expected value") # noqa: PT009 def test_http_fetch_initial_fields(self): response = self.client.get_json(self.course_setting_url) @@ -1465,7 +1457,7 @@ def test_http_update_from_json(self): self.assertIn('display_name', test_model, 'Missing editable metadata field') # noqa: PT009 self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value") # noqa: PT009 self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') # noqa: PT009 - self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") # noqa: PT009 @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True}) @patch('xmodule.util.xmodule_django.get_current_request') diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index f298b570b2b5..13629e5d6404 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -387,7 +387,7 @@ def _test_course_about_store_index(self, store): field_dictionary={"course": str(self.course.id)} ) self.assertEqual(response["total"], 1) # noqa: PT009 - self.assertEqual(response["results"][0]["data"]["content"]["short_description"], short_description) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(response["results"][0]["data"]["content"]["short_description"], short_description) # noqa: PT009 def _test_course_about_mode_index(self, store): """ @@ -459,7 +459,7 @@ def _test_course_location_null(self, store): result = response["results"][0]["data"] self.assertEqual(result["course_name"], "Search Index Test Course") # noqa: PT009 - self.assertEqual(result["location"], ["Week 1", CoursewareSearchIndexer.UNNAMED_MODULE_NAME, "Subsection 2"]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(result["location"], ["Week 1", CoursewareSearchIndexer.UNNAMED_MODULE_NAME, "Subsection 2"]) # noqa: PT009 @patch('django.conf.settings.SEARCH_ENGINE', 'search.tests.utils.ErroringIndexEngine') def _test_exception(self, store): @@ -584,7 +584,7 @@ def _test_large_course_deletion(self, store): load_factor = 6 try: self._do_test_large_course_deletion(store, load_factor) - except: # pylint: disable=bare-except + except: # noqa: E722 # Catch any exception here to see when we fail print(f"Failed with load_factor of {load_factor}") @@ -1199,7 +1199,7 @@ def test_content_group_gets_indexed(self): self.assertIn(self._html_experiment_group_result(self.html_unit4, [str(2)]), indexed_content) # noqa: PT009 self.assertIn(self._html_experiment_group_result(self.html_unit5, [str(3)]), indexed_content) # noqa: PT009 self.assertIn(self._html_experiment_group_result(self.html_unit6, [str(4)]), indexed_content) # noqa: PT009 - self.assertNotIn(self._html_experiment_group_result(self.html_unit6, [str(5)]), indexed_content) # noqa: PT009 # pylint: disable=line-too-long + self.assertNotIn(self._html_experiment_group_result(self.html_unit6, [str(5)]), indexed_content) # noqa: PT009 self.assertIn( # noqa: PT009 self._vertical_experiment_group_result(self.condition_0_vertical, [str(2)]), indexed_content diff --git a/cms/djangoapps/contentstore/tests/test_import.py b/cms/djangoapps/contentstore/tests/test_import.py index d7e8d7905842..af703184f776 100644 --- a/cms/djangoapps/contentstore/tests/test_import.py +++ b/cms/djangoapps/contentstore/tests/test_import.py @@ -239,7 +239,7 @@ def test_rewrite_reference_value_dict_draft(self): {"0": '9f0941d021414798836ef140fb5f6841', "1": '0faf29473cf1497baa33fcc828b179cd'}, ) - def _verify_split_test_import(self, target_course_name, source_course_name, split_test_name, groups_to_verticals): # lint-amnesty, pylint: disable=missing-function-docstring + def _verify_split_test_import(self, target_course_name, source_course_name, split_test_name, groups_to_verticals): # pylint: disable=missing-function-docstring module_store = modulestore() target_id = module_store.make_course_key('testX', target_course_name, 'copy_run') import_course_from_xml( diff --git a/cms/djangoapps/contentstore/tests/test_import_draft_order.py b/cms/djangoapps/contentstore/tests/test_import_draft_order.py index 16ba4872655f..b039f3fcd209 100644 --- a/cms/djangoapps/contentstore/tests/test_import_draft_order.py +++ b/cms/djangoapps/contentstore/tests/test_import_draft_order.py @@ -38,7 +38,7 @@ def test_order(self): # '5a05be9d59fc4bb79282c94c9e6b88c7' and 'second' are public verticals. self.assertEqual(7, len(verticals)) # noqa: PT009 self.assertEqual(course_key.make_usage_key('vertical', 'z'), verticals[0]) # noqa: PT009 - self.assertEqual(course_key.make_usage_key('vertical', '5a05be9d59fc4bb79282c94c9e6b88c7'), verticals[1]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(course_key.make_usage_key('vertical', '5a05be9d59fc4bb79282c94c9e6b88c7'), verticals[1]) # noqa: PT009 self.assertEqual(course_key.make_usage_key('vertical', 'a'), verticals[2]) # noqa: PT009 self.assertEqual(course_key.make_usage_key('vertical', 'second'), verticals[3]) # noqa: PT009 self.assertEqual(course_key.make_usage_key('vertical', 'b'), verticals[4]) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 29a194fd017e..503ca4be7b79 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -499,10 +499,10 @@ def test_library_filters(self): self._create_library(library="l3", display_name="Library-Title-3", org='org-test1') self._create_library(library="l4", display_name="Library-Title-4", org='org-test2') - self.assertEqual(len(self.client.get_json(LIBRARY_REST_URL).json()), 5) # 1 more from self.setUp() # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(len(self.client.get_json(LIBRARY_REST_URL).json()), 5) # 1 more from self.setUp() # noqa: E501, PT009 self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?org=org-test1').json()), 2) # noqa: PT009 self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?text_search=test-lib').json()), 2) # noqa: PT009 - self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?text_search=library-title').json()), 3) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?text_search=library-title').json()), 3) # noqa: PT009 self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?text_search=library-').json()), 3) # noqa: PT009 self.assertEqual(len(self.client.get_json(f'{LIBRARY_REST_URL}?text_search=org-test').json()), 3) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/tests/test_permissions.py b/cms/djangoapps/contentstore/tests/test_permissions.py index 86e77118426d..a82905521dbb 100644 --- a/cms/djangoapps/contentstore/tests/test_permissions.py +++ b/cms/djangoapps/contentstore/tests/test_permissions.py @@ -101,7 +101,7 @@ def test_get_all_users(self): user = users.pop() group.add_users(user) user_by_role[role].append(user) - self.assertTrue(auth.has_course_author_access(user, self.course_key), f"{user} does not have access") # lint-amnesty, pylint: disable=line-too-long # noqa: PT009 + self.assertTrue(auth.has_course_author_access(user, self.course_key), f"{user} does not have access") # noqa: PT009 course_team_url = reverse_course_url('course_team_handler', self.course_key) response = self.client.get_html(course_team_url) @@ -134,9 +134,9 @@ def test_get_all_users(self): if hasattr(user, '_roles'): del user._roles - self.assertTrue(auth.has_course_author_access(user, copy_course_key), f"{user} no copy access") # noqa: PT009 # pylint: disable=line-too-long + self.assertTrue(auth.has_course_author_access(user, copy_course_key), f"{user} no copy access") # noqa: PT009 if (role is OrgStaffRole) or (role is OrgInstructorRole): auth.remove_users(self.user, role(self.course_key.org), user) else: auth.remove_users(self.user, role(self.course_key), user) - self.assertFalse(auth.has_course_author_access(user, self.course_key), f"{user} remove didn't work") # lint-amnesty, pylint: disable=line-too-long # noqa: PT009 + self.assertFalse(auth.has_course_author_access(user, self.course_key), f"{user} remove didn't work") # noqa: PT009 diff --git a/cms/djangoapps/contentstore/tests/test_proctoring.py b/cms/djangoapps/contentstore/tests/test_proctoring.py index 83b46ca2a063..b4e936de33c7 100644 --- a/cms/djangoapps/contentstore/tests/test_proctoring.py +++ b/cms/djangoapps/contentstore/tests/test_proctoring.py @@ -70,7 +70,7 @@ def _verify_exam_data(self, sequence, expected_active): self.assertEqual(exam['exam_name'], sequence.display_name) # noqa: PT009 self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) # noqa: PT009 self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) # noqa: PT009 - self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam or sequence.is_onboarding_exam) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam or sequence.is_onboarding_exam) # noqa: PT009 self.assertEqual(exam['is_active'], expected_active) # noqa: PT009 self.assertEqual(exam['backend'], self.course.proctoring_provider) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index e1dd0b68d273..50fe216cff04 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -224,7 +224,7 @@ class RegisterExamsTaskTestCase(CourseTestCase): # pylint: disable=missing-clas @mock.patch('cms.djangoapps.contentstore.exams.register_exams') @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') - def test_exam_service_not_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): # noqa: PT019 # pylint: disable=line-too-long + def test_exam_service_not_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): # noqa: PT019 """ edx-proctoring interface is called if exam service is not enabled """ update_special_exams_and_publish(str(self.course.id)) _mock_register_exams_proctoring.assert_called_once_with(self.course.id) @@ -233,7 +233,7 @@ def test_exam_service_not_enabled_success(self, _mock_register_exams_proctoring, @mock.patch('cms.djangoapps.contentstore.exams.register_exams') @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') @override_waffle_flag(EXAMS_IDA, active=True) - def test_exam_service_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): # noqa: PT019 # pylint: disable=line-too-long + def test_exam_service_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): # noqa: PT019 """ exams service interface is called if exam service is enabled """ update_special_exams_and_publish(str(self.course.id)) _mock_register_exams_proctoring.assert_not_called() @@ -261,7 +261,7 @@ class CheckBrokenLinksTaskTest(ModuleStoreTestCase): """Tests for CheckBrokenLinksTask""" def setUp(self): super().setUp() - self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # lint-amnesty, pylint: disable=protected-access + self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # pylint: disable=protected-access self.test_course = CourseFactory.create( org="test", course="course1", display_name="run1" ) diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index 721bc59eb2d2..3a208a826759 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -361,7 +361,7 @@ def test_fail_bad_subs_filedata(self): with self.assertRaises(TranscriptsGenerationException) as cm: # noqa: PT027 transcripts_utils.generate_subs_from_source(youtube_subs, 'srt', srt_filedata, self.course) exception_message = str(cm.exception) - self.assertEqual(exception_message, "Something wrong with SubRip transcripts file during parsing.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(exception_message, "Something wrong with SubRip transcripts file during parsing.") # noqa: PT009 class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs): # lint-amnesty, pylint: disable=test-inherits-tests diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index 85e694f02e24..7c380f82bae6 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -44,7 +44,7 @@ def lms_link_test(self): course_key = CourseLocator('mitX', '101', 'test') location = course_key.make_usage_key('vertical', 'contacting_us') link = utils.get_lms_link_for_item(location, False) - self.assertEqual(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" # noqa: PT009 "@vertical+block@contacting_us") # test preview @@ -58,7 +58,7 @@ def lms_link_test(self): # now test with the course' location location = course_key.make_usage_key('course', 'test') link = utils.get_lms_link_for_item(location) - self.assertEqual(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" # noqa: PT009 "@course+block@test") def lms_link_for_certificate_web_view_test(self): @@ -159,7 +159,7 @@ def _test_visible_to_students(self, expected_visible_without_lock, name, start_d with and without visible_to_staff_only set. """ no_staff_lock = self._create_xblock_with_start_date(name, start_date, publish, visible_to_staff_only=False) - self.assertEqual(expected_visible_without_lock, utils.is_currently_visible_to_students(no_staff_lock)) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(expected_visible_without_lock, utils.is_currently_visible_to_students(no_staff_lock)) # noqa: PT009 # any xblock with visible_to_staff_only set to True should not be visible to students. staff_lock = self._create_xblock_with_start_date( diff --git a/cms/djangoapps/contentstore/tests/test_video_utils.py b/cms/djangoapps/contentstore/tests/test_video_utils.py index 1fb1ed76ccb7..9ab61db98d68 100644 --- a/cms/djangoapps/contentstore/tests/test_video_utils.py +++ b/cms/djangoapps/contentstore/tests/test_video_utils.py @@ -49,7 +49,7 @@ def test_corrupt_image_file(self): size=settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'] ) error = validate_video_image(uploaded_image_file) - self.assertEqual(error, 'There is a problem with this image file. Try to upload a different file.') # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(error, 'There is a problem with this image file. Try to upload a different file.') # noqa: PT009 @ddt.ddt @@ -214,7 +214,7 @@ def mocked_youtube_thumbnail_responses(resolutions): mocked_responses = [] for resolution in YOUTUBE_THUMBNAIL_SIZES: mocked_content = resolutions.get(resolution, '') - error_response = False if mocked_content else True # lint-amnesty, pylint: disable=simplifiable-if-expression + error_response = False if mocked_content else True # pylint: disable=simplifiable-if-expression mocked_responses.append(self.mocked_youtube_thumbnail_response(mocked_content, error_response)) return mocked_responses @@ -320,14 +320,14 @@ def test_scrape_youtube_thumbnail_logging( ( b'dummy-content', None, - 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 # pylint: disable=line-too-long + 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) ) ), ( None, None, - 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 # pylint: disable=line-too-long + 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) ) ), diff --git a/cms/djangoapps/contentstore/tests/test_xblock_handler_permissions.py b/cms/djangoapps/contentstore/tests/test_xblock_handler_permissions.py index cc7b53ca0991..065092636039 100644 --- a/cms/djangoapps/contentstore/tests/test_xblock_handler_permissions.py +++ b/cms/djangoapps/contentstore/tests/test_xblock_handler_permissions.py @@ -30,7 +30,7 @@ def test_get_block_fields_staff_allowed(self): self.assertEqual(self.client.get_json(f'/xblock/{self.html_block.location}').status_code, 200) # noqa: PT009 def test_get_block_fields_non_staff_forbidden(self): - self.assertEqual(self.non_staff_client.get_json(f'/xblock/{self.html_block.location}').status_code, 403) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(self.non_staff_client.get_json(f'/xblock/{self.html_block.location}').status_code, 403) # noqa: PT009 # --- POST /xblock/{blockId} metadata --- diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 30a030f966a9..400e56fffbdd 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -125,7 +125,7 @@ def use_new_pdf_editor(): # .. toggle_name: new_core_editors.use_video_gallery_flow # .. toggle_implementation: WaffleFlag # .. toggle_default: False -# .. toggle_description: This flag enables the use the video selection gallery on the flow of the new core video xblock editor +# .. toggle_description: This flag enables the use the video selection gallery on the flow of the new core video xblock editor # noqa: E501 # .. toggle_use_cases: temporary # .. toggle_creation_date: 2023-04-03 # .. toggle_target_removal_date: 2023-6-01 @@ -163,7 +163,7 @@ def individualize_anonymous_user_id(course_id): # .. toggle_name: contentstore.use_react_markdown_editor # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False -# .. toggle_description: This flag enables the use of the Markdown editor when creating or editing problems in the authoring MFE +# .. toggle_description: This flag enables the use of the Markdown editor when creating or editing problems in the authoring MFE # noqa: E501 # .. toggle_use_cases: opt_in # .. toggle_creation_date: 2025-4-11 # .. toggle_tickets: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/4517232656/Re-enable+Markdown+editing+of+CAPA+problems+to+meet+various+use+cases @@ -387,7 +387,7 @@ def use_new_group_configurations_page(course_key): # .. toggle_name: contentstore.mock_video_uploads # .. toggle_implementation: WaffleFlag # .. toggle_default: False -# .. toggle_description: This flag mocks contentstore video uploads for local development, if you don't have access to AWS +# .. toggle_description: This flag mocks contentstore video uploads for local development, if you don't have access to AWS # noqa: E501 # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2023-7-25 # .. toggle_tickets: TNL-10897 @@ -544,7 +544,7 @@ def use_legacy_logged_out_home(): # .. toggle_name: contentstore.enable_course_optimizer_check_prev_run_links # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False -# .. toggle_description: When enabled, allows the Course Optimizer to detect and update links pointing to previous course runs. +# .. toggle_description: When enabled, allows the Course Optimizer to detect and update links pointing to previous course runs. # noqa: E501 # This feature enables instructors to fix internal course links that still point to old course runs # after creating a course rerun. # .. toggle_use_cases: temporary diff --git a/cms/djangoapps/contentstore/transcript_storage_handlers.py b/cms/djangoapps/contentstore/transcript_storage_handlers.py index 293475232c95..f60055e59429 100644 --- a/cms/djangoapps/contentstore/transcript_storage_handlers.py +++ b/cms/djangoapps/contentstore/transcript_storage_handlers.py @@ -64,7 +64,7 @@ def validate_transcript_credentials(provider, **credentials): must_have_props = ['api_key', 'username'] missing = [ - must_have_prop for must_have_prop in must_have_props if must_have_prop not in list(credentials.keys()) # lint-amnesty, pylint: disable=consider-iterating-dictionary + must_have_prop for must_have_prop in must_have_props if must_have_prop not in list(credentials.keys()) # pylint: disable=consider-iterating-dictionary ] if missing: error_message = '{missing} must be specified.'.format(missing=' and '.join(missing)) @@ -233,7 +233,7 @@ def validate_transcript_upload_data(data, files): data['language_code'] != data['new_language_code'] and data['new_language_code'] in get_available_transcript_languages(video_id=data['edx_video_id']) ): - error = _('A transcript with the "{language_code}" language code already exists.'.format( # lint-amnesty, pylint: disable=translation-of-non-string + error = _('A transcript with the "{language_code}" language code already exists.'.format( # pylint: disable=translation-of-non-string language_code=data['new_language_code'] )) elif 'file' not in files: diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 35818125efab..9ab864ab114c 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -599,7 +599,7 @@ def is_currently_visible_to_students(xblock): return False # Check start date - if 'detached' not in published._class_tags and published.start is not None: # lint-amnesty, pylint: disable=protected-access + if 'detached' not in published._class_tags and published.start is not None: # pylint: disable=protected-access return datetime.now(UTC) > published.start # No start date, so it's always visible @@ -996,7 +996,7 @@ def get_subsections_in_section(): section_subsections = section.get_children() return section_subsections except AttributeError: - log.error("URL Retrieval Error: subsection {subsection} included in section {section}".format( # noqa: UP032 # pylint: disable=line-too-long + log.error("URL Retrieval Error: subsection {subsection} included in section {section}".format( # noqa: UP032 section=section.location, subsection=subsection.location )) @@ -1494,7 +1494,7 @@ def get_course_settings(request, course_key, course_block): # if 'minimum_grade_credit' of a course is not set or 0 then # show warning message to course author. - show_min_grade_warning = False if course_block.minimum_grade_credit > 0 else True # lint-amnesty, pylint: disable=simplifiable-if-expression + show_min_grade_warning = False if course_block.minimum_grade_credit > 0 else True # pylint: disable=simplifiable-if-expression settings_context.update( { 'is_credit_course': True, @@ -1950,7 +1950,7 @@ def _get_course_index_context(request, course_key, course_block): 'lms_link': lms_link, 'sections': sections, 'course_structure': course_structure, - 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, # lint-amnesty, pylint: disable=line-too-long + 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, 'initial_user_clipboard': user_clipboard, 'rerun_notification_id': current_action.id if current_action else None, 'course_release_date': course_release_date, diff --git a/cms/djangoapps/contentstore/video_utils.py b/cms/djangoapps/contentstore/video_utils.py index 44e216947117..85cf0cd16fe3 100644 --- a/cms/djangoapps/contentstore/video_utils.py +++ b/cms/djangoapps/contentstore/video_utils.py @@ -87,7 +87,7 @@ def download_youtube_video_thumbnail(youtube_id): thumbnail_content = thumbnail_content_type = None # Download highest resolution thumbnail available. for thumbnail_quality in YOUTUBE_THUMBNAIL_SIZES: - thumbnail_url = urljoin('https://img.youtube.com', '/vi/{youtube_id}/{thumbnail_quality}.jpg'.format( # noqa: UP032 # pylint: disable=line-too-long + thumbnail_url = urljoin('https://img.youtube.com', '/vi/{youtube_id}/{thumbnail_quality}.jpg'.format( # noqa: UP032 youtube_id=youtube_id, thumbnail_quality=thumbnail_quality )) response = requests.get(thumbnail_url) @@ -115,7 +115,7 @@ def validate_and_update_video_image(course_key_string, edx_video_id, image_file, update_video_image(edx_video_id, course_key_string, image_file, image_filename) LOGGER.info( - 'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string # lint-amnesty, pylint: disable=line-too-long + 'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string ) diff --git a/cms/djangoapps/contentstore/views/certificate_manager.py b/cms/djangoapps/contentstore/views/certificate_manager.py index dda1e172c503..b53c93565612 100644 --- a/cms/djangoapps/contentstore/views/certificate_manager.py +++ b/cms/djangoapps/contentstore/views/certificate_manager.py @@ -88,7 +88,7 @@ def parse(json_string): try: certificate = json.loads(json_string) except ValueError: - raise CertificateValidationError(_("invalid JSON")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise CertificateValidationError(_("invalid JSON")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 # Include the data contract version certificate["version"] = CERTIFICATE_SCHEMA_VERSION # Ensure a signatories list is always returned diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 9fa38665466f..a5f2125e71bc 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -350,7 +350,7 @@ def create_support_legend_dict(): templates_for_category.append( create_template_dict( - _(template['metadata'].get('display_name')), # lint-amnesty, pylint: disable=translation-of-non-string + _(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string category, support_level_with_template, template_id, diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 3d248944bdd5..4ad82865565b 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -1346,7 +1346,7 @@ def course_info_update_handler(request, course_key_string, provided_id=None): elif request.method == 'DELETE': try: return JsonResponse(delete_course_update(usage_key, request.json, provided_id, request.user)) - except: # lint-amnesty, pylint: disable=bare-except + except: # noqa: E722 return HttpResponseBadRequest( "Failed to delete", content_type="text/plain" @@ -1357,7 +1357,7 @@ def course_info_update_handler(request, course_key_string, provided_id=None): return JsonResponse(update_course_updates( usage_key, request.json, provided_id, request.user, request.method )) - except: # lint-amnesty, pylint: disable=bare-except + except: # noqa: E722 return HttpResponseBadRequest( "Failed to save", content_type="text/plain" @@ -1639,7 +1639,7 @@ def validate_textbook_json(textbook): try: textbook = json.loads(textbook) except ValueError: - raise TextbookValidationError("invalid JSON") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise TextbookValidationError("invalid JSON") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if not isinstance(textbook, dict): raise TextbookValidationError("must be JSON object") if not textbook.get("tab_title"): @@ -1923,7 +1923,7 @@ def group_configurations_detail_handler(request, course_key_string, group_config if request.method in ('POST', 'PUT'): # can be either and sometimes django is rewriting one to the other try: - new_configuration = GroupConfiguration(request.body, course, group_configuration_id).get_user_partition() # lint-amnesty, pylint: disable=line-too-long + new_configuration = GroupConfiguration(request.body, course, group_configuration_id).get_user_partition() # noqa: E501 except GroupConfigurationsValidationError as err: return JsonResponse({"error": str(err)}, status=400) diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index 5a9c23490ba6..ed0db0af62bb 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -20,7 +20,7 @@ def outer(func): def inner(request, *args, **kwargs): if request.headers.get('x-requested-with') == 'XMLHttpRequest': content = dump_js_escaped_json({"error": message}) - return HttpResponse(content, content_type="application/json", # lint-amnesty, pylint: disable=http-response-with-content-type-json + return HttpResponse(content, content_type="application/json", # pylint: disable=http-response-with-content-type-json status=status) else: return func(request, *args, **kwargs) diff --git a/cms/djangoapps/contentstore/views/organization.py b/cms/djangoapps/contentstore/views/organization.py index 7ece98201ac1..c9c9eaa60966 100644 --- a/cms/djangoapps/contentstore/views/organization.py +++ b/cms/djangoapps/contentstore/views/organization.py @@ -22,4 +22,4 @@ def get(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused """Returns organization list as json.""" organizations = get_organizations() org_names_list = [(org["short_name"]) for org in organizations] - return HttpResponse(dump_js_escaped_json(org_names_list), content_type='application/json; charset=utf-8') # lint-amnesty, pylint: disable=http-response-with-content-type-json + return HttpResponse(dump_js_escaped_json(org_names_list), content_type='application/json; charset=utf-8') # pylint: disable=http-response-with-content-type-json diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index d3ca840c01fd..45b0298601af 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -93,7 +93,7 @@ def preview_handler(request, usage_key_string, handler, suffix=''): return webob_to_django_response(resp) -def handler_url(block, handler_name, suffix='', query='', thirdparty=False): # lint-amnesty, pylint: disable=unused-argument +def handler_url(block, handler_name, suffix='', query='', thirdparty=False): # pylint: disable=unused-argument """ Handler URL function for Preview """ @@ -312,7 +312,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): is_reorderable = _is_xblock_reorderable(xblock, context) selected_groups_label = get_visibility_partition_info(xblock)['selected_groups_label'] if selected_groups_label: - selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label) # lint-amnesty, pylint: disable=line-too-long + selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label) # noqa: E501 course = modulestore().get_course(xblock.location.course_key) can_edit = context.get('can_edit', True) diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index 86a725bcfc78..ba0334d1caf4 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -179,7 +179,7 @@ def edit_tab_handler(course_item: CourseBlock, tabs_data: Dict, user: User): # raise NotImplementedError(f"Unsupported request to edit tab: {tabs_data}") -def get_tab_by_tab_id_locator(tab_list: List[CourseTab], tab_id_locator: Dict[str, str]) -> Optional[CourseTab]: # noqa: UP006, UP045 # pylint: disable=line-too-long +def get_tab_by_tab_id_locator(tab_list: List[CourseTab], tab_id_locator: Dict[str, str]) -> Optional[CourseTab]: # noqa: UP006, UP045 """ Look for a tab with the specified tab_id or locator. Returns the first matching tab. """ @@ -191,7 +191,7 @@ def get_tab_by_tab_id_locator(tab_list: List[CourseTab], tab_id_locator: Dict[st return tab -def get_tab_by_locator(tab_list: List[CourseTab], tab_location: Union[str, UsageKey]) -> Optional[CourseTab]: # noqa: UP006, UP007, UP045 # pylint: disable=line-too-long +def get_tab_by_locator(tab_list: List[CourseTab], tab_location: Union[str, UsageKey]) -> Optional[CourseTab]: # noqa: UP006, UP007, UP045 """ Look for a tab with the specified locator. Returns the first matching tab. """ diff --git a/cms/djangoapps/contentstore/views/tests/test_assets.py b/cms/djangoapps/contentstore/views/tests/test_assets.py index 4601f27f05f0..99825f6f24e5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_assets.py +++ b/cms/djangoapps/contentstore/views/tests/test_assets.py @@ -447,10 +447,10 @@ def test_basic(self): output["external_url"], "https://lms_root_url/asset-v1:org+class+run+type@asset+block@my_file_name.jpg" ) self.assertEqual(output["portable_url"], "/static/my_file_name.jpg") # noqa: PT009 - self.assertEqual(output["thumbnail"], "/asset-v1:org+class+run+type@thumbnail+block@my_file_name_thumb.jpg") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(output["thumbnail"], "/asset-v1:org+class+run+type@thumbnail+block@my_file_name_thumb.jpg") # noqa: PT009 self.assertEqual(output["id"], str(location)) # noqa: PT009 self.assertEqual(output['locked'], True) # noqa: PT009 - self.assertEqual(output['static_full_url'], '/asset-v1:org+class+run+type@asset+block@my_file_name.jpg') # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(output['static_full_url'], '/asset-v1:org+class+run+type@asset+block@my_file_name.jpg') # noqa: PT009 output = assets._get_asset_json("name", content_type, upload_date, location, None, False, course_key) self.assertIsNone(output["thumbnail"]) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index bb1206169189..1a75fcd25e6a 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -2212,7 +2212,7 @@ def _make_draft_content_different_from_published(self): published = modulestore().get_item( self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only, - ) # lint-amnesty, pylint: disable=line-too-long + ) # Update the draft version and check that published is different. self.client.ajax_post( @@ -2940,7 +2940,7 @@ def test_advanced_components(self): # Now fully disable done through XBlockConfiguration XBlockConfiguration.objects.create(name="done", enabled=False) self.templates = get_component_templates(self.course) - self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced")))) # noqa: PT009, UP034 # pylint: disable=line-too-long + self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced")))) # noqa: PT009, UP034 def test_deprecated_no_advance_component_button(self): """ @@ -4613,5 +4613,5 @@ def test_xblock_edit_view_contains_resources(self): resource_links = [link["href"] for link in soup.find_all("link", {"rel": "stylesheet"})] script_sources = [script["src"] for script in soup.find_all("script") if script.get("src")] - self.assertGreater(len(resource_links), 0, f"No CSS resources found in HTML. Found: {resource_links}") # noqa: PT009 # pylint: disable=line-too-long - self.assertGreater(len(script_sources), 0, f"No JS resources found in HTML. Found: {script_sources}") # noqa: PT009 # pylint: disable=line-too-long + self.assertGreater(len(resource_links), 0, f"No CSS resources found in HTML. Found: {resource_links}") # noqa: PT009 + self.assertGreater(len(script_sources), 0, f"No JS resources found in HTML. Found: {script_sources}") # noqa: PT009 diff --git a/cms/djangoapps/contentstore/views/tests/test_course_updates.py b/cms/djangoapps/contentstore/views/tests/test_course_updates.py index 36c4e45c56c7..9c2328b83dae 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_updates.py @@ -42,7 +42,7 @@ def get_response(content, date): return json.loads(resp.content.decode('utf-8')) - init_content = '' payload = get_response(content, 'January 8, 2013') self.assertHTMLEqual(payload['content'], content) @@ -172,13 +172,13 @@ def test_course_updates_compatibility(self): ) self.assertHTMLEqual(update_content, json.loads(resp.content.decode('utf-8'))['content']) course_updates = modulestore().get_item(location) - self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 1}]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 1}]) # noqa: PT009 # course_updates 'data' field should not update automatically self.assertEqual(course_updates.data, '') # noqa: PT009 # test delete course update item (soft delete) course_updates = modulestore().get_item(location) - self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 1}]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 1}]) # noqa: PT009 # now try to delete first update item resp = self.client.delete(course_update_url + '1') self.assertEqual(json.loads(resp.content.decode('utf-8')), []) # noqa: PT009 @@ -220,7 +220,7 @@ def test_no_ol_course_update(self): course_updates.data = 'bad news' modulestore().update_item(course_updates, self.user.id) - init_content = '' payload = {'content': content, 'date': 'January 8, 2013'} @@ -316,7 +316,7 @@ def test_course_update_id(self): self.assertHTMLEqual(update_content, json.loads(resp.content.decode('utf-8'))['content']) course_updates = modulestore().get_item(updates_location) del course_updates.items[0]["status"] - self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 2}]) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 2}]) # noqa: PT009 class CourseUpdateAuthzTest(CourseAuthzTestMixin, CourseTestCase): diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index 83c20f3c4fbb..d4745eb75187 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -282,7 +282,7 @@ def test_view_index_ok(self): # This creates a random UserPartition. self.course.user_partitions = [ - UserPartition(0, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]), # lint-amnesty, pylint: disable=line-too-long + UserPartition(0, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]), # noqa: E501 ] self.save_course() @@ -881,7 +881,7 @@ def test_can_get_correct_usage_info_for_split_test(self): self.store.update_item(self.course, ModuleStoreEnum.UserID.test) self.reload_course() - __, split_test, problem = self._create_content_experiment(cid=0, name_suffix='0', group_id=3, cid_for_problem=1) # lint-amnesty, pylint: disable=unused-variable + __, split_test, problem = self._create_content_experiment(cid=0, name_suffix='0', group_id=3, cid_for_problem=1) # pylint: disable=unused-variable expected = { 'id': 1, diff --git a/cms/djangoapps/contentstore/views/tests/test_helpers.py b/cms/djangoapps/contentstore/views/tests/test_helpers.py index 2f7f092b2f58..84b36c8a5af9 100644 --- a/cms/djangoapps/contentstore/views/tests/test_helpers.py +++ b/cms/djangoapps/contentstore/views/tests/test_helpers.py @@ -56,7 +56,7 @@ def test_xblock_studio_url(self): display_name="My Video") self.assertIsNone(xblock_studio_url(video)) # noqa: PT009 # Verify video URL with find_parent=True - self.assertEqual(xblock_studio_url(video, find_parent=True), f'/container/{child_vertical.location}') # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(xblock_studio_url(video, find_parent=True), f'/container/{child_vertical.location}') # noqa: PT009 # Verify library URL library = LibraryFactory.create() @@ -99,7 +99,7 @@ def test_xblock_embed_lms_url(self, mock_get_value: Mock): sequential = BlockFactory.create( parent_location=chapter.location, category='sequential', display_name="Lesson 1" ) - self.assertEqual(xblock_embed_lms_url(sequential), f"lms.example.com/xblock/{sequential.location}") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(xblock_embed_lms_url(sequential), f"lms.example.com/xblock/{sequential.location}") # noqa: PT009 def test_xblock_type_display_name(self): diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index b363faa57174..efad389b528b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -1186,7 +1186,7 @@ def assert_problem_display_names(self, source_course_location, dest_course_locat self.assertEqual(len(source_course_lib_children), len(dest_course_lib_children)) # noqa: PT009 - for source_child_location, dest_child_location in zip(source_course_lib_children, dest_course_lib_children): # noqa: B905 # pylint: disable=line-too-long + for source_child_location, dest_child_location in zip(source_course_lib_children, dest_course_lib_children): # noqa: B905 # Assert problem names on draft branch. with self.store.branch_setting(branch_setting=ModuleStoreEnum.Branch.draft_preferred): self.assert_names(source_child_location, dest_child_location) diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index 902f70637fb0..7fa9a5ed9b7e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -202,7 +202,7 @@ def create_transcript_file(self, content, suffix, include_bom=False): """ Setup a transcript file with suffix and content. """ - transcript_file = tempfile.NamedTemporaryFile(suffix=suffix) # lint-amnesty, pylint: disable=consider-using-with + transcript_file = tempfile.NamedTemporaryFile(suffix=suffix) # pylint: disable=consider-using-with wrapped_content = textwrap.dedent(content) if include_bom: wrapped_content = wrapped_content.encode('utf-8-sig') @@ -1044,7 +1044,7 @@ def test_fail_data_without_id(self): } resp = self.client.get(link, {'data': json.dumps(data)}) self.assertEqual(resp.status_code, 400) # noqa: PT009 - self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 def test_fail_data_with_bad_locator(self): # Test for raising `InvalidLocationError` exception. @@ -1059,7 +1059,7 @@ def test_fail_data_with_bad_locator(self): } resp = self.client.get(link, {'data': json.dumps(data)}) self.assertEqual(resp.status_code, 400) # noqa: PT009 - self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 # Test for raising `ItemNotFoundError` exception. data = { @@ -1072,7 +1072,7 @@ def test_fail_data_with_bad_locator(self): } resp = self.client.get(link, {'data': json.dumps(data)}) self.assertEqual(resp.status_code, 400) # noqa: PT009 - self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(json.loads(resp.content.decode('utf-8')).get('status'), "Can't find item by locator.") # noqa: PT009 def test_fail_for_non_video_block(self): # Not video block: setup diff --git a/cms/djangoapps/contentstore/views/tests/test_unit_page.py b/cms/djangoapps/contentstore/views/tests/test_unit_page.py index c79a54f52619..7e2365f3a5b1 100644 --- a/cms/djangoapps/contentstore/views/tests/test_unit_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_unit_page.py @@ -27,7 +27,7 @@ def test_public_component_preview_html(self): """ Verify that a public xblock's preview returns the expected HTML. """ - published_video = self.store.publish(self.video.location, self.user.id) # lint-amnesty, pylint: disable=unused-variable # noqa: F841 + published_video = self.store.publish(self.video.location, self.user.id) # pylint: disable=unused-variable # noqa: F841 self.validate_preview_html(self.video, STUDENT_VIEW, in_unit=True, can_add=False) def test_draft_component_preview_html(self): diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index 75948340bae0..1091a0107cf5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -1,1759 +1,1759 @@ -""" -Unit tests for video-related REST APIs. -""" - - -import csv -import json -import re -from contextlib import contextmanager -from datetime import datetime -from io import StringIO -from unittest.mock import Mock, patch - -import dateutil.parser -import ddt -import pytz -from django.conf import settings -from django.test import TestCase -from django.test.utils import override_settings -from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch -from edxval.api import ( - create_or_update_transcript_preferences, - create_or_update_video_transcript, - create_profile, - create_video, - get_course_video_image_url, - get_transcript_preferences, - get_video_info, -) - -from cms.djangoapps.contentstore.models import VideoUploadConfig -from cms.djangoapps.contentstore.tests.utils import CourseTestCase -from cms.djangoapps.contentstore.utils import reverse_course_url -from cms.djangoapps.contentstore.video_storage_handlers import ( - PUBLIC_VIDEO_SHARE, - StatusDisplayStrings, - TranscriptProvider, - convert_video_status, - storage_service_bucket, - storage_service_key, -) -from common.djangoapps.student.tests.factories import UserFactory -from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file -from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE, ENABLE_DEVSTACK_VIDEO_UPLOADS -from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order - -from ..videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_IMAGE_UPLOAD_ENABLED - - -def setup_s3_mocks(mock_boto3_resource, bucket_name='test-bucket'): - """ - Helper function to set up consistent boto3 S3 mocks. - - Args: - mock_boto3_resource: The patched boto3.resource mock - bucket_name: Name for the mock bucket (default: 'test-bucket') - - Returns: - tuple: (mock_s3_client, mock_bucket, mock_s3_resource) - """ - mock_s3_client = Mock() - mock_bucket = Mock() - mock_bucket.name = bucket_name - mock_bucket.meta.client = mock_s3_client - mock_s3_resource = Mock() - mock_s3_resource.Bucket.return_value = mock_bucket - mock_boto3_resource.return_value = mock_s3_resource - return mock_s3_client, mock_bucket, mock_s3_resource - - -class VideoUploadTestBase: - """ - Test cases for the video upload feature - """ - - def get_url_for_course_key(self, course_key, kwargs=None): - """Return video handler URL for the given course""" - return reverse_course_url(self.VIEW_NAME, course_key, kwargs) # lint-amnesty, pylint: disable=no-member - - def setUp(self): - super().setUp() # lint-amnesty, pylint: disable=no-member - self.url = self.get_url_for_course_key(self.course.id) - self.test_token = "test_token" - self.course.video_upload_pipeline = { - "course_video_upload_token": self.test_token, - } - self.save_course() # lint-amnesty, pylint: disable=no-member - - # create another course for videos belonging to multiple courses - self.course2 = CourseFactory.create() - self.course2.video_upload_pipeline = { - "course_video_upload_token": self.test_token, - } - self.course2.save() - self.store.update_item(self.course2, self.user.id) # lint-amnesty, pylint: disable=no-member - - # course ids for videos - course_ids = [str(self.course.id), str(self.course2.id)] - created = datetime.now(pytz.utc) - - self.profiles = ["profile1", "profile2"] - self.previous_uploads = [ - { - "edx_video_id": "test1", - "client_video_id": "test1.mp4", - "duration": 42.0, - "status": "upload", - "courses": course_ids, - "encoded_videos": [], - "created": created - }, - { - "edx_video_id": "test2", - "client_video_id": "test2.mp4", - "duration": 128.0, - "status": "file_complete", - "courses": course_ids, - "created": created, - "encoded_videos": [ - { - "profile": "profile1", - "url": "http://example.com/profile1/test2.mp4", - "file_size": 1600, - "bitrate": 100, - }, - { - "profile": "profile2", - "url": "http://example.com/profile2/test2.mov", - "file_size": 16000, - "bitrate": 1000, - }, - ], - }, - { - "edx_video_id": "non-ascii", - "client_video_id": "nón-ascii-näme.mp4", - "duration": 256.0, - "status": "transcode_active", - "courses": course_ids, - "created": created, - "encoded_videos": [ - { - "profile": "profile1", - "url": "http://example.com/profile1/nón-ascii-näme.mp4", - "file_size": 3200, - "bitrate": 100, - }, - ] - }, - ] - # Ensure every status string is tested - self.previous_uploads += [ - { - "edx_video_id": f"status_test_{status}", - "client_video_id": "status_test.mp4", - "duration": 3.14, - "status": status, - "courses": course_ids, - "created": created, - "encoded_videos": [], - } - for status in ( - list(StatusDisplayStrings._STATUS_MAP.keys()) + # pylint:disable=protected-access - ["non_existent_status"] - ) - ] - for profile in self.profiles: - create_profile(profile) - for video in self.previous_uploads: - create_video(video) - - def _get_previous_upload(self, edx_video_id): - """Returns the previous upload with the given video id.""" - return next( - video - for video in self.previous_uploads - if video["edx_video_id"] == edx_video_id - ) - - -class VideoStudioAccessTestsMixin: - """ - Base Access tests for studio video views - """ - def test_anon_user(self): - self.client.logout() - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) # noqa: PT009 - - def test_put(self): - response = self.client.put(self.url) - self.assertEqual(response.status_code, 405) # noqa: PT009 - - def test_invalid_course_key(self): - response = self.client.get( - self.get_url_for_course_key("Non/Existent/Course") - ) - self.assertEqual(response.status_code, 404) # noqa: PT009 - - def test_non_staff_user(self): - client, __ = self.create_non_staff_authed_user_client() - response = client.get(self.url) - self.assertEqual(response.status_code, 403) # noqa: PT009 - - -class VideoPipelineStudioAccessTestsMixin: - """ - Access tests for video views that rely on the video pipeline - """ - def test_video_pipeline_not_enabled(self): - settings.FEATURES["ENABLE_VIDEO_UPLOAD_PIPELINE"] = False - self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 - - def test_video_pipeline_not_configured(self): - settings.VIDEO_UPLOAD_PIPELINE = None - self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 - - def test_course_not_configured(self): - self.course.video_upload_pipeline = {} - self.save_course() - self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 - - -class VideoUploadPostTestsMixin: - """ - Shared test cases for video post tests. - """ - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - def test_post_success(self, mock_boto3_resource): - files = [ - { - 'file_name': 'first.mp4', - 'content_type': 'video/mp4', - }, - { - 'file_name': 'second.mp4', - 'content_type': 'video/mp4', - }, - { - 'file_name': 'third.mov', - 'content_type': 'video/quicktime', - }, - { - 'file_name': 'fourth.mp4', - 'content_type': 'video/mp4', - }, - ] - - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Mock generate_presigned_url to return different URLs for each file - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - response = self.client.post( - self.url, - json.dumps({'files': files}), - content_type='application/json' - ) - self.assertEqual(response.status_code, 200) # noqa: PT009 - response_obj = json.loads(response.content.decode('utf-8')) - - # Verify boto3 resource was called correctly - mock_boto3_resource.assert_called_once_with( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY - ) - - self.assertEqual(len(response_obj['files']), len(files)) # noqa: PT009 - self.assertEqual(mock_s3_client.generate_presigned_url.call_count, len(files)) # noqa: PT009 - - for i, file_info in enumerate(files): - # Get the call args for this file's presigned URL generation - call_args = mock_s3_client.generate_presigned_url.call_args_list[i] - args, kwargs = call_args - - # Verify the operation and params - self.assertEqual(args[0], 'put_object') # noqa: PT009 - self.assertEqual(kwargs['Params']['Bucket'], 'test-bucket') # noqa: PT009 - self.assertEqual(kwargs['Params']['ContentType'], file_info['content_type']) # noqa: PT009 - self.assertEqual(kwargs['ExpiresIn'], KEY_EXPIRATION_IN_SECONDS) # noqa: PT009 - - # Extract video_id from the key - key_name = kwargs['Params']['Key'] - path_match = re.match( - ( - settings.VIDEO_UPLOAD_PIPELINE['ROOT_PATH'] + - '/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$' - ), - key_name - ) - self.assertIsNotNone(path_match) # noqa: PT009 - video_id = path_match.group(1) - - # Verify metadata - metadata = kwargs['Params']['Metadata'] - self.assertEqual(metadata['course_video_upload_token'], self.test_token) # noqa: PT009 - self.assertEqual(metadata['client_video_id'], file_info['file_name']) # noqa: PT009 - self.assertEqual(metadata['course_key'], str(self.course.id)) # noqa: PT009 - - # Ensure VAL was updated - val_info = get_video_info(video_id) - self.assertEqual(val_info['status'], 'upload') # noqa: PT009 - self.assertEqual(val_info['client_video_id'], file_info['file_name']) # noqa: PT009 - self.assertEqual(val_info['duration'], 0) # noqa: PT009 - self.assertEqual(val_info['courses'], [{str(self.course.id): None}]) # noqa: PT009 - - # Ensure response is correct - response_file = response_obj['files'][i] - self.assertEqual(response_file['file_name'], file_info['file_name']) # noqa: PT009 - self.assertEqual(response_file['upload_url'], f'http://example.com/url_{file_info["file_name"]}') # noqa: PT009 # pylint: disable=line-too-long - - def test_post_non_json(self): - response = self.client.post(self.url, {"files": []}) - self.assertEqual(response.status_code, 400) # noqa: PT009 - - def test_post_malformed_json(self): - response = self.client.post(self.url, "{", content_type="application/json") - self.assertEqual(response.status_code, 400) # noqa: PT009 - - def test_post_invalid_json(self): - def assert_bad(content): - """Make request with content and assert that response is 400""" - response = self.client.post( - self.url, - json.dumps(content), - content_type="application/json" - ) - self.assertEqual(response.status_code, 400) # noqa: PT009 - - # Top level missing files key - assert_bad({}) - - # Entry missing file_name - assert_bad({"files": [{"content_type": "video/mp4"}]}) - - # Entry missing content_type - assert_bad({"files": [{"file_name": "test.mp4"}]}) - - -@ddt.ddt -@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) -@override_settings(VIDEO_UPLOAD_PIPELINE={ - "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" -}) -class VideosHandlerTestCase( - VideoUploadTestBase, - VideoStudioAccessTestsMixin, - VideoPipelineStudioAccessTestsMixin, - VideoUploadPostTestsMixin, - CourseTestCase -): - """Test cases for the main video upload endpoint""" - - VIEW_NAME = 'videos_handler' - - def test_get_json(self): - response = self.client.get_json(self.url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - response_videos = json.loads(response.content.decode('utf-8'))['videos'] - self.assertEqual(len(response_videos), len(self.previous_uploads)) # noqa: PT009 - for i, response_video in enumerate(response_videos): - # Videos should be returned by creation date descending - original_video = self.previous_uploads[-(i + 1)] - print(response_video.keys()) - self.assertEqual( # noqa: PT009 - set(response_video.keys()), - { - 'edx_video_id', - 'client_video_id', - 'created', - 'duration', - 'status', - 'status_nontranslated', - 'course_video_image_url', - 'file_size', - 'download_link', - 'transcripts', - 'transcription_status', - 'transcript_urls', - 'error_description', - } - ) - dateutil.parser.parse(response_video['created']) - for field in ['edx_video_id', 'client_video_id', 'duration']: - self.assertEqual(response_video[field], original_video[field]) # noqa: PT009 - self.assertEqual( # noqa: PT009 - response_video['status'], - convert_video_status(original_video) - ) - - @ddt.data( - ( - [ - 'edx_video_id', 'client_video_id', 'created', 'duration', - 'status', 'status_nontranslated', 'course_video_image_url', 'file_size', - 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', - 'error_description' - ], - [ - { - 'video_id': 'test1', - 'language_code': 'en', - 'file_name': 'edx101.srt', - 'file_format': 'srt', - 'provider': 'Cielo24' - } - ], - ['en'] - ), - ( - [ - 'edx_video_id', 'client_video_id', 'created', 'duration', - 'status', 'status_nontranslated', 'course_video_image_url', 'file_size', - 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', - 'error_description' - ], - [ - { - 'video_id': 'test1', - 'language_code': 'en', - 'file_name': 'edx101_en.srt', - 'file_format': 'srt', - 'provider': 'Cielo24' - }, - { - 'video_id': 'test1', - 'language_code': 'es', - 'file_name': 'edx101_es.srt', - 'file_format': 'srt', - 'provider': 'Cielo24' - } - ], - ['en', 'es'] - ) - ) - @ddt.unpack - def test_get_json_transcripts(self, expected_video_keys, uploaded_transcripts, expected_transcripts): - """ - Test that transcripts are attached based on whether the video transcript feature is enabled. - """ - for transcript in uploaded_transcripts: - create_or_update_video_transcript( - transcript['video_id'], - transcript['language_code'], - metadata={ - 'file_name': transcript['file_name'], - 'file_format': transcript['file_format'], - 'provider': transcript['provider'] - } - ) - - response = self.client.get_json(self.url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - response_videos = json.loads(response.content.decode('utf-8'))['videos'] - self.assertEqual(len(response_videos), len(self.previous_uploads)) # noqa: PT009 - for response_video in response_videos: - print(response_video) - - self.assertEqual(set(response_video.keys()), set(expected_video_keys)) # noqa: PT009 - if response_video['edx_video_id'] == self.previous_uploads[0]['edx_video_id']: - self.assertEqual(response_video.get('transcripts', []), expected_transcripts) # noqa: PT009 - - def test_get_redirects_to_video_uploads_url(self): - """ - Test that GET requests redirect to the MFE video uploads page. - """ - from cms.djangoapps.contentstore.utils import get_video_uploads_url - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) # noqa: PT009 - self.assertEqual(response.url, get_video_uploads_url(self.course.id)) # noqa: PT009 - - @override_settings(AWS_ACCESS_KEY_ID="test_key_id", AWS_SECRET_ACCESS_KEY="test_secret") - @patch("cms.djangoapps.contentstore.video_storage_handlers.boto3.resource") - @ddt.data( - ( - [ - { - "file_name": "supported-1.mp4", - "content_type": "video/mp4", - }, - { - "file_name": "supported-2.mov", - "content_type": "video/quicktime", - }, - ], - 200 - ), - ( - [ - { - "file_name": "unsupported-1.txt", - "content_type": "text/plain", - }, - { - "file_name": "unsupported-2.png", - "content_type": "image/png", - }, - ], - 400 - ) - ) - @ddt.unpack - def test_video_supported_file_formats(self, files, expected_status, mock_boto3_resource): - """ - Test that video upload works correctly against supported and unsupported file formats. - """ - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Mock generate_presigned_url to return different URLs for each file - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - # Check supported formats - response = self.client.post( - self.url, - json.dumps({"files": files}), - content_type="application/json" - ) - self.assertEqual(response.status_code, expected_status) # noqa: PT009 - response = json.loads(response.content.decode('utf-8')) - - if expected_status == 200: - self.assertNotIn('error', response) # noqa: PT009 - else: - self.assertIn('error', response) # noqa: PT009 - self.assertEqual(response['error'], "Request 'files' entry contain unsupported content_type") # noqa: PT009 - - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - def test_upload_with_non_ascii_charaters(self, mock_boto3_resource): - """ - Test that video uploads throws error message when file name contains special characters. - """ - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - file_name = 'test\u2019_file.mp4' - files = [{'file_name': file_name, 'content_type': 'video/mp4'}] - - response = self.client.post( - self.url, - json.dumps({'files': files}), - content_type='application/json' - ) - self.assertEqual(response.status_code, 400) # noqa: PT009 - response = json.loads(response.content.decode('utf-8')) - self.assertEqual(response['error'], 'The file name for %s must contain only ASCII characters.' % file_name) # noqa: PT009, UP031 # pylint: disable=line-too-long - - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret', AWS_SECURITY_TOKEN='token') - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - @override_waffle_flag(ENABLE_DEVSTACK_VIDEO_UPLOADS, active=True) - def test_devstack_upload_connection(self, mock_boto3_resource): - files = [{'file_name': 'first.mp4', 'content_type': 'video/mp4'}] - - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Mock generate_presigned_url to return different URLs for each file - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - response = self.client.post( - self.url, - json.dumps({'files': files}), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) # noqa: PT009 - mock_boto3_resource.assert_called_once_with( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - security_token=settings.AWS_SECURITY_TOKEN - ) - - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - def test_send_course_to_vem_pipeline(self, mock_boto3_resource): - """ - Test that uploads always go to VEM S3 bucket by default. - """ - files = [{'file_name': 'first.mp4', 'content_type': 'video/mp4'}] - - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Mock generate_presigned_url to return different URLs for each file - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - response = self.client.post( - self.url, - json.dumps({'files': files}), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 200) # noqa: PT009 - mock_s3_resource.Bucket.assert_called_once_with( - settings.VIDEO_UPLOAD_PIPELINE['VEM_S3_BUCKET'] # pylint: disable=unsubscriptable-object - ) - - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - @ddt.data( - { - 'global_waffle': True, - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, - 'expect_token': True - }, - { - 'global_waffle': False, - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, - 'expect_token': False - }, - { - 'global_waffle': False, - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, - 'expect_token': True - } - ) - def test_video_upload_token_in_meta(self, data, mock_boto3_resource): - """ - Test video upload token in s3 metadata. - """ - @contextmanager - def proxy_manager(manager, ignore_manager): - """ - This acts as proxy to the original manager in the arguments given - the original manager is not set to be ignored. - """ - if ignore_manager: - yield - else: - with manager: - yield - - file_data = { - 'file_name': 'first.mp4', - 'content_type': 'video/mp4', - } - - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Track generate_presigned_url calls to inspect metadata - presigned_url_calls = [] - - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - presigned_url_calls.append((operation, Params, ExpiresIn)) - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']): - with override_waffle_flag(DEPRECATE_YOUTUBE, active=data['global_waffle']): - response = self.client.post( - self.url, - json.dumps({'files': [file_data]}), - content_type='application/json' - ) - self.assertEqual(response.status_code, 200) # noqa: PT009 - - # Check if course_video_upload_token is in metadata based on expectation - if data['expect_token']: - # We should find the token in the metadata - self.assertEqual(len(presigned_url_calls), 1) # noqa: PT009 - metadata = presigned_url_calls[0][1]['Metadata'] - self.assertIn('course_video_upload_token', metadata) # noqa: PT009 - self.assertEqual(metadata['course_video_upload_token'], self.test_token) # noqa: PT009 - else: - # If we don't expect a token, verify it's not in metadata - if presigned_url_calls: - metadata = presigned_url_calls[0][1]['Metadata'] - self.assertNotIn('course_video_upload_token', metadata) # noqa: PT009 - - def _assert_video_removal(self, url, edx_video_id, deleted_videos): - """ - Verify that if correct video is removed from a particular course. - - Arguments: - url (str): URL to get uploaded videos - edx_video_id (str): video id - deleted_videos (int): how many videos are deleted - """ - response = self.client.get_json(url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - response_videos = json.loads(response.content.decode('utf-8'))["videos"] - self.assertEqual(len(response_videos), len(self.previous_uploads) - deleted_videos) # noqa: PT009 - - if deleted_videos: - self.assertNotIn(edx_video_id, [video.get('edx_video_id') for video in response_videos]) # noqa: PT009 - else: - self.assertIn(edx_video_id, [video.get('edx_video_id') for video in response_videos]) # noqa: PT009 - - def test_video_removal(self): - """ - Verifies that video removal is working as expected. - """ - edx_video_id = 'test1' - remove_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) - response = self.client.delete(remove_url, HTTP_ACCEPT="application/json") - self.assertEqual(response.status_code, 204) # noqa: PT009 - - self._assert_video_removal(self.url, edx_video_id, 1) - - def test_video_removal_multiple_courses(self): - """ - Verifies that video removal is working as expected for multiple courses. - - If a video is used by multiple courses then removal from one course shouldn't effect the other course. - """ - # remove video from course1 - edx_video_id = 'test1' - remove_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) - response = self.client.delete(remove_url, HTTP_ACCEPT="application/json") - self.assertEqual(response.status_code, 204) # noqa: PT009 - - # verify that video is only deleted from course1 only - self._assert_video_removal(self.url, edx_video_id, 1) - self._assert_video_removal(self.get_url_for_course_key(self.course2.id), edx_video_id, 0) - - def test_convert_video_status(self): - """ - Verifies that convert_video_status works as expected. - """ - video = self.previous_uploads[0] - - # video status should be failed if it's in upload state for more than 24 hours - video['created'] = datetime(2016, 1, 1, 10, 10, 10, 0, pytz.UTC) - status = convert_video_status(video) - self.assertEqual(status, StatusDisplayStrings.get('upload_failed')) # noqa: PT009 - - # `invalid_token` should be converted to `youtube_duplicate` - video['created'] = datetime.now(pytz.UTC) - video['status'] = 'invalid_token' - status = convert_video_status(video) - self.assertEqual(status, StatusDisplayStrings.get('youtube_duplicate')) # noqa: PT009 - - # The "encode status" should be converted to `file_complete` if video encodes are complete - video['status'] = 'transcription_in_progress' - status = convert_video_status(video, is_video_encodes_ready=True) - self.assertEqual(status, StatusDisplayStrings.get('file_complete')) # noqa: PT009 - - # If encoding is not complete return the status as it is - video['status'] = 's3_upload_failed' - status = convert_video_status(video) - self.assertEqual(status, StatusDisplayStrings.get('s3_upload_failed')) # noqa: PT009 - - # for all other status, there should not be any conversion - statuses = list(StatusDisplayStrings._STATUS_MAP.keys()) # pylint: disable=protected-access - statuses.remove('invalid_token') - for status in statuses: - video['status'] = status - new_status = convert_video_status(video) - self.assertEqual(new_status, StatusDisplayStrings.get(status)) # noqa: PT009 - - def assert_video_status(self, url, edx_video_id, status): - """ - Verifies that video with `edx_video_id` has `status` - """ - response = self.client.get_json(url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - videos = json.loads(response.content.decode('utf-8'))["videos"] - for video in videos: - if video['edx_video_id'] == edx_video_id: - return self.assertEqual(video['status'], status) # noqa: PT009 - - # Test should fail if video not found - self.assertEqual(True, False, 'Invalid edx_video_id') # noqa: PT009 - - @patch('cms.djangoapps.contentstore.video_storage_handlers.LOGGER') - def test_video_status_update_request(self, mock_logger): - """ - Verifies that video status update request works as expected. - """ - url = self.get_url_for_course_key(self.course.id) - edx_video_id = 'test1' - self.assert_video_status(url, edx_video_id, 'Uploading') - - response = self.client.post( - url, - json.dumps([{ - 'edxVideoId': edx_video_id, - 'status': 'upload_failed', - 'message': 'server down' - }]), - content_type="application/json" - ) - - mock_logger.info.assert_called_with( - 'VIDEOS: Video status update with id [%s], status [%s] and message [%s]', - edx_video_id, - 'upload_failed', - 'server down' - ) - - self.assertEqual(response.status_code, 204) # noqa: PT009 - - self.assert_video_status(url, edx_video_id, 'Failed') - - @ddt.data( - ('test_video_token', "Transcription in Progress"), - ('', "Ready"), - ) - @ddt.unpack - def test_video_transcript_status_conversion(self, course_video_upload_token, expected_video_status_text): - """ - Verifies that video status `transcription_in_progress` gets converted - correctly into the `file_complete` for the new video workflow and - stays as it is, for the old video workflow. - """ - self.course.video_upload_pipeline = { - 'course_video_upload_token': course_video_upload_token - } - self.save_course() - - url = self.get_url_for_course_key(self.course.id) - edx_video_id = 'test1' - self.assert_video_status(url, edx_video_id, 'Uploading') - - response = self.client.post( - url, - json.dumps([{ - 'edxVideoId': edx_video_id, - 'status': 'transcription_in_progress', - 'message': 'Transcription is in progress' - }]), - content_type="application/json" - ) - self.assertEqual(response.status_code, 204) # noqa: PT009 - - self.assert_video_status(url, edx_video_id, expected_video_status_text) - - -@ddt.ddt -@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) -@override_settings(VIDEO_UPLOAD_PIPELINE={ - "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" -}) -class GenerateVideoUploadLinkTestCase( - VideoUploadTestBase, - VideoUploadPostTestsMixin, - CourseTestCase -): - """ - Test cases for the main video upload endpoint - """ - - VIEW_NAME = 'generate_video_upload_link' - - def test_unsupported_requests_fail(self): - """ - The API only supports post, make sure other requests fail - """ - response = self.client.get(self.url) - self.assertEqual(response.status_code, 405) # noqa: PT009 - - response = self.client.put(self.url) - self.assertEqual(response.status_code, 405) # noqa: PT009 - - response = self.client.patch(self.url) - self.assertEqual(response.status_code, 405) # noqa: PT009 - - -@ddt.ddt -@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True}) -@override_settings(VIDEO_UPLOAD_PIPELINE={'BUCKET': 'test_bucket', 'ROOT_PATH': 'test_root'}) -class VideoImageTestCase(VideoUploadTestBase, CourseTestCase): - """ - Tests for video image. - """ - - VIEW_NAME = "video_images_handler" - - def verify_image_upload_reponse(self, course_id, edx_video_id, upload_response): - """ - Verify that image is uploaded successfully. - - Arguments: - course_id: ID of course - edx_video_id: ID of video - upload_response: Upload response object - - Returns: - uploaded image url - """ - self.assertEqual(upload_response.status_code, 200) # noqa: PT009 - response = json.loads(upload_response.content.decode('utf-8')) - val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id) - self.assertEqual(response['image_url'], val_image_url) # noqa: PT009 - - return val_image_url - - def verify_error_message(self, response, error_message): - """ - Verify that image upload failure gets proper error message. - - Arguments: - response: Response object. - error_message: Expected error message. - """ - self.assertEqual(response.status_code, 400) # noqa: PT009 - response = json.loads(response.content.decode('utf-8')) - self.assertIn('error', response) # noqa: PT009 - self.assertEqual(response['error'], error_message) # noqa: PT009 - - @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, False) - def test_video_image_upload_disabled(self): - """ - Tests the video image upload when the feature is disabled. - """ - video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': 'test_vid_id'}) - response = self.client.post(video_image_upload_url, {'file': 'dummy_file'}, format='multipart') - self.assertEqual(response.status_code, 404) # noqa: PT009 - - @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) - def test_video_image(self): - """ - Test video image is saved. - """ - edx_video_id = 'test1' - video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) - with make_image_file( - dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), - ) as image_file: - response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') - image_url1 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) - - # upload again to verify that new image is uploaded successfully - with make_image_file( - dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), - ) as image_file: - response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') - image_url2 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) - - self.assertNotEqual(image_url1, image_url2) # noqa: PT009 - - @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) - def test_video_image_no_file(self): - """ - Test that an error error message is returned if upload request is incorrect. - """ - video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': 'test1'}) - response = self.client.post(video_image_upload_url, {}) - self.verify_error_message(response, 'An image file is required.') - - @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) - def test_no_video_image(self): - """ - Test image url is set to None if no video image. - """ - edx_video_id = 'test1' - get_videos_url = reverse_course_url('videos_handler', self.course.id) - video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) - with make_image_file( - dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), - ) as image_file: - self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') - - val_image_url = get_course_video_image_url(course_id=self.course.id, edx_video_id=edx_video_id) - - response = self.client.get_json(get_videos_url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - response_videos = json.loads(response.content.decode('utf-8'))["videos"] - for response_video in response_videos: - if response_video['edx_video_id'] == edx_video_id: - self.assertEqual(response_video['course_video_image_url'], val_image_url) # noqa: PT009 - else: - self.assertEqual(response_video['course_video_image_url'], None) # noqa: PT009 - - @ddt.data( - # Image file type validation - ( - { - 'extension': '.png' - }, - None - ), - ( - { - 'extension': '.gif' - }, - None - ), - ( - { - 'extension': '.bmp' - }, - None - ), - ( - { - 'extension': '.jpg' - }, - None - ), - ( - { - 'extension': '.jpeg' - }, - None - ), - ( - { - 'extension': '.PNG' - }, - None - ), - ( - { - 'extension': '.tiff' - }, - 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 # pylint: disable=line-too-long - supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) - ) - ), - # Image file size validation - ( - { - 'size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'] + 10 - }, - 'This image file must be smaller than {image_max_size}.'.format( # noqa: UP032 - image_max_size=settings.VIDEO_IMAGE_MAX_FILE_SIZE_MB - ) - ), - ( - { - 'size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'] - 10 - }, - 'This image file must be larger than {image_min_size}.'.format( # noqa: UP032 - image_min_size=settings.VIDEO_IMAGE_MIN_FILE_SIZE_KB - ) - ), - # Image file minimum width / height - ( - { - 'width': 16, # 16x9 - 'height': 9 - }, - 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. The minimum resolution is {image_file_min_width}x{image_file_min_height}.'.format( # lint-amnesty, pylint: disable=line-too-long # noqa: UP032 - image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, - image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, - image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, - image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT - ) - ), - ( - { - 'width': settings.VIDEO_IMAGE_MIN_WIDTH - 10, - 'height': settings.VIDEO_IMAGE_MIN_HEIGHT - }, - 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. The minimum resolution is {image_file_min_width}x{image_file_min_height}.'.format( # lint-amnesty, pylint: disable=line-too-long # noqa: UP032 - image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, - image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, - image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, - image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT - ) - ), - ( - { - 'width': settings.VIDEO_IMAGE_MIN_WIDTH, - 'height': settings.VIDEO_IMAGE_MIN_HEIGHT - 10 - }, - ( # noqa: UP032 - 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. ' - 'The minimum resolution is {image_file_min_width}x{image_file_min_height}.' - ).format( - image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, - image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, - image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, - image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT - ) - ), - ( - { - 'width': 1200, # not 16:9, but width/height check first. - 'height': 100 - }, - ( # noqa: UP032 - 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. ' - 'The minimum resolution is {image_file_min_width}x{image_file_min_height}.' - ).format( - image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, - image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, - image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, - image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT - ) - ), - # Image file aspect ratio validation - ( - { - 'width': settings.VIDEO_IMAGE_MAX_WIDTH, # 1280x720 - 'height': settings.VIDEO_IMAGE_MAX_HEIGHT - }, - None - ), - ( - { - 'width': 850, # 16:9 - 'height': 478 - }, - None - ), - ( - { - 'width': 940, # 1.67 ratio, applicable aspect ratio margin of .01 - 'height': 560 - }, - None - ), - ( - { - 'width': settings.VIDEO_IMAGE_MIN_WIDTH + 100, - 'height': settings.VIDEO_IMAGE_MIN_HEIGHT + 200 - }, - 'This image file must have an aspect ratio of {video_image_aspect_ratio_text}.'.format( # noqa: UP032 - video_image_aspect_ratio_text=settings.VIDEO_IMAGE_ASPECT_RATIO_TEXT - ) - ), - # Image file name validation - ( - { - 'prefix': 'nøn-åßç¡¡' - }, - 'The image file name can only contain letters, numbers, hyphens (-), and underscores (_).' - ) - ) - @ddt.unpack - @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) - def test_video_image_validation_message(self, image_data, error_message): - """ - Test video image validation gives proper error message. - - Arguments: - image_data (Dict): Specific data to create image file. - error_message (String): Error message - """ - edx_video_id = 'test1' - video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) - with make_image_file( - dimensions=( - image_data.get('width', settings.VIDEO_IMAGE_MIN_WIDTH), - image_data.get('height', settings.VIDEO_IMAGE_MIN_HEIGHT) - ), - prefix=image_data.get('prefix', 'videoimage'), - extension=image_data.get('extension', '.png'), - force_size=image_data.get('size', settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES']) - ) as image_file: - response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') - if error_message: - self.verify_error_message(response, error_message) - else: - self.verify_image_upload_reponse(self.course.id, edx_video_id, response) - - -@ddt.ddt -@patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', - Mock(return_value=True) -) -@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True}) -class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase): - """ - Tests for video transcripts preferences. - """ - - VIEW_NAME = 'transcript_preferences_handler' - - def test_405_with_not_allowed_request_method(self): - """ - Verify that 405 is returned in case of not-allowed request methods. - Allowed request methods are POST and DELETE. - """ - video_transcript_url = self.get_url_for_course_key(self.course.id) - response = self.client.get( - video_transcript_url, - content_type='application/json' - ) - self.assertEqual(response.status_code, 405) # noqa: PT009 - - @ddt.data( - # Video transcript feature disabled - ( - {}, - False, - '', - 404, - ), - # Error cases - ( - {}, - True, - "Invalid provider None.", - 400 - ), - ( - { - 'provider': '' - }, - True, - "Invalid provider .", - 400 - ), - ( - { - 'provider': 'dummy-provider' - }, - True, - "Invalid provider dummy-provider.", - 400 - ), - ( - { - 'provider': TranscriptProvider.CIELO24 - }, - True, - "Invalid cielo24 fidelity None.", - 400 - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - }, - True, - "Invalid cielo24 turnaround None.", - 400 - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - 'cielo24_turnaround': 'STANDARD', - 'video_source_language': 'en' - }, - True, - "Invalid languages [].", - 400 - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PREMIUM', - 'cielo24_turnaround': 'STANDARD', - 'video_source_language': 'es' - }, - True, - "Unsupported source language es.", - 400 - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - 'cielo24_turnaround': 'STANDARD', - 'video_source_language': 'en', - 'preferred_languages': ['es', 'ur'] - }, - True, - "Invalid languages ['es', 'ur'].", - 400 - ), - ( - { - 'provider': TranscriptProvider.THREE_PLAY_MEDIA - }, - True, - "Invalid 3play turnaround None.", - 400 - ), - ( - { - 'provider': TranscriptProvider.THREE_PLAY_MEDIA, - 'three_play_turnaround': 'standard', - 'video_source_language': 'zh', - }, - True, - "Unsupported source language zh.", - 400 - ), - ( - { - 'provider': TranscriptProvider.THREE_PLAY_MEDIA, - 'three_play_turnaround': 'standard', - 'video_source_language': 'es', - 'preferred_languages': ['es', 'ur'] - }, - True, - "Invalid languages ['es', 'ur'].", - 400 - ), - ( - { - 'provider': TranscriptProvider.THREE_PLAY_MEDIA, - 'three_play_turnaround': 'standard', - 'video_source_language': 'en', - 'preferred_languages': ['es', 'ur'] - }, - True, - "Invalid languages ['es', 'ur'].", - 400 - ), - # Success - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - 'cielo24_turnaround': 'STANDARD', - 'video_source_language': 'es', - 'preferred_languages': ['en'] - }, - True, - '', - 200 - ), - ( - { - 'provider': TranscriptProvider.THREE_PLAY_MEDIA, - 'three_play_turnaround': 'standard', - 'preferred_languages': ['en'], - 'video_source_language': 'en', - }, - True, - '', - 200 - ) - ) - @ddt.unpack - def test_video_transcript(self, preferences, is_video_transcript_enabled, error_message, expected_status_code): - """ - Tests that transcript handler works correctly. - """ - video_transcript_url = self.get_url_for_course_key(self.course.id) - preferences_data = { - 'provider': preferences.get('provider'), - 'cielo24_fidelity': preferences.get('cielo24_fidelity'), - 'cielo24_turnaround': preferences.get('cielo24_turnaround'), - 'three_play_turnaround': preferences.get('three_play_turnaround'), - 'preferred_languages': preferences.get('preferred_languages', []), - 'video_source_language': preferences.get('video_source_language'), - } - - with patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled' - ) as video_transcript_feature: - video_transcript_feature.return_value = is_video_transcript_enabled - response = self.client.post( - video_transcript_url, - json.dumps(preferences_data), - content_type='application/json' - ) - status_code = response.status_code - response = json.loads(response.content.decode('utf-8')) if is_video_transcript_enabled else response - - self.assertEqual(status_code, expected_status_code) # noqa: PT009 - self.assertEqual(response.get('error', ''), error_message) # noqa: PT009 - - # Remove modified and course_id fields from the response so as to check the expected transcript preferences. - response.get('transcript_preferences', {}).pop('modified', None) - response.get('transcript_preferences', {}).pop('course_id', None) - expected_preferences = preferences_data if is_video_transcript_enabled and not error_message else {} - self.assertDictEqual(response.get('transcript_preferences', {}), expected_preferences) # noqa: PT009 - - def test_remove_transcript_preferences(self): - """ - Test that transcript handler removes transcript preferences correctly. - """ - # First add course wide transcript preferences. - preferences = create_or_update_transcript_preferences(str(self.course.id)) - - # Verify transcript preferences exist - self.assertIsNotNone(preferences) # noqa: PT009 - - response = self.client.delete( - self.get_url_for_course_key(self.course.id), - content_type='application/json' - ) - - self.assertEqual(response.status_code, 204) # noqa: PT009 - - # Verify transcript preferences no loger exist - preferences = get_transcript_preferences(str(self.course.id)) - self.assertIsNone(preferences) # noqa: PT009 - - def test_remove_transcript_preferences_not_found(self): - """ - Test that transcript handler works correctly even when no preferences are found. - """ - course_id = 'course-v1:dummy+course+id' - # Verify transcript preferences do not exist - preferences = get_transcript_preferences(course_id) - self.assertIsNone(preferences) # noqa: PT009 - - response = self.client.delete( - self.get_url_for_course_key(course_id), - content_type='application/json' - ) - self.assertEqual(response.status_code, 204) # noqa: PT009 - - # Verify transcript preferences do not exist - preferences = get_transcript_preferences(course_id) - self.assertIsNone(preferences) # noqa: PT009 - - @ddt.data( - ( - None, - False - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - 'cielo24_turnaround': 'STANDARD', - 'preferred_languages': ['en'] - }, - False - ), - ( - { - 'provider': TranscriptProvider.CIELO24, - 'cielo24_fidelity': 'PROFESSIONAL', - 'cielo24_turnaround': 'STANDARD', - 'preferred_languages': ['en'] - }, - True - ) - ) - @ddt.unpack - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') - @patch('cms.djangoapps.contentstore.video_storage_handlers.get_transcript_preferences') - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - def test_transcript_preferences_metadata(self, transcript_preferences, is_video_transcript_enabled, - mock_boto3_resource, mock_transcript_preferences): - """ - Tests that transcript preference metadata is only set if it is video transcript feature is enabled and - transcript preferences are already stored in the system. - """ - file_name = 'test-video.mp4' - request_data = {'files': [{'file_name': file_name, 'content_type': 'video/mp4'}]} - - mock_transcript_preferences.return_value = transcript_preferences - - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) - - # Track generate_presigned_url calls to inspect metadata - presigned_url_calls = [] - - def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): - presigned_url_calls.append((operation, Params, ExpiresIn)) - file_name = Params['Metadata']['client_video_id'] - return f'http://example.com/url_{file_name}' - - mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url - - videos_handler_url = reverse_course_url('videos_handler', self.course.id) - with patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled' - ) as video_transcript_feature: - video_transcript_feature.return_value = is_video_transcript_enabled - response = self.client.post(videos_handler_url, json.dumps(request_data), content_type='application/json') - - self.assertEqual(response.status_code, 200) # noqa: PT009 - - # Ensure `transcript_preferences` was set up in metadata correctly if sent through request. - if is_video_transcript_enabled and transcript_preferences: - self.assertEqual(len(presigned_url_calls), 1) # noqa: PT009 - metadata = presigned_url_calls[0][1]['Metadata'] - self.assertIn('transcript_preferences', metadata) # noqa: PT009 - self.assertEqual(metadata['transcript_preferences'], json.dumps(transcript_preferences)) # noqa: PT009 - else: - # If conditions aren't met, verify transcript_preferences is not in metadata - if presigned_url_calls: - metadata = presigned_url_calls[0][1]['Metadata'] - self.assertNotIn('transcript_preferences', metadata) # noqa: PT009 - - -@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) -@override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"}) -class VideoUrlsCsvTestCase( - VideoUploadTestBase, - VideoStudioAccessTestsMixin, - VideoPipelineStudioAccessTestsMixin, - CourseTestCase -): - """Test cases for the CSV download endpoint for video uploads""" - - VIEW_NAME = "video_encodings_download" - - def setUp(self): - super().setUp() - VideoUploadConfig(profile_whitelist="profile1").save() - - def _check_csv_response(self, expected_profiles): - """ - Check that the response is a valid CSV response containing rows - corresponding to previous_uploads and including the expected profiles. - """ - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertEqual( # noqa: PT009 - response["Content-Disposition"], - f"attachment; filename=\"{self.course.id.course}_video_urls.csv\"" - ) - response_content = b"".join(response.streaming_content) - response_reader = StringIO(response_content.decode()) - reader = csv.DictReader(response_reader, dialect=csv.excel) - self.assertEqual( # noqa: PT009 - reader.fieldnames, - ( - ["Name", "Duration", "Date Added", "Video ID", "Status"] + - [f"{profile} URL" for profile in expected_profiles] - ) - ) - rows = list(reader) - self.assertEqual(len(rows), len(self.previous_uploads)) # noqa: PT009 - for i, row in enumerate(rows): - response_video = dict(row.items()) - # Videos should be returned by creation date descending - original_video = self.previous_uploads[-(i + 1)] - client_video_id = original_video["client_video_id"] - self.assertEqual(response_video["Name"], client_video_id) # noqa: PT009 - self.assertEqual(response_video["Duration"], str(original_video["duration"])) # noqa: PT009 - dateutil.parser.parse(response_video["Date Added"]) - self.assertEqual(response_video["Video ID"], original_video["edx_video_id"]) # noqa: PT009 - self.assertEqual(response_video["Status"], convert_video_status(original_video)) # noqa: PT009 - for profile in expected_profiles: - response_profile_url = response_video[f"{profile} URL"] - original_encoded_for_profile = next( - ( - original_encoded - for original_encoded in original_video["encoded_videos"] - if original_encoded["profile"] == profile - ), - None - ) - if original_encoded_for_profile: - original_encoded_for_profile_url = original_encoded_for_profile["url"] - self.assertEqual(response_profile_url, original_encoded_for_profile_url) # noqa: PT009 - else: - self.assertEqual(response_profile_url, "") # noqa: PT009 - - def test_basic(self): - self._check_csv_response(["profile1"]) - - def test_profile_whitelist(self): - VideoUploadConfig(profile_whitelist="profile1,profile2").save() - self._check_csv_response(["profile1", "profile2"]) - - def test_non_ascii_course(self): - course = CourseFactory.create( - number="nón-äscii", - video_upload_pipeline={ - "course_video_upload_token": self.test_token, - } - ) - response = self.client.get(self.get_url_for_course_key(course.id)) - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertEqual( # noqa: PT009 - response["Content-Disposition"], - "attachment; filename*=utf-8''n%C3%B3n-%C3%A4scii_video_urls.csv" - ) - - -@ddt.ddt -class GetVideoFeaturesTestCase( - CourseTestCase -): - """Test cases for the get_video_features endpoint """ - def setUp(self): - super().setUp() - self.url = self.get_url_for_course_key() - - def get_url_for_course_key(self): - """ Helper to generate a url for a course key """ - return reverse("video_features") - - def test_basic(self): - """ Test for expected return keys """ - response = self.client.get(self.get_url_for_course_key()) - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertEqual( # noqa: PT009 - set(response.json().keys()), - { - 'videoSharingEnabled', - 'allowThumbnailUpload', - } - ) - - @ddt.data(True, False) - def test_video_share_enabled(self, is_enabled): - """ Test the public video share flag """ - self._test_video_feature( - PUBLIC_VIDEO_SHARE, - 'videoSharingEnabled', - override_waffle_flag, - is_enabled, - ) - - @ddt.data(True, False) - def test_video_image_upload_enabled(self, is_enabled): - """ Test the video image upload switch """ - self._test_video_feature( - VIDEO_IMAGE_UPLOAD_ENABLED, - 'allowThumbnailUpload', - override_waffle_switch, - is_enabled, - ) - - def _test_video_feature(self, flag, key, override_fn, is_enabled): - """ Test that setting a waffle flag or switch on or off will cause the expected result """ - with override_fn(flag, is_enabled): - response = self.client.get(self.get_url_for_course_key()) - - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertEqual(response.json()[key], is_enabled) # noqa: PT009 - - -class GetStorageBucketTestCase(TestCase): - """ This test just check that connection works and returns the bucket. - It does not involve any mocking and triggers errors if has any import issue. - """ - @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') - @override_settings(VIDEO_UPLOAD_PIPELINE={ - "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" - }) - @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') - def test_storage_bucket(self, mock_boto3_resource): - """ Test that storage service functions work correctly with boto3.""" - # Setup boto3 mocks - mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource, 'vem_test_bucket') - - # Test storage_service_bucket function - bucket = storage_service_bucket() - self.assertEqual(bucket.name, 'vem_test_bucket') # noqa: PT009 - mock_s3_resource.Bucket.assert_called_once_with('vem_test_bucket') - - # Test storage_service_key function - edx_video_id = 'dummy_video' - key_name = storage_service_key(bucket, file_name=edx_video_id) - expected_key = 'test_root/dummy_video' - self.assertEqual(key_name, expected_key) # noqa: PT009 - - # Test that we can generate presigned URL using the bucket's client - mock_s3_client.generate_presigned_url.return_value = ( - 'https://vem_test_bucket.s3.amazonaws.com:443/test_root/dummy_video?signature=test' - ) - upload_url = mock_s3_client.generate_presigned_url( - 'put_object', - Params={ - 'Bucket': bucket.name, - 'Key': key_name, - 'ContentType': 'video/mp4' - }, - ExpiresIn=KEY_EXPIRATION_IN_SECONDS - ) - - self.assertIn("vem_test_bucket.s3.amazonaws.com", upload_url) # noqa: PT009 - self.assertIn("test_root/dummy_video", upload_url) # noqa: PT009 - - -class CourseYoutubeEdxVideoIds(ModuleStoreTestCase): - """ - This test checks youtube videos in a course - """ - VIEW_NAME = 'youtube_edx_video_ids' - - def setUp(self): - super().setUp() - self.course = CourseFactory.create() - self.course_with_no_youtube_videos = CourseFactory.create() - self.store = modulestore() - self.user = UserFactory() - self.client.login(username=self.user.username, password='Password1234') - - def get_url_for_course_key(self, course_key, kwargs=None): - """Return video handler URL for the given course""" - return reverse_course_url(self.VIEW_NAME, course_key, kwargs) # lint-amnesty, pylint: disable=no-member - - def test_course_with_youtube_videos(self): - course_key = self.course.id - - with self.store.bulk_operations(course_key): - chapter_loc = self.store.create_child( - self.user.id, self.course.location, 'chapter', 'test_chapter' - ).location - seq_loc = self.store.create_child( - self.user.id, chapter_loc, 'sequential', 'test_seq' - ).location - vert_loc = self.store.create_child(self.user.id, seq_loc, 'vertical', 'test_vert').location - self.store.create_child( - self.user.id, - vert_loc, - 'problem', - 'test_problem', - fields={"data": "Test"} - ) - self.store.create_child( - self.user.id, vert_loc, 'video', fields={ - "youtube_is_available": False, - "name": "sample_video", - "edx_video_id": "youtube_193_84709099", - } - ) - - response = self.client.get(self.get_url_for_course_key(course_key)) - self.assertEqual(response.status_code, 200) # noqa: PT009 - - edx_video_ids = json.loads(response.content.decode('utf-8'))['edx_video_ids'] - self.assertEqual(len(edx_video_ids), 1) # noqa: PT009 - - def test_course_with_no_youtube_videos(self): - course_key = self.course_with_no_youtube_videos.id - - with self.store.bulk_operations(course_key): - chapter_loc = self.store.create_child( - self.user.id, self.course_with_no_youtube_videos.location, 'chapter', 'test_chapter' - ).location - seq_loc = self.store.create_child( - self.user.id, chapter_loc, 'sequential', 'test_seq' - ).location - vert_loc = self.store.create_child(self.user.id, seq_loc, 'vertical', 'test_vert').location - self.store.create_child( - self.user.id, vert_loc, 'problem', 'test_problem', fields={"data": "Test"} - ) - self.store.create_child( - self.user.id, vert_loc, 'video', fields={ - "youtube_id_1_0": None, - "name": "sample_video", - "edx_video_id": "no_youtube_193_84709099", - } - ) - - response = self.client.get(self.get_url_for_course_key(course_key)) - - edx_video_ids = json.loads(response.content.decode('utf-8'))['edx_video_ids'] - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertEqual(len(edx_video_ids), 0) # noqa: PT009 +""" +Unit tests for video-related REST APIs. +""" + + +import csv +import json +import re +from contextlib import contextmanager +from datetime import datetime +from io import StringIO +from unittest.mock import Mock, patch + +import dateutil.parser +import ddt +import pytz +from django.conf import settings +from django.test import TestCase +from django.test.utils import override_settings +from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch +from edxval.api import ( + create_or_update_transcript_preferences, + create_or_update_video_transcript, + create_profile, + create_video, + get_course_video_image_url, + get_transcript_preferences, + get_video_info, +) + +from cms.djangoapps.contentstore.models import VideoUploadConfig +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.utils import reverse_course_url +from cms.djangoapps.contentstore.video_storage_handlers import ( + PUBLIC_VIDEO_SHARE, + StatusDisplayStrings, + TranscriptProvider, + convert_video_status, + storage_service_bucket, + storage_service_key, +) +from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file +from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE, ENABLE_DEVSTACK_VIDEO_UPLOADS +from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order + +from ..videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_IMAGE_UPLOAD_ENABLED + + +def setup_s3_mocks(mock_boto3_resource, bucket_name='test-bucket'): + """ + Helper function to set up consistent boto3 S3 mocks. + + Args: + mock_boto3_resource: The patched boto3.resource mock + bucket_name: Name for the mock bucket (default: 'test-bucket') + + Returns: + tuple: (mock_s3_client, mock_bucket, mock_s3_resource) + """ + mock_s3_client = Mock() + mock_bucket = Mock() + mock_bucket.name = bucket_name + mock_bucket.meta.client = mock_s3_client + mock_s3_resource = Mock() + mock_s3_resource.Bucket.return_value = mock_bucket + mock_boto3_resource.return_value = mock_s3_resource + return mock_s3_client, mock_bucket, mock_s3_resource + + +class VideoUploadTestBase: + """ + Test cases for the video upload feature + """ + + def get_url_for_course_key(self, course_key, kwargs=None): + """Return video handler URL for the given course""" + return reverse_course_url(self.VIEW_NAME, course_key, kwargs) # lint-amnesty, pylint: disable=no-member + + def setUp(self): + super().setUp() # lint-amnesty, pylint: disable=no-member + self.url = self.get_url_for_course_key(self.course.id) + self.test_token = "test_token" + self.course.video_upload_pipeline = { + "course_video_upload_token": self.test_token, + } + self.save_course() # lint-amnesty, pylint: disable=no-member + + # create another course for videos belonging to multiple courses + self.course2 = CourseFactory.create() + self.course2.video_upload_pipeline = { + "course_video_upload_token": self.test_token, + } + self.course2.save() + self.store.update_item(self.course2, self.user.id) # lint-amnesty, pylint: disable=no-member + + # course ids for videos + course_ids = [str(self.course.id), str(self.course2.id)] + created = datetime.now(pytz.utc) + + self.profiles = ["profile1", "profile2"] + self.previous_uploads = [ + { + "edx_video_id": "test1", + "client_video_id": "test1.mp4", + "duration": 42.0, + "status": "upload", + "courses": course_ids, + "encoded_videos": [], + "created": created + }, + { + "edx_video_id": "test2", + "client_video_id": "test2.mp4", + "duration": 128.0, + "status": "file_complete", + "courses": course_ids, + "created": created, + "encoded_videos": [ + { + "profile": "profile1", + "url": "http://example.com/profile1/test2.mp4", + "file_size": 1600, + "bitrate": 100, + }, + { + "profile": "profile2", + "url": "http://example.com/profile2/test2.mov", + "file_size": 16000, + "bitrate": 1000, + }, + ], + }, + { + "edx_video_id": "non-ascii", + "client_video_id": "nón-ascii-näme.mp4", + "duration": 256.0, + "status": "transcode_active", + "courses": course_ids, + "created": created, + "encoded_videos": [ + { + "profile": "profile1", + "url": "http://example.com/profile1/nón-ascii-näme.mp4", + "file_size": 3200, + "bitrate": 100, + }, + ] + }, + ] + # Ensure every status string is tested + self.previous_uploads += [ + { + "edx_video_id": f"status_test_{status}", + "client_video_id": "status_test.mp4", + "duration": 3.14, + "status": status, + "courses": course_ids, + "created": created, + "encoded_videos": [], + } + for status in ( + list(StatusDisplayStrings._STATUS_MAP.keys()) + # pylint:disable=protected-access + ["non_existent_status"] + ) + ] + for profile in self.profiles: + create_profile(profile) + for video in self.previous_uploads: + create_video(video) + + def _get_previous_upload(self, edx_video_id): + """Returns the previous upload with the given video id.""" + return next( + video + for video in self.previous_uploads + if video["edx_video_id"] == edx_video_id + ) + + +class VideoStudioAccessTestsMixin: + """ + Base Access tests for studio video views + """ + def test_anon_user(self): + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) # noqa: PT009 + + def test_put(self): + response = self.client.put(self.url) + self.assertEqual(response.status_code, 405) # noqa: PT009 + + def test_invalid_course_key(self): + response = self.client.get( + self.get_url_for_course_key("Non/Existent/Course") + ) + self.assertEqual(response.status_code, 404) # noqa: PT009 + + def test_non_staff_user(self): + client, __ = self.create_non_staff_authed_user_client() + response = client.get(self.url) + self.assertEqual(response.status_code, 403) # noqa: PT009 + + +class VideoPipelineStudioAccessTestsMixin: + """ + Access tests for video views that rely on the video pipeline + """ + def test_video_pipeline_not_enabled(self): + settings.FEATURES["ENABLE_VIDEO_UPLOAD_PIPELINE"] = False + self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 + + def test_video_pipeline_not_configured(self): + settings.VIDEO_UPLOAD_PIPELINE = None + self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 + + def test_course_not_configured(self): + self.course.video_upload_pipeline = {} + self.save_course() + self.assertEqual(self.client.get(self.url).status_code, 404) # noqa: PT009 + + +class VideoUploadPostTestsMixin: + """ + Shared test cases for video post tests. + """ + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + def test_post_success(self, mock_boto3_resource): + files = [ + { + 'file_name': 'first.mp4', + 'content_type': 'video/mp4', + }, + { + 'file_name': 'second.mp4', + 'content_type': 'video/mp4', + }, + { + 'file_name': 'third.mov', + 'content_type': 'video/quicktime', + }, + { + 'file_name': 'fourth.mp4', + 'content_type': 'video/mp4', + }, + ] + + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Mock generate_presigned_url to return different URLs for each file + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + response = self.client.post( + self.url, + json.dumps({'files': files}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) # noqa: PT009 + response_obj = json.loads(response.content.decode('utf-8')) + + # Verify boto3 resource was called correctly + mock_boto3_resource.assert_called_once_with( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY + ) + + self.assertEqual(len(response_obj['files']), len(files)) # noqa: PT009 + self.assertEqual(mock_s3_client.generate_presigned_url.call_count, len(files)) # noqa: PT009 + + for i, file_info in enumerate(files): + # Get the call args for this file's presigned URL generation + call_args = mock_s3_client.generate_presigned_url.call_args_list[i] + args, kwargs = call_args + + # Verify the operation and params + self.assertEqual(args[0], 'put_object') # noqa: PT009 + self.assertEqual(kwargs['Params']['Bucket'], 'test-bucket') # noqa: PT009 + self.assertEqual(kwargs['Params']['ContentType'], file_info['content_type']) # noqa: PT009 + self.assertEqual(kwargs['ExpiresIn'], KEY_EXPIRATION_IN_SECONDS) # noqa: PT009 + + # Extract video_id from the key + key_name = kwargs['Params']['Key'] + path_match = re.match( + ( + settings.VIDEO_UPLOAD_PIPELINE['ROOT_PATH'] + + '/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$' + ), + key_name + ) + self.assertIsNotNone(path_match) # noqa: PT009 + video_id = path_match.group(1) + + # Verify metadata + metadata = kwargs['Params']['Metadata'] + self.assertEqual(metadata['course_video_upload_token'], self.test_token) # noqa: PT009 + self.assertEqual(metadata['client_video_id'], file_info['file_name']) # noqa: PT009 + self.assertEqual(metadata['course_key'], str(self.course.id)) # noqa: PT009 + + # Ensure VAL was updated + val_info = get_video_info(video_id) + self.assertEqual(val_info['status'], 'upload') # noqa: PT009 + self.assertEqual(val_info['client_video_id'], file_info['file_name']) # noqa: PT009 + self.assertEqual(val_info['duration'], 0) # noqa: PT009 + self.assertEqual(val_info['courses'], [{str(self.course.id): None}]) # noqa: PT009 + + # Ensure response is correct + response_file = response_obj['files'][i] + self.assertEqual(response_file['file_name'], file_info['file_name']) # noqa: PT009 + self.assertEqual(response_file['upload_url'], f'http://example.com/url_{file_info["file_name"]}') # noqa: PT009 + + def test_post_non_json(self): + response = self.client.post(self.url, {"files": []}) + self.assertEqual(response.status_code, 400) # noqa: PT009 + + def test_post_malformed_json(self): + response = self.client.post(self.url, "{", content_type="application/json") + self.assertEqual(response.status_code, 400) # noqa: PT009 + + def test_post_invalid_json(self): + def assert_bad(content): + """Make request with content and assert that response is 400""" + response = self.client.post( + self.url, + json.dumps(content), + content_type="application/json" + ) + self.assertEqual(response.status_code, 400) # noqa: PT009 + + # Top level missing files key + assert_bad({}) + + # Entry missing file_name + assert_bad({"files": [{"content_type": "video/mp4"}]}) + + # Entry missing content_type + assert_bad({"files": [{"file_name": "test.mp4"}]}) + + +@ddt.ddt +@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) +@override_settings(VIDEO_UPLOAD_PIPELINE={ + "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" +}) +class VideosHandlerTestCase( + VideoUploadTestBase, + VideoStudioAccessTestsMixin, + VideoPipelineStudioAccessTestsMixin, + VideoUploadPostTestsMixin, + CourseTestCase +): + """Test cases for the main video upload endpoint""" + + VIEW_NAME = 'videos_handler' + + def test_get_json(self): + response = self.client.get_json(self.url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + response_videos = json.loads(response.content.decode('utf-8'))['videos'] + self.assertEqual(len(response_videos), len(self.previous_uploads)) # noqa: PT009 + for i, response_video in enumerate(response_videos): + # Videos should be returned by creation date descending + original_video = self.previous_uploads[-(i + 1)] + print(response_video.keys()) + self.assertEqual( # noqa: PT009 + set(response_video.keys()), + { + 'edx_video_id', + 'client_video_id', + 'created', + 'duration', + 'status', + 'status_nontranslated', + 'course_video_image_url', + 'file_size', + 'download_link', + 'transcripts', + 'transcription_status', + 'transcript_urls', + 'error_description', + } + ) + dateutil.parser.parse(response_video['created']) + for field in ['edx_video_id', 'client_video_id', 'duration']: + self.assertEqual(response_video[field], original_video[field]) # noqa: PT009 + self.assertEqual( # noqa: PT009 + response_video['status'], + convert_video_status(original_video) + ) + + @ddt.data( + ( + [ + 'edx_video_id', 'client_video_id', 'created', 'duration', + 'status', 'status_nontranslated', 'course_video_image_url', 'file_size', + 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', + 'error_description' + ], + [ + { + 'video_id': 'test1', + 'language_code': 'en', + 'file_name': 'edx101.srt', + 'file_format': 'srt', + 'provider': 'Cielo24' + } + ], + ['en'] + ), + ( + [ + 'edx_video_id', 'client_video_id', 'created', 'duration', + 'status', 'status_nontranslated', 'course_video_image_url', 'file_size', + 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', + 'error_description' + ], + [ + { + 'video_id': 'test1', + 'language_code': 'en', + 'file_name': 'edx101_en.srt', + 'file_format': 'srt', + 'provider': 'Cielo24' + }, + { + 'video_id': 'test1', + 'language_code': 'es', + 'file_name': 'edx101_es.srt', + 'file_format': 'srt', + 'provider': 'Cielo24' + } + ], + ['en', 'es'] + ) + ) + @ddt.unpack + def test_get_json_transcripts(self, expected_video_keys, uploaded_transcripts, expected_transcripts): + """ + Test that transcripts are attached based on whether the video transcript feature is enabled. + """ + for transcript in uploaded_transcripts: + create_or_update_video_transcript( + transcript['video_id'], + transcript['language_code'], + metadata={ + 'file_name': transcript['file_name'], + 'file_format': transcript['file_format'], + 'provider': transcript['provider'] + } + ) + + response = self.client.get_json(self.url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + response_videos = json.loads(response.content.decode('utf-8'))['videos'] + self.assertEqual(len(response_videos), len(self.previous_uploads)) # noqa: PT009 + for response_video in response_videos: + print(response_video) + + self.assertEqual(set(response_video.keys()), set(expected_video_keys)) # noqa: PT009 + if response_video['edx_video_id'] == self.previous_uploads[0]['edx_video_id']: + self.assertEqual(response_video.get('transcripts', []), expected_transcripts) # noqa: PT009 + + def test_get_redirects_to_video_uploads_url(self): + """ + Test that GET requests redirect to the MFE video uploads page. + """ + from cms.djangoapps.contentstore.utils import get_video_uploads_url + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) # noqa: PT009 + self.assertEqual(response.url, get_video_uploads_url(self.course.id)) # noqa: PT009 + + @override_settings(AWS_ACCESS_KEY_ID="test_key_id", AWS_SECRET_ACCESS_KEY="test_secret") + @patch("cms.djangoapps.contentstore.video_storage_handlers.boto3.resource") + @ddt.data( + ( + [ + { + "file_name": "supported-1.mp4", + "content_type": "video/mp4", + }, + { + "file_name": "supported-2.mov", + "content_type": "video/quicktime", + }, + ], + 200 + ), + ( + [ + { + "file_name": "unsupported-1.txt", + "content_type": "text/plain", + }, + { + "file_name": "unsupported-2.png", + "content_type": "image/png", + }, + ], + 400 + ) + ) + @ddt.unpack + def test_video_supported_file_formats(self, files, expected_status, mock_boto3_resource): + """ + Test that video upload works correctly against supported and unsupported file formats. + """ + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Mock generate_presigned_url to return different URLs for each file + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + # Check supported formats + response = self.client.post( + self.url, + json.dumps({"files": files}), + content_type="application/json" + ) + self.assertEqual(response.status_code, expected_status) # noqa: PT009 + response = json.loads(response.content.decode('utf-8')) + + if expected_status == 200: + self.assertNotIn('error', response) # noqa: PT009 + else: + self.assertIn('error', response) # noqa: PT009 + self.assertEqual(response['error'], "Request 'files' entry contain unsupported content_type") # noqa: PT009 + + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + def test_upload_with_non_ascii_charaters(self, mock_boto3_resource): + """ + Test that video uploads throws error message when file name contains special characters. + """ + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + file_name = 'test\u2019_file.mp4' + files = [{'file_name': file_name, 'content_type': 'video/mp4'}] + + response = self.client.post( + self.url, + json.dumps({'files': files}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 400) # noqa: PT009 + response = json.loads(response.content.decode('utf-8')) + self.assertEqual(response['error'], 'The file name for %s must contain only ASCII characters.' % file_name) # noqa: PT009, UP031 + + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret', AWS_SECURITY_TOKEN='token') + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + @override_waffle_flag(ENABLE_DEVSTACK_VIDEO_UPLOADS, active=True) + def test_devstack_upload_connection(self, mock_boto3_resource): + files = [{'file_name': 'first.mp4', 'content_type': 'video/mp4'}] + + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Mock generate_presigned_url to return different URLs for each file + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + response = self.client.post( + self.url, + json.dumps({'files': files}), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) # noqa: PT009 + mock_boto3_resource.assert_called_once_with( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + security_token=settings.AWS_SECURITY_TOKEN + ) + + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + def test_send_course_to_vem_pipeline(self, mock_boto3_resource): + """ + Test that uploads always go to VEM S3 bucket by default. + """ + files = [{'file_name': 'first.mp4', 'content_type': 'video/mp4'}] + + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Mock generate_presigned_url to return different URLs for each file + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + response = self.client.post( + self.url, + json.dumps({'files': files}), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) # noqa: PT009 + mock_s3_resource.Bucket.assert_called_once_with( + settings.VIDEO_UPLOAD_PIPELINE['VEM_S3_BUCKET'] # pylint: disable=unsubscriptable-object + ) + + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + @ddt.data( + { + 'global_waffle': True, + 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, + 'expect_token': True + }, + { + 'global_waffle': False, + 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, + 'expect_token': False + }, + { + 'global_waffle': False, + 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, + 'expect_token': True + } + ) + def test_video_upload_token_in_meta(self, data, mock_boto3_resource): + """ + Test video upload token in s3 metadata. + """ + @contextmanager + def proxy_manager(manager, ignore_manager): + """ + This acts as proxy to the original manager in the arguments given + the original manager is not set to be ignored. + """ + if ignore_manager: + yield + else: + with manager: + yield + + file_data = { + 'file_name': 'first.mp4', + 'content_type': 'video/mp4', + } + + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Track generate_presigned_url calls to inspect metadata + presigned_url_calls = [] + + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + presigned_url_calls.append((operation, Params, ExpiresIn)) + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']): + with override_waffle_flag(DEPRECATE_YOUTUBE, active=data['global_waffle']): + response = self.client.post( + self.url, + json.dumps({'files': [file_data]}), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) # noqa: PT009 + + # Check if course_video_upload_token is in metadata based on expectation + if data['expect_token']: + # We should find the token in the metadata + self.assertEqual(len(presigned_url_calls), 1) # noqa: PT009 + metadata = presigned_url_calls[0][1]['Metadata'] + self.assertIn('course_video_upload_token', metadata) # noqa: PT009 + self.assertEqual(metadata['course_video_upload_token'], self.test_token) # noqa: PT009 + else: + # If we don't expect a token, verify it's not in metadata + if presigned_url_calls: + metadata = presigned_url_calls[0][1]['Metadata'] + self.assertNotIn('course_video_upload_token', metadata) # noqa: PT009 + + def _assert_video_removal(self, url, edx_video_id, deleted_videos): + """ + Verify that if correct video is removed from a particular course. + + Arguments: + url (str): URL to get uploaded videos + edx_video_id (str): video id + deleted_videos (int): how many videos are deleted + """ + response = self.client.get_json(url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + response_videos = json.loads(response.content.decode('utf-8'))["videos"] + self.assertEqual(len(response_videos), len(self.previous_uploads) - deleted_videos) # noqa: PT009 + + if deleted_videos: + self.assertNotIn(edx_video_id, [video.get('edx_video_id') for video in response_videos]) # noqa: PT009 + else: + self.assertIn(edx_video_id, [video.get('edx_video_id') for video in response_videos]) # noqa: PT009 + + def test_video_removal(self): + """ + Verifies that video removal is working as expected. + """ + edx_video_id = 'test1' + remove_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + response = self.client.delete(remove_url, HTTP_ACCEPT="application/json") + self.assertEqual(response.status_code, 204) # noqa: PT009 + + self._assert_video_removal(self.url, edx_video_id, 1) + + def test_video_removal_multiple_courses(self): + """ + Verifies that video removal is working as expected for multiple courses. + + If a video is used by multiple courses then removal from one course shouldn't effect the other course. + """ + # remove video from course1 + edx_video_id = 'test1' + remove_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + response = self.client.delete(remove_url, HTTP_ACCEPT="application/json") + self.assertEqual(response.status_code, 204) # noqa: PT009 + + # verify that video is only deleted from course1 only + self._assert_video_removal(self.url, edx_video_id, 1) + self._assert_video_removal(self.get_url_for_course_key(self.course2.id), edx_video_id, 0) + + def test_convert_video_status(self): + """ + Verifies that convert_video_status works as expected. + """ + video = self.previous_uploads[0] + + # video status should be failed if it's in upload state for more than 24 hours + video['created'] = datetime(2016, 1, 1, 10, 10, 10, 0, pytz.UTC) + status = convert_video_status(video) + self.assertEqual(status, StatusDisplayStrings.get('upload_failed')) # noqa: PT009 + + # `invalid_token` should be converted to `youtube_duplicate` + video['created'] = datetime.now(pytz.UTC) + video['status'] = 'invalid_token' + status = convert_video_status(video) + self.assertEqual(status, StatusDisplayStrings.get('youtube_duplicate')) # noqa: PT009 + + # The "encode status" should be converted to `file_complete` if video encodes are complete + video['status'] = 'transcription_in_progress' + status = convert_video_status(video, is_video_encodes_ready=True) + self.assertEqual(status, StatusDisplayStrings.get('file_complete')) # noqa: PT009 + + # If encoding is not complete return the status as it is + video['status'] = 's3_upload_failed' + status = convert_video_status(video) + self.assertEqual(status, StatusDisplayStrings.get('s3_upload_failed')) # noqa: PT009 + + # for all other status, there should not be any conversion + statuses = list(StatusDisplayStrings._STATUS_MAP.keys()) # pylint: disable=protected-access + statuses.remove('invalid_token') + for status in statuses: + video['status'] = status + new_status = convert_video_status(video) + self.assertEqual(new_status, StatusDisplayStrings.get(status)) # noqa: PT009 + + def assert_video_status(self, url, edx_video_id, status): + """ + Verifies that video with `edx_video_id` has `status` + """ + response = self.client.get_json(url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + videos = json.loads(response.content.decode('utf-8'))["videos"] + for video in videos: + if video['edx_video_id'] == edx_video_id: + return self.assertEqual(video['status'], status) # noqa: PT009 + + # Test should fail if video not found + self.assertEqual(True, False, 'Invalid edx_video_id') # noqa: PT009 + + @patch('cms.djangoapps.contentstore.video_storage_handlers.LOGGER') + def test_video_status_update_request(self, mock_logger): + """ + Verifies that video status update request works as expected. + """ + url = self.get_url_for_course_key(self.course.id) + edx_video_id = 'test1' + self.assert_video_status(url, edx_video_id, 'Uploading') + + response = self.client.post( + url, + json.dumps([{ + 'edxVideoId': edx_video_id, + 'status': 'upload_failed', + 'message': 'server down' + }]), + content_type="application/json" + ) + + mock_logger.info.assert_called_with( + 'VIDEOS: Video status update with id [%s], status [%s] and message [%s]', + edx_video_id, + 'upload_failed', + 'server down' + ) + + self.assertEqual(response.status_code, 204) # noqa: PT009 + + self.assert_video_status(url, edx_video_id, 'Failed') + + @ddt.data( + ('test_video_token', "Transcription in Progress"), + ('', "Ready"), + ) + @ddt.unpack + def test_video_transcript_status_conversion(self, course_video_upload_token, expected_video_status_text): + """ + Verifies that video status `transcription_in_progress` gets converted + correctly into the `file_complete` for the new video workflow and + stays as it is, for the old video workflow. + """ + self.course.video_upload_pipeline = { + 'course_video_upload_token': course_video_upload_token + } + self.save_course() + + url = self.get_url_for_course_key(self.course.id) + edx_video_id = 'test1' + self.assert_video_status(url, edx_video_id, 'Uploading') + + response = self.client.post( + url, + json.dumps([{ + 'edxVideoId': edx_video_id, + 'status': 'transcription_in_progress', + 'message': 'Transcription is in progress' + }]), + content_type="application/json" + ) + self.assertEqual(response.status_code, 204) # noqa: PT009 + + self.assert_video_status(url, edx_video_id, expected_video_status_text) + + +@ddt.ddt +@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) +@override_settings(VIDEO_UPLOAD_PIPELINE={ + "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" +}) +class GenerateVideoUploadLinkTestCase( + VideoUploadTestBase, + VideoUploadPostTestsMixin, + CourseTestCase +): + """ + Test cases for the main video upload endpoint + """ + + VIEW_NAME = 'generate_video_upload_link' + + def test_unsupported_requests_fail(self): + """ + The API only supports post, make sure other requests fail + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, 405) # noqa: PT009 + + response = self.client.put(self.url) + self.assertEqual(response.status_code, 405) # noqa: PT009 + + response = self.client.patch(self.url) + self.assertEqual(response.status_code, 405) # noqa: PT009 + + +@ddt.ddt +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True}) +@override_settings(VIDEO_UPLOAD_PIPELINE={'BUCKET': 'test_bucket', 'ROOT_PATH': 'test_root'}) +class VideoImageTestCase(VideoUploadTestBase, CourseTestCase): + """ + Tests for video image. + """ + + VIEW_NAME = "video_images_handler" + + def verify_image_upload_reponse(self, course_id, edx_video_id, upload_response): + """ + Verify that image is uploaded successfully. + + Arguments: + course_id: ID of course + edx_video_id: ID of video + upload_response: Upload response object + + Returns: + uploaded image url + """ + self.assertEqual(upload_response.status_code, 200) # noqa: PT009 + response = json.loads(upload_response.content.decode('utf-8')) + val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id) + self.assertEqual(response['image_url'], val_image_url) # noqa: PT009 + + return val_image_url + + def verify_error_message(self, response, error_message): + """ + Verify that image upload failure gets proper error message. + + Arguments: + response: Response object. + error_message: Expected error message. + """ + self.assertEqual(response.status_code, 400) # noqa: PT009 + response = json.loads(response.content.decode('utf-8')) + self.assertIn('error', response) # noqa: PT009 + self.assertEqual(response['error'], error_message) # noqa: PT009 + + @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, False) + def test_video_image_upload_disabled(self): + """ + Tests the video image upload when the feature is disabled. + """ + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': 'test_vid_id'}) + response = self.client.post(video_image_upload_url, {'file': 'dummy_file'}, format='multipart') + self.assertEqual(response.status_code, 404) # noqa: PT009 + + @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) + def test_video_image(self): + """ + Test video image is saved. + """ + edx_video_id = 'test1' + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + with make_image_file( + dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), + ) as image_file: + response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + image_url1 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) + + # upload again to verify that new image is uploaded successfully + with make_image_file( + dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), + ) as image_file: + response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + image_url2 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) + + self.assertNotEqual(image_url1, image_url2) # noqa: PT009 + + @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) + def test_video_image_no_file(self): + """ + Test that an error error message is returned if upload request is incorrect. + """ + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': 'test1'}) + response = self.client.post(video_image_upload_url, {}) + self.verify_error_message(response, 'An image file is required.') + + @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) + def test_no_video_image(self): + """ + Test image url is set to None if no video image. + """ + edx_video_id = 'test1' + get_videos_url = reverse_course_url('videos_handler', self.course.id) + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + with make_image_file( + dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), + ) as image_file: + self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + + val_image_url = get_course_video_image_url(course_id=self.course.id, edx_video_id=edx_video_id) + + response = self.client.get_json(get_videos_url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + response_videos = json.loads(response.content.decode('utf-8'))["videos"] + for response_video in response_videos: + if response_video['edx_video_id'] == edx_video_id: + self.assertEqual(response_video['course_video_image_url'], val_image_url) # noqa: PT009 + else: + self.assertEqual(response_video['course_video_image_url'], None) # noqa: PT009 + + @ddt.data( + # Image file type validation + ( + { + 'extension': '.png' + }, + None + ), + ( + { + 'extension': '.gif' + }, + None + ), + ( + { + 'extension': '.bmp' + }, + None + ), + ( + { + 'extension': '.jpg' + }, + None + ), + ( + { + 'extension': '.jpeg' + }, + None + ), + ( + { + 'extension': '.PNG' + }, + None + ), + ( + { + 'extension': '.tiff' + }, + 'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( # noqa: UP032 + supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) + ) + ), + # Image file size validation + ( + { + 'size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'] + 10 + }, + 'This image file must be smaller than {image_max_size}.'.format( # noqa: UP032 + image_max_size=settings.VIDEO_IMAGE_MAX_FILE_SIZE_MB + ) + ), + ( + { + 'size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'] - 10 + }, + 'This image file must be larger than {image_min_size}.'.format( # noqa: UP032 + image_min_size=settings.VIDEO_IMAGE_MIN_FILE_SIZE_KB + ) + ), + # Image file minimum width / height + ( + { + 'width': 16, # 16x9 + 'height': 9 + }, + 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. The minimum resolution is {image_file_min_width}x{image_file_min_height}.'.format( # noqa: E501, UP032 + image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, + image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, + image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, + image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT + ) + ), + ( + { + 'width': settings.VIDEO_IMAGE_MIN_WIDTH - 10, + 'height': settings.VIDEO_IMAGE_MIN_HEIGHT + }, + 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. The minimum resolution is {image_file_min_width}x{image_file_min_height}.'.format( # noqa: E501, UP032 + image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, + image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, + image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, + image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT + ) + ), + ( + { + 'width': settings.VIDEO_IMAGE_MIN_WIDTH, + 'height': settings.VIDEO_IMAGE_MIN_HEIGHT - 10 + }, + ( # noqa: UP032 + 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. ' + 'The minimum resolution is {image_file_min_width}x{image_file_min_height}.' + ).format( + image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, + image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, + image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, + image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT + ) + ), + ( + { + 'width': 1200, # not 16:9, but width/height check first. + 'height': 100 + }, + ( # noqa: UP032 + 'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. ' + 'The minimum resolution is {image_file_min_width}x{image_file_min_height}.' + ).format( + image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH, + image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT, + image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH, + image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT + ) + ), + # Image file aspect ratio validation + ( + { + 'width': settings.VIDEO_IMAGE_MAX_WIDTH, # 1280x720 + 'height': settings.VIDEO_IMAGE_MAX_HEIGHT + }, + None + ), + ( + { + 'width': 850, # 16:9 + 'height': 478 + }, + None + ), + ( + { + 'width': 940, # 1.67 ratio, applicable aspect ratio margin of .01 + 'height': 560 + }, + None + ), + ( + { + 'width': settings.VIDEO_IMAGE_MIN_WIDTH + 100, + 'height': settings.VIDEO_IMAGE_MIN_HEIGHT + 200 + }, + 'This image file must have an aspect ratio of {video_image_aspect_ratio_text}.'.format( # noqa: UP032 + video_image_aspect_ratio_text=settings.VIDEO_IMAGE_ASPECT_RATIO_TEXT + ) + ), + # Image file name validation + ( + { + 'prefix': 'nøn-åßç¡¡' + }, + 'The image file name can only contain letters, numbers, hyphens (-), and underscores (_).' + ) + ) + @ddt.unpack + @override_waffle_switch(VIDEO_IMAGE_UPLOAD_ENABLED, True) + def test_video_image_validation_message(self, image_data, error_message): + """ + Test video image validation gives proper error message. + + Arguments: + image_data (Dict): Specific data to create image file. + error_message (String): Error message + """ + edx_video_id = 'test1' + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + with make_image_file( + dimensions=( + image_data.get('width', settings.VIDEO_IMAGE_MIN_WIDTH), + image_data.get('height', settings.VIDEO_IMAGE_MIN_HEIGHT) + ), + prefix=image_data.get('prefix', 'videoimage'), + extension=image_data.get('extension', '.png'), + force_size=image_data.get('size', settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES']) + ) as image_file: + response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + if error_message: + self.verify_error_message(response, error_message) + else: + self.verify_image_upload_reponse(self.course.id, edx_video_id, response) + + +@ddt.ddt +@patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', + Mock(return_value=True) +) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True}) +class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase): + """ + Tests for video transcripts preferences. + """ + + VIEW_NAME = 'transcript_preferences_handler' + + def test_405_with_not_allowed_request_method(self): + """ + Verify that 405 is returned in case of not-allowed request methods. + Allowed request methods are POST and DELETE. + """ + video_transcript_url = self.get_url_for_course_key(self.course.id) + response = self.client.get( + video_transcript_url, + content_type='application/json' + ) + self.assertEqual(response.status_code, 405) # noqa: PT009 + + @ddt.data( + # Video transcript feature disabled + ( + {}, + False, + '', + 404, + ), + # Error cases + ( + {}, + True, + "Invalid provider None.", + 400 + ), + ( + { + 'provider': '' + }, + True, + "Invalid provider .", + 400 + ), + ( + { + 'provider': 'dummy-provider' + }, + True, + "Invalid provider dummy-provider.", + 400 + ), + ( + { + 'provider': TranscriptProvider.CIELO24 + }, + True, + "Invalid cielo24 fidelity None.", + 400 + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + }, + True, + "Invalid cielo24 turnaround None.", + 400 + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + 'cielo24_turnaround': 'STANDARD', + 'video_source_language': 'en' + }, + True, + "Invalid languages [].", + 400 + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PREMIUM', + 'cielo24_turnaround': 'STANDARD', + 'video_source_language': 'es' + }, + True, + "Unsupported source language es.", + 400 + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + 'cielo24_turnaround': 'STANDARD', + 'video_source_language': 'en', + 'preferred_languages': ['es', 'ur'] + }, + True, + "Invalid languages ['es', 'ur'].", + 400 + ), + ( + { + 'provider': TranscriptProvider.THREE_PLAY_MEDIA + }, + True, + "Invalid 3play turnaround None.", + 400 + ), + ( + { + 'provider': TranscriptProvider.THREE_PLAY_MEDIA, + 'three_play_turnaround': 'standard', + 'video_source_language': 'zh', + }, + True, + "Unsupported source language zh.", + 400 + ), + ( + { + 'provider': TranscriptProvider.THREE_PLAY_MEDIA, + 'three_play_turnaround': 'standard', + 'video_source_language': 'es', + 'preferred_languages': ['es', 'ur'] + }, + True, + "Invalid languages ['es', 'ur'].", + 400 + ), + ( + { + 'provider': TranscriptProvider.THREE_PLAY_MEDIA, + 'three_play_turnaround': 'standard', + 'video_source_language': 'en', + 'preferred_languages': ['es', 'ur'] + }, + True, + "Invalid languages ['es', 'ur'].", + 400 + ), + # Success + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + 'cielo24_turnaround': 'STANDARD', + 'video_source_language': 'es', + 'preferred_languages': ['en'] + }, + True, + '', + 200 + ), + ( + { + 'provider': TranscriptProvider.THREE_PLAY_MEDIA, + 'three_play_turnaround': 'standard', + 'preferred_languages': ['en'], + 'video_source_language': 'en', + }, + True, + '', + 200 + ) + ) + @ddt.unpack + def test_video_transcript(self, preferences, is_video_transcript_enabled, error_message, expected_status_code): + """ + Tests that transcript handler works correctly. + """ + video_transcript_url = self.get_url_for_course_key(self.course.id) + preferences_data = { + 'provider': preferences.get('provider'), + 'cielo24_fidelity': preferences.get('cielo24_fidelity'), + 'cielo24_turnaround': preferences.get('cielo24_turnaround'), + 'three_play_turnaround': preferences.get('three_play_turnaround'), + 'preferred_languages': preferences.get('preferred_languages', []), + 'video_source_language': preferences.get('video_source_language'), + } + + with patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled' + ) as video_transcript_feature: + video_transcript_feature.return_value = is_video_transcript_enabled + response = self.client.post( + video_transcript_url, + json.dumps(preferences_data), + content_type='application/json' + ) + status_code = response.status_code + response = json.loads(response.content.decode('utf-8')) if is_video_transcript_enabled else response + + self.assertEqual(status_code, expected_status_code) # noqa: PT009 + self.assertEqual(response.get('error', ''), error_message) # noqa: PT009 + + # Remove modified and course_id fields from the response so as to check the expected transcript preferences. + response.get('transcript_preferences', {}).pop('modified', None) + response.get('transcript_preferences', {}).pop('course_id', None) + expected_preferences = preferences_data if is_video_transcript_enabled and not error_message else {} + self.assertDictEqual(response.get('transcript_preferences', {}), expected_preferences) # noqa: PT009 + + def test_remove_transcript_preferences(self): + """ + Test that transcript handler removes transcript preferences correctly. + """ + # First add course wide transcript preferences. + preferences = create_or_update_transcript_preferences(str(self.course.id)) + + # Verify transcript preferences exist + self.assertIsNotNone(preferences) # noqa: PT009 + + response = self.client.delete( + self.get_url_for_course_key(self.course.id), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 204) # noqa: PT009 + + # Verify transcript preferences no loger exist + preferences = get_transcript_preferences(str(self.course.id)) + self.assertIsNone(preferences) # noqa: PT009 + + def test_remove_transcript_preferences_not_found(self): + """ + Test that transcript handler works correctly even when no preferences are found. + """ + course_id = 'course-v1:dummy+course+id' + # Verify transcript preferences do not exist + preferences = get_transcript_preferences(course_id) + self.assertIsNone(preferences) # noqa: PT009 + + response = self.client.delete( + self.get_url_for_course_key(course_id), + content_type='application/json' + ) + self.assertEqual(response.status_code, 204) # noqa: PT009 + + # Verify transcript preferences do not exist + preferences = get_transcript_preferences(course_id) + self.assertIsNone(preferences) # noqa: PT009 + + @ddt.data( + ( + None, + False + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + 'cielo24_turnaround': 'STANDARD', + 'preferred_languages': ['en'] + }, + False + ), + ( + { + 'provider': TranscriptProvider.CIELO24, + 'cielo24_fidelity': 'PROFESSIONAL', + 'cielo24_turnaround': 'STANDARD', + 'preferred_languages': ['en'] + }, + True + ) + ) + @ddt.unpack + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @patch('cms.djangoapps.contentstore.video_storage_handlers.get_transcript_preferences') + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + def test_transcript_preferences_metadata(self, transcript_preferences, is_video_transcript_enabled, + mock_boto3_resource, mock_transcript_preferences): + """ + Tests that transcript preference metadata is only set if it is video transcript feature is enabled and + transcript preferences are already stored in the system. + """ + file_name = 'test-video.mp4' + request_data = {'files': [{'file_name': file_name, 'content_type': 'video/mp4'}]} + + mock_transcript_preferences.return_value = transcript_preferences + + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource) + + # Track generate_presigned_url calls to inspect metadata + presigned_url_calls = [] + + def mock_generate_presigned_url(operation, Params=None, ExpiresIn=None): + presigned_url_calls.append((operation, Params, ExpiresIn)) + file_name = Params['Metadata']['client_video_id'] + return f'http://example.com/url_{file_name}' + + mock_s3_client.generate_presigned_url.side_effect = mock_generate_presigned_url + + videos_handler_url = reverse_course_url('videos_handler', self.course.id) + with patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled' + ) as video_transcript_feature: + video_transcript_feature.return_value = is_video_transcript_enabled + response = self.client.post(videos_handler_url, json.dumps(request_data), content_type='application/json') + + self.assertEqual(response.status_code, 200) # noqa: PT009 + + # Ensure `transcript_preferences` was set up in metadata correctly if sent through request. + if is_video_transcript_enabled and transcript_preferences: + self.assertEqual(len(presigned_url_calls), 1) # noqa: PT009 + metadata = presigned_url_calls[0][1]['Metadata'] + self.assertIn('transcript_preferences', metadata) # noqa: PT009 + self.assertEqual(metadata['transcript_preferences'], json.dumps(transcript_preferences)) # noqa: PT009 + else: + # If conditions aren't met, verify transcript_preferences is not in metadata + if presigned_url_calls: + metadata = presigned_url_calls[0][1]['Metadata'] + self.assertNotIn('transcript_preferences', metadata) # noqa: PT009 + + +@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) +@override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"}) +class VideoUrlsCsvTestCase( + VideoUploadTestBase, + VideoStudioAccessTestsMixin, + VideoPipelineStudioAccessTestsMixin, + CourseTestCase +): + """Test cases for the CSV download endpoint for video uploads""" + + VIEW_NAME = "video_encodings_download" + + def setUp(self): + super().setUp() + VideoUploadConfig(profile_whitelist="profile1").save() + + def _check_csv_response(self, expected_profiles): + """ + Check that the response is a valid CSV response containing rows + corresponding to previous_uploads and including the expected profiles. + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) # noqa: PT009 + self.assertEqual( # noqa: PT009 + response["Content-Disposition"], + f"attachment; filename=\"{self.course.id.course}_video_urls.csv\"" + ) + response_content = b"".join(response.streaming_content) + response_reader = StringIO(response_content.decode()) + reader = csv.DictReader(response_reader, dialect=csv.excel) + self.assertEqual( # noqa: PT009 + reader.fieldnames, + ( + ["Name", "Duration", "Date Added", "Video ID", "Status"] + + [f"{profile} URL" for profile in expected_profiles] + ) + ) + rows = list(reader) + self.assertEqual(len(rows), len(self.previous_uploads)) # noqa: PT009 + for i, row in enumerate(rows): + response_video = dict(row.items()) + # Videos should be returned by creation date descending + original_video = self.previous_uploads[-(i + 1)] + client_video_id = original_video["client_video_id"] + self.assertEqual(response_video["Name"], client_video_id) # noqa: PT009 + self.assertEqual(response_video["Duration"], str(original_video["duration"])) # noqa: PT009 + dateutil.parser.parse(response_video["Date Added"]) + self.assertEqual(response_video["Video ID"], original_video["edx_video_id"]) # noqa: PT009 + self.assertEqual(response_video["Status"], convert_video_status(original_video)) # noqa: PT009 + for profile in expected_profiles: + response_profile_url = response_video[f"{profile} URL"] + original_encoded_for_profile = next( + ( + original_encoded + for original_encoded in original_video["encoded_videos"] + if original_encoded["profile"] == profile + ), + None + ) + if original_encoded_for_profile: + original_encoded_for_profile_url = original_encoded_for_profile["url"] + self.assertEqual(response_profile_url, original_encoded_for_profile_url) # noqa: PT009 + else: + self.assertEqual(response_profile_url, "") # noqa: PT009 + + def test_basic(self): + self._check_csv_response(["profile1"]) + + def test_profile_whitelist(self): + VideoUploadConfig(profile_whitelist="profile1,profile2").save() + self._check_csv_response(["profile1", "profile2"]) + + def test_non_ascii_course(self): + course = CourseFactory.create( + number="nón-äscii", + video_upload_pipeline={ + "course_video_upload_token": self.test_token, + } + ) + response = self.client.get(self.get_url_for_course_key(course.id)) + self.assertEqual(response.status_code, 200) # noqa: PT009 + self.assertEqual( # noqa: PT009 + response["Content-Disposition"], + "attachment; filename*=utf-8''n%C3%B3n-%C3%A4scii_video_urls.csv" + ) + + +@ddt.ddt +class GetVideoFeaturesTestCase( + CourseTestCase +): + """Test cases for the get_video_features endpoint """ + def setUp(self): + super().setUp() + self.url = self.get_url_for_course_key() + + def get_url_for_course_key(self): + """ Helper to generate a url for a course key """ + return reverse("video_features") + + def test_basic(self): + """ Test for expected return keys """ + response = self.client.get(self.get_url_for_course_key()) + self.assertEqual(response.status_code, 200) # noqa: PT009 + self.assertEqual( # noqa: PT009 + set(response.json().keys()), + { + 'videoSharingEnabled', + 'allowThumbnailUpload', + } + ) + + @ddt.data(True, False) + def test_video_share_enabled(self, is_enabled): + """ Test the public video share flag """ + self._test_video_feature( + PUBLIC_VIDEO_SHARE, + 'videoSharingEnabled', + override_waffle_flag, + is_enabled, + ) + + @ddt.data(True, False) + def test_video_image_upload_enabled(self, is_enabled): + """ Test the video image upload switch """ + self._test_video_feature( + VIDEO_IMAGE_UPLOAD_ENABLED, + 'allowThumbnailUpload', + override_waffle_switch, + is_enabled, + ) + + def _test_video_feature(self, flag, key, override_fn, is_enabled): + """ Test that setting a waffle flag or switch on or off will cause the expected result """ + with override_fn(flag, is_enabled): + response = self.client.get(self.get_url_for_course_key()) + + self.assertEqual(response.status_code, 200) # noqa: PT009 + self.assertEqual(response.json()[key], is_enabled) # noqa: PT009 + + +class GetStorageBucketTestCase(TestCase): + """ This test just check that connection works and returns the bucket. + It does not involve any mocking and triggers errors if has any import issue. + """ + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @override_settings(VIDEO_UPLOAD_PIPELINE={ + "VEM_S3_BUCKET": "vem_test_bucket", "BUCKET": "test_bucket", "ROOT_PATH": "test_root" + }) + @patch('cms.djangoapps.contentstore.video_storage_handlers.boto3.resource') + def test_storage_bucket(self, mock_boto3_resource): + """ Test that storage service functions work correctly with boto3.""" + # Setup boto3 mocks + mock_s3_client, mock_bucket, mock_s3_resource = setup_s3_mocks(mock_boto3_resource, 'vem_test_bucket') + + # Test storage_service_bucket function + bucket = storage_service_bucket() + self.assertEqual(bucket.name, 'vem_test_bucket') # noqa: PT009 + mock_s3_resource.Bucket.assert_called_once_with('vem_test_bucket') + + # Test storage_service_key function + edx_video_id = 'dummy_video' + key_name = storage_service_key(bucket, file_name=edx_video_id) + expected_key = 'test_root/dummy_video' + self.assertEqual(key_name, expected_key) # noqa: PT009 + + # Test that we can generate presigned URL using the bucket's client + mock_s3_client.generate_presigned_url.return_value = ( + 'https://vem_test_bucket.s3.amazonaws.com:443/test_root/dummy_video?signature=test' + ) + upload_url = mock_s3_client.generate_presigned_url( + 'put_object', + Params={ + 'Bucket': bucket.name, + 'Key': key_name, + 'ContentType': 'video/mp4' + }, + ExpiresIn=KEY_EXPIRATION_IN_SECONDS + ) + + self.assertIn("vem_test_bucket.s3.amazonaws.com", upload_url) # noqa: PT009 + self.assertIn("test_root/dummy_video", upload_url) # noqa: PT009 + + +class CourseYoutubeEdxVideoIds(ModuleStoreTestCase): + """ + This test checks youtube videos in a course + """ + VIEW_NAME = 'youtube_edx_video_ids' + + def setUp(self): + super().setUp() + self.course = CourseFactory.create() + self.course_with_no_youtube_videos = CourseFactory.create() + self.store = modulestore() + self.user = UserFactory() + self.client.login(username=self.user.username, password='Password1234') + + def get_url_for_course_key(self, course_key, kwargs=None): + """Return video handler URL for the given course""" + return reverse_course_url(self.VIEW_NAME, course_key, kwargs) # lint-amnesty, pylint: disable=no-member + + def test_course_with_youtube_videos(self): + course_key = self.course.id + + with self.store.bulk_operations(course_key): + chapter_loc = self.store.create_child( + self.user.id, self.course.location, 'chapter', 'test_chapter' + ).location + seq_loc = self.store.create_child( + self.user.id, chapter_loc, 'sequential', 'test_seq' + ).location + vert_loc = self.store.create_child(self.user.id, seq_loc, 'vertical', 'test_vert').location + self.store.create_child( + self.user.id, + vert_loc, + 'problem', + 'test_problem', + fields={"data": "Test"} + ) + self.store.create_child( + self.user.id, vert_loc, 'video', fields={ + "youtube_is_available": False, + "name": "sample_video", + "edx_video_id": "youtube_193_84709099", + } + ) + + response = self.client.get(self.get_url_for_course_key(course_key)) + self.assertEqual(response.status_code, 200) # noqa: PT009 + + edx_video_ids = json.loads(response.content.decode('utf-8'))['edx_video_ids'] + self.assertEqual(len(edx_video_ids), 1) # noqa: PT009 + + def test_course_with_no_youtube_videos(self): + course_key = self.course_with_no_youtube_videos.id + + with self.store.bulk_operations(course_key): + chapter_loc = self.store.create_child( + self.user.id, self.course_with_no_youtube_videos.location, 'chapter', 'test_chapter' + ).location + seq_loc = self.store.create_child( + self.user.id, chapter_loc, 'sequential', 'test_seq' + ).location + vert_loc = self.store.create_child(self.user.id, seq_loc, 'vertical', 'test_vert').location + self.store.create_child( + self.user.id, vert_loc, 'problem', 'test_problem', fields={"data": "Test"} + ) + self.store.create_child( + self.user.id, vert_loc, 'video', fields={ + "youtube_id_1_0": None, + "name": "sample_video", + "edx_video_id": "no_youtube_193_84709099", + } + ) + + response = self.client.get(self.get_url_for_course_key(course_key)) + + edx_video_ids = json.loads(response.content.decode('utf-8'))['edx_video_ids'] + self.assertEqual(response.status_code, 200) # noqa: PT009 + self.assertEqual(len(edx_video_ids), 0) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/views/transcripts_ajax.py b/cms/djangoapps/contentstore/views/transcripts_ajax.py index 231676df80a8..290bd06b180e 100644 --- a/cms/djangoapps/contentstore/views/transcripts_ajax.py +++ b/cms/djangoapps/contentstore/views/transcripts_ajax.py @@ -521,7 +521,7 @@ def _validate_transcripts_data(request): try: item = _get_item(request, data) except (InvalidKeyError, ItemNotFoundError): - raise TranscriptsRequestValidationException(_("Can't find item by locator.")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise TranscriptsRequestValidationException(_("Can't find item by locator.")) # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if item.usage_key.block_type != 'video': raise TranscriptsRequestValidationException(_('Transcripts are supported only for "video" blocks.')) diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index e3120c1173de..d9e84e0d781d 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -1596,7 +1596,7 @@ def safe_get_username(user_id): if user_id: try: return User.objects.get(id=user_id).username - except: # pylint: disable=bare-except + except: # noqa: E722 pass return None diff --git a/cms/djangoapps/course_creators/admin.py b/cms/djangoapps/course_creators/admin.py index c02bd99bbccf..b93bb9c07f08 100644 --- a/cms/djangoapps/course_creators/admin.py +++ b/cms/djangoapps/course_creators/admin.py @@ -158,7 +158,7 @@ def send_user_notification_callback(sender, **kwargs): # pylint: disable=unused try: user.email_user(subject, message, studio_request_email) - except: # lint-amnesty, pylint: disable=bare-except + except: # noqa: E722 log.warning("Unable to send course creator status e-mail to %s", user.email) diff --git a/cms/djangoapps/course_creators/tests/test_admin.py b/cms/djangoapps/course_creators/tests/test_admin.py index 28b9a6a61862..10eea935320d 100644 --- a/cms/djangoapps/course_creators/tests/test_admin.py +++ b/cms/djangoapps/course_creators/tests/test_admin.py @@ -123,7 +123,7 @@ def check_admin_message_state(state, expect_sent_to_admin, expect_sent_to_user): if expect_sent_to_admin: context = {'user_name': 'test_user', 'user_email': 'test_user+courses@edx.org'} - self.assertEqual(base_num_emails + 1, len(mail.outbox), 'Expected admin message to be sent') # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(base_num_emails + 1, len(mail.outbox), 'Expected admin message to be sent') # noqa: PT009 sent_mail = mail.outbox[base_num_emails] self.assertEqual( # noqa: PT009 mock_render_to_string('emails/course_creator_admin_subject.txt', context), diff --git a/cms/djangoapps/export_course_metadata/toggles.py b/cms/djangoapps/export_course_metadata/toggles.py index 9eca63c6ef19..e0148091f63c 100644 --- a/cms/djangoapps/export_course_metadata/toggles.py +++ b/cms/djangoapps/export_course_metadata/toggles.py @@ -12,4 +12,4 @@ # .. toggle_creation_date: 2021-03-01 # .. toggle_target_removal_date: None # .. toggle_tickets: AA-461 -EXPORT_COURSE_METADATA_FLAG = WaffleFlag('cms.export_course_metadata', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation +EXPORT_COURSE_METADATA_FLAG = WaffleFlag('cms.export_course_metadata', __name__) # pylint: disable=toggle-missing-annotation diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index e6a0e3690c5c..da5a49677054 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -275,7 +275,7 @@ def get_section_grader_type(location): } @staticmethod - def update_section_grader_type(block, grader_type, user): # lint-amnesty, pylint: disable=missing-function-docstring + def update_section_grader_type(block, grader_type, user): # pylint: disable=missing-function-docstring if grader_type is not None and grader_type != 'notgraded': block.format = grader_type block.graded = True diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 402bf834da63..86a49d20bfc2 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -232,7 +232,7 @@ def update_from_json(cls, block, jsondict, user, filter_tabs=True): else: key_values[key] = block.fields[key].from_json(val) except (TypeError, ValueError) as err: - raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}").format( # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}").format( # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 name=model['display_name'], detailed_message=str(err))) return cls.update_from_dict(key_values, block, user) diff --git a/cms/djangoapps/modulestore_migrator/tests/test_tasks.py b/cms/djangoapps/modulestore_migrator/tests/test_tasks.py index ae4ad1548937..449ed96c943e 100644 --- a/cms/djangoapps/modulestore_migrator/tests/test_tasks.py +++ b/cms/djangoapps/modulestore_migrator/tests/test_tasks.py @@ -311,7 +311,7 @@ def test_bulk_migrate_invalid_sources(self): status = UserTaskStatus.objects.get(task_id=task.id) self.assertEqual(status.state, UserTaskStatus.FAILED) # noqa: PT009 - self.assertEqual(self._get_task_status_fail_message(status), "ModulestoreSource matching query does not exist.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(self._get_task_status_fail_message(status), "ModulestoreSource matching query does not exist.") # noqa: PT009 def test_bulk_migrate_invalid_collection(self): """ @@ -336,7 +336,7 @@ def test_bulk_migrate_invalid_collection(self): status = UserTaskStatus.objects.get(task_id=task.id) self.assertEqual(status.state, UserTaskStatus.FAILED) # noqa: PT009 - self.assertEqual(self._get_task_status_fail_message(status), "Collection matching query does not exist.") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(self._get_task_status_fail_message(status), "Collection matching query does not exist.") # noqa: PT009 def test_bulk_migration_task_calculate_total_steps(self): """ @@ -803,7 +803,7 @@ def test_migrate_container_different_container_types(self): container_version = result.containerversion self.assertEqual(container_version.title, f"Test {block_type.title()}") # noqa: PT009 # The container is published - self.assertFalse(content_api.contains_unpublished_changes(container_version.container.pk)) # noqa: PT009 # pylint: disable=line-too-long + self.assertFalse(content_api.contains_unpublished_changes(container_version.container.pk)) # noqa: PT009 def test_migrate_container_same_title(self): """ diff --git a/cms/envs/common.py b/cms/envs/common.py index 3c9ada1bb4e3..9bffd9e64064 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1224,9 +1224,7 @@ def make_lms_template_path(settings): DISCUSSIONS_INCONTEXT_FEEDBACK_URL = '' # Learn More link in upgraded discussion notification alert -# pylint: disable=line-too-long DISCUSSIONS_INCONTEXT_LEARNMORE_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_discussions.html" -# pylint: enable=line-too-long #### Event bus producing #### diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 6dd89f5e36ec..67fd82eac713 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -1,4 +1,4 @@ -# ruff: noqa: I001 - settings file: star-import order is semantically significant + """ Specific overrides to the base prod settings to make development easier. """ @@ -277,7 +277,7 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing SOCIAL_AUTH_EDX_OAUTH2_KEY = 'studio-sso-key' SOCIAL_AUTH_EDX_OAUTH2_SECRET = 'studio-sso-secret' # in stage, prod would be high-entropy secret # routed internally server-to-server -SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = ENV_TOKENS.get('SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT', 'http://edx.devstack.lms:18000') # noqa: F405 # pylint: disable=line-too-long +SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = ENV_TOKENS.get('SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT', 'http://edx.devstack.lms:18000') # noqa: F405 SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = 'http://localhost:18000' # used in browser redirect # Don't form the return redirect URL with HTTPS on devstack @@ -317,14 +317,14 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing EVENT_BUS_TOPIC_PREFIX = 'dev' EVENT_BUS_CONSUMER = 'edx_event_bus_redis.RedisEventConsumer' -course_catalog_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.course.catalog_info.changed.v1'] # noqa: F405 # pylint: disable=line-too-long +course_catalog_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.course.catalog_info.changed.v1'] # noqa: F405 course_catalog_event_setting['course-catalog-info-changed']['enabled'] = True -xblock_published_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.published.v1'] # noqa: F405 # pylint: disable=line-too-long +xblock_published_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.published.v1'] # noqa: F405 xblock_published_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True -xblock_deleted_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.deleted.v1'] # noqa: F405 # pylint: disable=line-too-long +xblock_deleted_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.deleted.v1'] # noqa: F405 xblock_deleted_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True -xblock_duplicated_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.duplicated.v1'] # noqa: F405 # pylint: disable=line-too-long +xblock_duplicated_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.duplicated.v1'] # noqa: F405 xblock_duplicated_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True diff --git a/cms/envs/docker-production.py b/cms/envs/docker-production.py index 4096ca6b11f6..2590329c19cc 100644 --- a/cms/envs/docker-production.py +++ b/cms/envs/docker-production.py @@ -1,4 +1,4 @@ -# ruff: noqa: I001 - settings file: star-import order is semantically significant + """ Specific overrides to the base prod settings for a docker production deployment. """ diff --git a/cms/envs/test.py b/cms/envs/test.py index b4d32a8aecc5..e499252a10b1 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -211,7 +211,7 @@ ############## openedx_content config ############## OPENEDX_LEARNING = { - "MEDIA": {"BACKEND": "django.core.files.storage.InMemoryStorage", "OPTIONS": {"location": MEDIA_ROOT + "_private"}} # noqa: F405 # pylint: disable=line-too-long + "MEDIA": {"BACKEND": "django.core.files.storage.InMemoryStorage", "OPTIONS": {"location": MEDIA_ROOT + "_private"}} # noqa: F405 } diff --git a/cms/lib/xblock/tagging/tagging.py b/cms/lib/xblock/tagging/tagging.py index a8796c4c5b05..e69afaf689a7 100644 --- a/cms/lib/xblock/tagging/tagging.py +++ b/cms/lib/xblock/tagging/tagging.py @@ -95,7 +95,7 @@ def save_tags(self, request=None, suffix=None): # lint-amnesty, pylint: disable for posted_tag_value in posted_data[av_tag.name]: if posted_tag_value not in tag_available_values and posted_tag_value not in tag_current_values: - return Response("Invalid tag value was passed: %s" % posted_tag_value, status=400) # noqa: UP031 # pylint: disable=line-too-long + return Response("Invalid tag value was passed: %s" % posted_tag_value, status=400) # noqa: UP031 saved_tags[av_tag.name] = posted_data[av_tag.name] need_update = True diff --git a/cms/lib/xblock/tagging/test.py b/cms/lib/xblock/tagging/test.py index 1ba55a774b9b..4b65899ab943 100644 --- a/cms/lib/xblock/tagging/test.py +++ b/cms/lib/xblock/tagging/test.py @@ -131,7 +131,7 @@ def test_aside_contains_tags(self): runtime = TestRuntime(services={'field-data': field_data}) xblock_aside = StructuredTagsAside(scope_ids=sids, runtime=runtime) available_tags = xblock_aside.get_available_tags() - self.assertEqual(len(available_tags), 2, "StructuredTagsAside should contains two tag categories") # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(len(available_tags), 2, "StructuredTagsAside should contains two tag categories") # noqa: PT009 def test_preview_html(self): """ @@ -184,7 +184,7 @@ def test_preview_html(self): self.assertEqual(len(option_nodes2), 3) # noqa: PT009 option_values2 = [opt_elem.text for opt_elem in option_nodes2 if opt_elem.text] - self.assertEqual(option_values2, ['Learned a few things', 'Learned everything', 'Learned nothing']) # noqa: PT009 # pylint: disable=line-too-long + self.assertEqual(option_values2, ['Learned a few things', 'Learned everything', 'Learned nothing']) # noqa: PT009 # Now ensure the acid_aside is not in the result self.assertNotRegex(problem_html, r"data-block-type=[\"\']acid_aside[\"\']") # noqa: PT009 diff --git a/cms/lib/xblock/test/test_authoring_mixin.py b/cms/lib/xblock/test/test_authoring_mixin.py index 428adf2dcd4d..4a6055432d41 100644 --- a/cms/lib/xblock/test/test_authoring_mixin.py +++ b/cms/lib/xblock/test/test_authoring_mixin.py @@ -26,8 +26,8 @@ class AuthoringMixinTestCase(ModuleStoreTestCase): MODULESTORE = TEST_DATA_SPLIT_MODULESTORE GROUP_NO_LONGER_EXISTS = "This group no longer exists" NO_CONTENT_OR_ENROLLMENT_GROUPS = "Access to this component is not restricted" - NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "You can restrict access to this component to learners in specific enrollment tracks or content groups" # lint-amnesty, pylint: disable=line-too-long - NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "You can restrict access to this component to learners in specific content groups" # lint-amnesty, pylint: disable=line-too-long + NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "You can restrict access to this component to learners in specific enrollment tracks or content groups" # noqa: E501 + NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "You can restrict access to this component to learners in specific content groups" # noqa: E501 CONTENT_GROUPS_TITLE = "Content Groups" ENROLLMENT_GROUPS_TITLE = "Enrollment Track Groups" STAFF_LOCKED = 'The unit that contains this component is hidden from learners' diff --git a/cms/urls.py b/cms/urls.py index 90b295f92804..1a46ce15f916 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -344,7 +344,7 @@ # pylint: disable=wrong-import-position, wrong-import-order from edx_django_utils.plugins import get_plugin_url_patterns # noqa: I001 - must be after urlpatterns are built # pylint: disable=wrong-import-position -from openedx.core.djangoapps.plugins.constants import ProjectType # noqa: I001 - must be after urlpatterns are built +from openedx.core.djangoapps.plugins.constants import ProjectType urlpatterns.extend(get_plugin_url_patterns(ProjectType.CMS)) diff --git a/cms/wsgi.py b/cms/wsgi.py index 10a8292b7e47..7f545b28a1e8 100644 --- a/cms/wsgi.py +++ b/cms/wsgi.py @@ -20,5 +20,5 @@ # This application object is used by the development server # as well as any WSGI server configured to use this file. -from django.core.wsgi import get_wsgi_application # lint-amnesty, pylint: disable=wrong-import-order, wrong-import-position +from django.core.wsgi import get_wsgi_application # pylint: disable=wrong-import-order, wrong-import-position application = get_wsgi_application() diff --git a/common/djangoapps/course_action_state/managers.py b/common/djangoapps/course_action_state/managers.py index 58ce34db1fbd..5a172aa476ac 100644 --- a/common/djangoapps/course_action_state/managers.py +++ b/common/djangoapps/course_action_state/managers.py @@ -74,7 +74,7 @@ def update_state( state_object.created_user = user else: raise CourseActionStateItemNotFoundError( - "Cannot update non-existent entry for course_key {course_key} and action {action}".format( # noqa: UP032 # pylint: disable=line-too-long + "Cannot update non-existent entry for course_key {course_key} and action {action}".format( # noqa: UP032 action=self.ACTION, course_key=course_key, )) diff --git a/common/djangoapps/course_action_state/tests/test_rerun_manager.py b/common/djangoapps/course_action_state/tests/test_rerun_manager.py index ad00aa755eb8..d2183cae7a1f 100644 --- a/common/djangoapps/course_action_state/tests/test_rerun_manager.py +++ b/common/djangoapps/course_action_state/tests/test_rerun_manager.py @@ -96,7 +96,7 @@ def test_rerun_failed(self): exception = Exception("failure in rerunning") try: raise exception - except: # lint-amnesty, pylint: disable=bare-except + except: # noqa: E722 CourseRerunState.objects.failed(course_key=self.course_key) self.expected_rerun_state.update( diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index d07200735115..a2c4e0c5dbb6 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -255,13 +255,13 @@ def clean(self): mode_display_name = mode_config.get('display_name', self.mode_slug) raise ValidationError( _( # lint-amnesty, pylint: disable=translation-of-non-string - "The {course_mode} course mode has a minimum price of {min_price}. You must set a price greater than or equal to {min_price}.".format( # lint-amnesty, pylint: disable=line-too-long + "The {course_mode} course mode has a minimum price of {min_price}. You must set a price greater than or equal to {min_price}.".format( # noqa: E501 course_mode=mode_display_name, min_price=min_price_for_mode ) ) ) - def save(self, force_insert=False, force_update=False, using=None): # lint-amnesty, pylint: disable=arguments-differ # noqa: DJ012 + def save(self, force_insert=False, force_update=False, using=None): # lint-amnesty, pylint: disable=arguments-differ # noqa: DJ012, E501 # Ensure currency is always lowercase. self.clean() # ensure object-level validation is performed before we save. self.currency = self.currency.lower() diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 46d01c4b3638..f9fd42c4d98a 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -347,7 +347,7 @@ def test_invalid_mode_expiration(self, mode_slug, exp_dt_name): assert not is_error_expected, 'Expected a ValidationError to be thrown.' except ValidationError as exc: assert is_error_expected, 'Did not expect a ValidationError to be thrown.' - assert exc.messages == ['Professional education modes are not allowed to have expiration_datetime set.'] # noqa: PT017 # pylint: disable=line-too-long + assert exc.messages == ['Professional education modes are not allowed to have expiration_datetime set.'] # noqa: PT017 @ddt.data( "verified", diff --git a/common/djangoapps/entitlements/admin.py b/common/djangoapps/entitlements/admin.py index 789490c7f42f..36c0948f7be5 100644 --- a/common/djangoapps/entitlements/admin.py +++ b/common/djangoapps/entitlements/admin.py @@ -36,7 +36,7 @@ def __init__(self, *args, **kwargs): try: self.data['unenrolled_run'] = CourseKey.from_string(self.data['unenrolled_run']) except InvalidKeyError: - raise forms.ValidationError("No valid CourseKey for id {}!".format(self.data['unenrolled_run'])) # lint-amnesty, pylint: disable=raise-missing-from,line-too-long # noqa: B904 + raise forms.ValidationError("No valid CourseKey for id {}!".format(self.data['unenrolled_run'])) # pylint: disable=raise-missing-from # noqa: B904 def clean_course_id(self): """Cleans course id and attempts to make course key from string version of key""" @@ -44,7 +44,7 @@ def clean_course_id(self): try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: - raise forms.ValidationError(f"Cannot make a valid CourseKey from id {course_id}!") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 + raise forms.ValidationError(f"Cannot make a valid CourseKey from id {course_id}!") # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904, E501 if not modulestore().has_course(course_key): raise forms.ValidationError(f"Cannot find course with id {course_id} in the modulestore") diff --git a/common/djangoapps/entitlements/migrations/0001_initial.py b/common/djangoapps/entitlements/migrations/0001_initial.py index 5b27fc4cbc9d..6f820cc33a9b 100644 --- a/common/djangoapps/entitlements/migrations/0001_initial.py +++ b/common/djangoapps/entitlements/migrations/0001_initial.py @@ -18,14 +18,14 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- name='CourseEntitlement', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), # lint-amnesty, pylint: disable=line-too-long - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), # lint-amnesty, pylint: disable=line-too-long + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), ('course_uuid', models.UUIDField()), ('expired_at', models.DateTimeField(null=True)), ('mode', models.CharField(default='audit', max_length=100)), ('order_number', models.CharField(max_length=128, null=True)), - ('enrollment_course_run', models.ForeignKey(to='student.CourseEnrollment', null=True, on_delete=models.CASCADE)), # lint-amnesty, pylint: disable=line-too-long + ('enrollment_course_run', models.ForeignKey(to='student.CourseEnrollment', null=True, on_delete=models.CASCADE)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ diff --git a/common/djangoapps/entitlements/migrations/0002_auto_20171102_0719.py b/common/djangoapps/entitlements/migrations/0002_auto_20171102_0719.py index 7cb69d3456db..5321ab7c8414 100644 --- a/common/djangoapps/entitlements/migrations/0002_auto_20171102_0719.py +++ b/common/djangoapps/entitlements/migrations/0002_auto_20171102_0719.py @@ -19,12 +19,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='courseentitlement', name='enrollment_course_run', - field=models.ForeignKey(to='student.CourseEnrollment', help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=models.CASCADE), # lint-amnesty, pylint: disable=line-too-long + field=models.ForeignKey(to='student.CourseEnrollment', help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=models.CASCADE), ), migrations.AlterField( model_name='courseentitlement', name='expired_at', - field=models.DateTimeField(help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True), # lint-amnesty, pylint: disable=line-too-long + field=models.DateTimeField(help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True), ), migrations.AlterField( model_name='courseentitlement', diff --git a/common/djangoapps/entitlements/migrations/0003_auto_20171205_1431.py b/common/djangoapps/entitlements/migrations/0003_auto_20171205_1431.py index 59e5cc2db7c5..89ac51812266 100644 --- a/common/djangoapps/entitlements/migrations/0003_auto_20171205_1431.py +++ b/common/djangoapps/entitlements/migrations/0003_auto_20171205_1431.py @@ -15,25 +15,25 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- name='CourseEntitlementPolicy', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('expiration_period', models.DurationField(default=datetime.timedelta(450), help_text='Duration in days from when an entitlement is created until when it is expired.')), # lint-amnesty, pylint: disable=line-too-long - ('refund_period', models.DurationField(default=datetime.timedelta(60), help_text='Duration in days from when an entitlement is created until when it is no longer refundable')), # lint-amnesty, pylint: disable=line-too-long - ('regain_period', models.DurationField(default=datetime.timedelta(14), help_text='Duration in days from when an entitlement is redeemed for a course run until it is no longer able to be regained by a user.')), # lint-amnesty, pylint: disable=line-too-long + ('expiration_period', models.DurationField(default=datetime.timedelta(450), help_text='Duration in days from when an entitlement is created until when it is expired.')), + ('refund_period', models.DurationField(default=datetime.timedelta(60), help_text='Duration in days from when an entitlement is created until when it is no longer refundable')), + ('regain_period', models.DurationField(default=datetime.timedelta(14), help_text='Duration in days from when an entitlement is redeemed for a course run until it is no longer able to be regained by a user.')), ('site', models.ForeignKey(to='sites.Site', on_delete=models.CASCADE)), ], ), migrations.AlterField( model_name='courseentitlement', name='enrollment_course_run', - field=models.ForeignKey(blank=True, to='student.CourseEnrollment', help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=models.CASCADE), # lint-amnesty, pylint: disable=line-too-long + field=models.ForeignKey(blank=True, to='student.CourseEnrollment', help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=models.CASCADE), ), migrations.AlterField( model_name='courseentitlement', name='expired_at', - field=models.DateTimeField(help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True, blank=True), # lint-amnesty, pylint: disable=line-too-long + field=models.DateTimeField(help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True, blank=True), ), migrations.AddField( model_name='courseentitlement', name='_policy', - field=models.ForeignKey(blank=True, to='entitlements.CourseEntitlementPolicy', null=True, on_delete=models.CASCADE), # lint-amnesty, pylint: disable=line-too-long + field=models.ForeignKey(blank=True, to='entitlements.CourseEntitlementPolicy', null=True, on_delete=models.CASCADE), ), ] diff --git a/common/djangoapps/entitlements/migrations/0005_courseentitlementsupportdetail.py b/common/djangoapps/entitlements/migrations/0005_courseentitlementsupportdetail.py index 1d64eba2829d..e630a14faccf 100644 --- a/common/djangoapps/entitlements/migrations/0005_courseentitlementsupportdetail.py +++ b/common/djangoapps/entitlements/migrations/0005_courseentitlementsupportdetail.py @@ -17,13 +17,13 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- name='CourseEntitlementSupportDetail', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), # lint-amnesty, pylint: disable=line-too-long - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), # lint-amnesty, pylint: disable=line-too-long - ('reason', models.CharField(max_length=15, choices=[('LEAVE', 'Learner requested leave session for expired entitlement'), ('CHANGE', 'Learner requested session change for expired entitlement'), ('LEARNER_NEW', 'Learner requested new entitlement'), ('COURSE_TEAM_NEW', 'Course team requested entitlement for learnerg'), ('OTHER', 'Other')])), # lint-amnesty, pylint: disable=line-too-long + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('reason', models.CharField(max_length=15, choices=[('LEAVE', 'Learner requested leave session for expired entitlement'), ('CHANGE', 'Learner requested session change for expired entitlement'), ('LEARNER_NEW', 'Learner requested new entitlement'), ('COURSE_TEAM_NEW', 'Course team requested entitlement for learnerg'), ('OTHER', 'Other')])), ('comments', models.TextField(null=True)), ('entitlement', models.ForeignKey(to='entitlements.CourseEntitlement', on_delete=models.CASCADE)), ('support_user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), - ('unenrolled_run', models.ForeignKey(db_constraint=False, blank=True, to='course_overviews.CourseOverview', null=True, on_delete=models.CASCADE)), # lint-amnesty, pylint: disable=line-too-long + ('unenrolled_run', models.ForeignKey(db_constraint=False, blank=True, to='course_overviews.CourseOverview', null=True, on_delete=models.CASCADE)), ], options={ 'abstract': False, diff --git a/common/djangoapps/entitlements/migrations/0006_courseentitlementsupportdetail_action.py b/common/djangoapps/entitlements/migrations/0006_courseentitlementsupportdetail_action.py index 397dd3bc0dc2..f6b47c9eefdd 100644 --- a/common/djangoapps/entitlements/migrations/0006_courseentitlementsupportdetail_action.py +++ b/common/djangoapps/entitlements/migrations/0006_courseentitlementsupportdetail_action.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- migrations.AddField( model_name='courseentitlementsupportdetail', name='action', - field=models.CharField(default='CREATE', max_length=15, choices=[('REISSUE', 'Re-issue entitlement'), ('CREATE', 'Create new entitlement')]), # lint-amnesty, pylint: disable=line-too-long + field=models.CharField(default='CREATE', max_length=15, choices=[('REISSUE', 'Re-issue entitlement'), ('CREATE', 'Create new entitlement')]), preserve_default=False, ), ] diff --git a/common/djangoapps/entitlements/migrations/0007_change_expiration_period_default.py b/common/djangoapps/entitlements/migrations/0007_change_expiration_period_default.py index 50509209124a..a6e869089079 100644 --- a/common/djangoapps/entitlements/migrations/0007_change_expiration_period_default.py +++ b/common/djangoapps/entitlements/migrations/0007_change_expiration_period_default.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- migrations.AlterField( model_name='courseentitlementpolicy', name='expiration_period', - field=models.DurationField(default=datetime.timedelta(730), help_text='Duration in days from when an entitlement is created until when it is expired.'), # lint-amnesty, pylint: disable=line-too-long + field=models.DurationField(default=datetime.timedelta(730), help_text='Duration in days from when an entitlement is created until when it is expired.'), ), ] diff --git a/common/djangoapps/entitlements/migrations/0008_auto_20180328_1107.py b/common/djangoapps/entitlements/migrations/0008_auto_20180328_1107.py index edaa38d98613..7389a3a75a2a 100644 --- a/common/djangoapps/entitlements/migrations/0008_auto_20180328_1107.py +++ b/common/djangoapps/entitlements/migrations/0008_auto_20180328_1107.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- migrations.AddField( model_name='courseentitlementpolicy', name='mode', - field=models.CharField(max_length=32, null=True, choices=[(None, '---------'), ('verified', 'verified'), ('professional', 'professional')]), # lint-amnesty, pylint: disable=line-too-long + field=models.CharField(max_length=32, null=True, choices=[(None, '---------'), ('verified', 'verified'), ('professional', 'professional')]), ), migrations.AlterField( model_name='courseentitlementpolicy', diff --git a/common/djangoapps/entitlements/migrations/0011_historicalcourseentitlement.py b/common/djangoapps/entitlements/migrations/0011_historicalcourseentitlement.py index 8fcaf7b5a821..da715b512748 100644 --- a/common/djangoapps/entitlements/migrations/0011_historicalcourseentitlement.py +++ b/common/djangoapps/entitlements/migrations/0011_historicalcourseentitlement.py @@ -24,22 +24,22 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- name='HistoricalCourseEntitlement', fields=[ ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), # lint-amnesty, pylint: disable=line-too-long - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), # lint-amnesty, pylint: disable=line-too-long + ('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')), ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), ('course_uuid', models.UUIDField(help_text='UUID for the Course, not the Course Run')), - ('expired_at', models.DateTimeField(blank=True, help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True)), # lint-amnesty, pylint: disable=line-too-long - ('mode', models.CharField(help_text='The mode of the Course that will be applied on enroll.', max_length=100)), # lint-amnesty, pylint: disable=line-too-long + ('expired_at', models.DateTimeField(blank=True, help_text='The date that an entitlement expired, if NULL the entitlement has not expired.', null=True)), + ('mode', models.CharField(help_text='The mode of the Course that will be applied on enroll.', max_length=100)), ('order_number', models.CharField(max_length=128, null=True)), ('refund_locked', models.BooleanField(default=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), # lint-amnesty, pylint: disable=line-too-long - ('_policy', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='entitlements.CourseEntitlementPolicy')), # lint-amnesty, pylint: disable=line-too-long - ('enrollment_course_run', models.ForeignKey(blank=True, db_constraint=False, help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='student.CourseEnrollment')), # lint-amnesty, pylint: disable=line-too-long - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), # lint-amnesty, pylint: disable=line-too-long - ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), # lint-amnesty, pylint: disable=line-too-long + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('_policy', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='entitlements.CourseEntitlementPolicy')), + ('enrollment_course_run', models.ForeignKey(blank=True, db_constraint=False, help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='student.CourseEnrollment')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ('-history_date', '-history_id'), diff --git a/common/djangoapps/entitlements/migrations/0013_historicalcourseentitlementsupportdetail.py b/common/djangoapps/entitlements/migrations/0013_historicalcourseentitlementsupportdetail.py index f2978a55246a..6a2abda7af52 100644 --- a/common/djangoapps/entitlements/migrations/0013_historicalcourseentitlementsupportdetail.py +++ b/common/djangoapps/entitlements/migrations/0013_historicalcourseentitlementsupportdetail.py @@ -22,19 +22,19 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- name='HistoricalCourseEntitlementSupportDetail', fields=[ ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), # lint-amnesty, pylint: disable=line-too-long - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), # lint-amnesty, pylint: disable=line-too-long - ('reason', models.CharField(choices=[('LEAVE', 'Learner requested leave session for expired entitlement'), ('CHANGE', 'Learner requested session change for expired entitlement'), ('LEARNER_NEW', 'Learner requested new entitlement'), ('COURSE_TEAM_NEW', 'Course team requested entitlement for learnerg'), ('OTHER', 'Other')], max_length=15)), # lint-amnesty, pylint: disable=line-too-long - ('action', models.CharField(choices=[('REISSUE', 'Re-issue entitlement'), ('CREATE', 'Create new entitlement')], max_length=15)), # lint-amnesty, pylint: disable=line-too-long + ('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')), + ('reason', models.CharField(choices=[('LEAVE', 'Learner requested leave session for expired entitlement'), ('CHANGE', 'Learner requested session change for expired entitlement'), ('LEARNER_NEW', 'Learner requested new entitlement'), ('COURSE_TEAM_NEW', 'Course team requested entitlement for learnerg'), ('OTHER', 'Other')], max_length=15)), + ('action', models.CharField(choices=[('REISSUE', 'Re-issue entitlement'), ('CREATE', 'Create new entitlement')], max_length=15)), ('comments', models.TextField(null=True)), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), # lint-amnesty, pylint: disable=line-too-long - ('entitlement', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='entitlements.CourseEntitlement')), # lint-amnesty, pylint: disable=line-too-long - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), # lint-amnesty, pylint: disable=line-too-long - ('support_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), # lint-amnesty, pylint: disable=line-too-long - ('unenrolled_run', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='course_overviews.CourseOverview')), # lint-amnesty, pylint: disable=line-too-long + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('entitlement', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='entitlements.CourseEntitlement')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('support_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('unenrolled_run', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='course_overviews.CourseOverview')), ], options={ 'ordering': ('-history_date', '-history_id'), diff --git a/common/djangoapps/entitlements/migrations/0014_auto_20200115_2022.py b/common/djangoapps/entitlements/migrations/0014_auto_20200115_2022.py index 2cf1ba1643fa..3b34dbcc799f 100644 --- a/common/djangoapps/entitlements/migrations/0014_auto_20200115_2022.py +++ b/common/djangoapps/entitlements/migrations/0014_auto_20200115_2022.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): # lint-amnesty, pylint: disable=missing- migrations.AlterField( model_name='courseentitlementsupportdetail', name='unenrolled_run', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview'), # lint-amnesty, pylint: disable=line-too-long + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview'), ), ] diff --git a/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py b/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py index 9743a7021f1e..cc521652f416 100644 --- a/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py +++ b/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py @@ -32,7 +32,7 @@ # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection if settings.ROOT_URLCONF == 'lms.urls': - from common.djangoapps.entitlements.models import ( # lint-amnesty, pylint: disable=line-too-long + from common.djangoapps.entitlements.models import ( CourseEntitlement, CourseEntitlementPolicy, CourseEntitlementSupportDetail, @@ -658,7 +658,7 @@ def test_get_entitlement_by_uuid(self): assert response.status_code == 200 results = response.data - assert results == CourseEntitlementSerializer(entitlement).data and results.get('expired_at') is None # noqa: PT018 # pylint: disable=line-too-long + assert results == CourseEntitlementSerializer(entitlement).data and results.get('expired_at') is None # noqa: PT018 def test_get_expired_entitlement_by_uuid(self): past_datetime = now() - timedelta(days=365 * 2) @@ -1148,7 +1148,7 @@ def test_user_can_revoke_and_refund(self, mock_course_uuid, mock_get_course_runs assert course_entitlement.enrollment_course_run is None assert course_entitlement.expired_at is not None - @patch('common.djangoapps.entitlements.rest_api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=False) # lint-amnesty, pylint: disable=line-too-long + @patch('common.djangoapps.entitlements.rest_api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=False) # noqa: E501 @patch('common.djangoapps.entitlements.models.refund_entitlement', return_value=True) @patch('common.djangoapps.entitlements.rest_api.v1.views.get_course_runs_for_course') def test_user_can_revoke_and_no_refund_available( @@ -1192,7 +1192,7 @@ def test_user_can_revoke_and_no_refund_available( assert course_entitlement.enrollment_course_run is not None assert course_entitlement.expired_at is None - @patch('common.djangoapps.entitlements.rest_api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=True) # lint-amnesty, pylint: disable=line-too-long + @patch('common.djangoapps.entitlements.rest_api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=True) # noqa: E501 @patch('common.djangoapps.entitlements.models.refund_entitlement', return_value=False) @patch("common.djangoapps.entitlements.rest_api.v1.views.get_course_runs_for_course") def test_user_is_not_unenrolled_on_failed_refund( diff --git a/common/djangoapps/entitlements/rest_api/v1/views.py b/common/djangoapps/entitlements/rest_api/v1/views.py index 08d9ecc38e17..62fb6aaeb742 100644 --- a/common/djangoapps/entitlements/rest_api/v1/views.py +++ b/common/djangoapps/entitlements/rest_api/v1/views.py @@ -17,7 +17,7 @@ from rest_framework.response import Response from common.djangoapps.course_modes.models import CourseMode -from common.djangoapps.entitlements.models import ( # lint-amnesty, pylint: disable=line-too-long +from common.djangoapps.entitlements.models import ( CourseEntitlement, CourseEntitlementPolicy, CourseEntitlementSupportDetail, @@ -300,12 +300,12 @@ def partial_update(self, request, *args, **kwargs): support_detail['unenrolled_run'] = CourseOverview.objects.get(id=unenrolled_run_course_key) except (InvalidKeyError, CourseOverview.DoesNotExist) as error: return HttpResponseBadRequest( - 'Error raised while trying to unenroll user {user} from course run {course_id}: {error}' # noqa: UP032 # pylint: disable=line-too-long + 'Error raised while trying to unenroll user {user} from course run {course_id}: {error}' # noqa: UP032 .format(user=entitlement.user.username, course_id=unenrolled_run_id, error=error) ) CourseEntitlementSupportDetail.objects.create(**support_detail) - return super().partial_update(request, *args, **kwargs) # lint-amnesty, pylint: disable=no-member, super-with-arguments + return super().partial_update(request, *args, **kwargs) # pylint: disable=no-member, super-with-arguments class EntitlementEnrollmentViewSet(viewsets.GenericViewSet): @@ -430,7 +430,7 @@ def create(self, request, uuid): return Response( status=status.HTTP_400_BAD_REQUEST, data={ - 'message': 'The User is unable to enroll in Course Run {course_id}, it is not available.'.format( # noqa: UP032 # pylint: disable=line-too-long + 'message': 'The User is unable to enroll in Course Run {course_id}, it is not available.'.format( # noqa: UP032 course_id=course_run_id ) } diff --git a/common/djangoapps/entitlements/tasks.py b/common/djangoapps/entitlements/tasks.py index 74229dcb1437..4e2821311d12 100644 --- a/common/djangoapps/entitlements/tasks.py +++ b/common/djangoapps/entitlements/tasks.py @@ -65,7 +65,7 @@ def expire_old_entitlements(self, start, end, logid='...'): # The call above is idempotent, so retry at will raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES) # noqa: B904 - LOGGER.info('Successfully completed the task expire_old_entitlements after examining %d entries [%s]', entitlements.count(), logid) # lint-amnesty, pylint: disable=line-too-long + LOGGER.info('Successfully completed the task expire_old_entitlements after examining %d entries [%s]', entitlements.count(), logid) # noqa: E501 @shared_task(bind=True) diff --git a/common/djangoapps/entitlements/tests/factories.py b/common/djangoapps/entitlements/tests/factories.py index e4c5a86ad3f7..85bf666a1f1e 100644 --- a/common/djangoapps/entitlements/tests/factories.py +++ b/common/djangoapps/entitlements/tests/factories.py @@ -22,7 +22,7 @@ class Meta: site = factory.SubFactory(SiteFactory) -class CourseEntitlementFactory(factory.django.DjangoModelFactory): # lint-amnesty, pylint: disable=missing-class-docstring +class CourseEntitlementFactory(factory.django.DjangoModelFactory): # pylint: disable=missing-class-docstring class Meta: model = CourseEntitlement diff --git a/common/djangoapps/entitlements/tests/test_utils.py b/common/djangoapps/entitlements/tests/test_utils.py index 0be720b2aa57..6e7f85f07b94 100644 --- a/common/djangoapps/entitlements/tests/test_utils.py +++ b/common/djangoapps/entitlements/tests/test_utils.py @@ -11,7 +11,7 @@ from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory -from common.djangoapps.student.tests.factories import ( # lint-amnesty, pylint: disable=line-too-long +from common.djangoapps.student.tests.factories import ( TEST_PASSWORD, CourseEnrollmentFactory, CourseOverviewFactory, diff --git a/common/djangoapps/pipeline_mako/tests/test_render.py b/common/djangoapps/pipeline_mako/tests/test_render.py index 8e37e8e2f99a..7e9afd7e8414 100644 --- a/common/djangoapps/pipeline_mako/tests/test_render.py +++ b/common/djangoapps/pipeline_mako/tests/test_render.py @@ -53,7 +53,7 @@ def mock_staticfiles_lookup(path): (True,), (False,), ) - def test_compressed_css(self, pipeline_enabled, mock_staticfiles_lookup): # lint-amnesty, pylint: disable=unused-argument + def test_compressed_css(self, pipeline_enabled, mock_staticfiles_lookup): # pylint: disable=unused-argument """ Verify the behavior of compressed_css, with the pipeline both enabled and disabled. @@ -71,7 +71,7 @@ def test_compressed_css(self, pipeline_enabled, mock_staticfiles_lookup): # lin @patch('django.contrib.staticfiles.storage.staticfiles_storage.exists', return_value=True) @patch('common.djangoapps.static_replace.try_staticfiles_lookup', side_effect=mock_staticfiles_lookup) - def test_compressed_js(self, mock_staticfiles_lookup, mock_staticfiles_exists): # lint-amnesty, pylint: disable=unused-argument + def test_compressed_js(self, mock_staticfiles_lookup, mock_staticfiles_exists): # pylint: disable=unused-argument """ Verify the behavior of compressed_css, with the pipeline both enabled and disabled. diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index 1d6cc6fe43f5..10b924b637dd 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -151,7 +151,7 @@ def test_mongo_filestore(mock_get_excluded_extensions, mock_get_base_url, mock_m @patch('common.djangoapps.static_replace.settings', autospec=True) @patch('xmodule.modulestore.django.modulestore', autospec=True) @patch('common.djangoapps.static_replace.staticfiles_storage', autospec=True) -def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): # lint-amnesty, pylint: disable=unused-argument +def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): # pylint: disable=unused-argument mock_modulestore.return_value = Mock(XMLModuleStore) mock_storage.url.side_effect = Exception @@ -185,8 +185,8 @@ def test_static_url_with_query(mock_modulestore, mock_storage): mock_storage.exists.return_value = False mock_modulestore.return_value = Mock(MongoModuleStore) - pre_text = 'EMBED src ="/static/LAlec04_controller.swf?csConfigFile=/static/LAlec04_config.xml&name1=value1&name2=value2"' # lint-amnesty, pylint: disable=line-too-long - post_text = 'EMBED src ="/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2"' # lint-amnesty, pylint: disable=line-too-long + pre_text = 'EMBED src ="/static/LAlec04_controller.swf?csConfigFile=/static/LAlec04_config.xml&name1=value1&name2=value2"' # noqa: E501 + post_text = 'EMBED src ="/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2"' # noqa: E501 assert replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY) == post_text @@ -205,13 +205,13 @@ def test_static_paths_out(mock_modulestore, mock_storage): mock_modulestore.return_value = Mock(MongoModuleStore) static_url = '/static/LAlec04_controller.swf?csConfigFile=/static/LAlec04_config.xml&name1=value1&name2=value2' - static_course_url = '/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2' # lint-amnesty, pylint: disable=line-too-long + static_course_url = '/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2' # noqa: E501 raw_url = '/static/js/capa/protex/protex.nocache.js?raw' xblock_url = '/static/xblock/resources/babys_first.lil_xblock/public/images/pacifier.png' # xss-lint: disable=python-wrap-html pre_text = f'EMBED src ="{static_url}" xblock={xblock_url} text