From 5f18e959f90f7386bc0a024cd19c83605ab3c94d Mon Sep 17 00:00:00 2001 From: Irtaza Akram <51848298+irtazaakram@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:35:50 +0500 Subject: [PATCH 1/3] feat: add support for django4.2 & update lint (#347) --- .github/workflows/ci.yml | 6 +-- RELEASE.rst | 5 ++ edx_sga/sga.py | 13 +---- pylintrc | 113 +++------------------------------------ setup.py | 9 +--- test_requirements.txt | 20 +++---- tox.ini | 3 +- 7 files changed, 29 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbf27992..99b242b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,10 @@ jobs: matrix: os: [ubuntu-20.04] python-version: ['3.8'] - toxenv: [py38-django32] + toxenv: [py38-django32, py38-django42] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Python setup uses: actions/setup-python@v4 @@ -37,7 +37,7 @@ jobs: run: tox - name: Upload coverage to CodeCov - if: matrix.python-version == '3.8' && matrix.toxenv == 'py38-django32' + if: matrix.python-version == '3.8' && matrix.toxenv == 'py38-django42' uses: codecov/codecov-action@v3 with: file: ./coverage.xml diff --git a/RELEASE.rst b/RELEASE.rst index ca11c5db..99fe3325 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,11 @@ Release Notes ============= +Version 0.24.0 (Unreleased) +-------------- + +- feat: add support for Django 4.2 + Version 0.23.1 (Released January 26, 2024) -------------- diff --git a/edx_sga/sga.py b/edx_sga/sga.py index 61f760ae..529a8bd6 100644 --- a/edx_sga/sga.py +++ b/edx_sga/sga.py @@ -94,7 +94,7 @@ class StaffGradedAssignmentXBlock( default=_("Staff Graded Assignment"), scope=Scope.settings, help=_( - "This name appears in the horizontal navigation at the top of " "the page." + "This name appears in the horizontal navigation at the top of the page." ), ) @@ -119,7 +119,7 @@ class StaffGradedAssignmentXBlock( staff_score = Integer( display_name=_("Score assigned by non-instructor staff"), help=_( - "Score will need to be approved by instructor before being " "published." + "Score will need to be approved by instructor before being published." ), default=None, scope=Scope.settings, @@ -484,7 +484,6 @@ def prepare_download_submissions( """ Runs a async task that collects submissions in background and zip them. """ - # pylint: disable=no-member require(self.is_course_staff()) user = self.get_real_user() require(user) @@ -539,7 +538,6 @@ def download_submissions( """ Api for downloading zip file which consist of all students submissions. """ - # pylint: disable=no-member require(self.is_course_staff()) user = self.get_real_user() require(user) @@ -575,7 +573,6 @@ def download_submissions_status( return Response(json_body={"zip_available": self.is_zip_file_available(user)}) def student_view(self, context=None): - # pylint: disable=no-member """ The primary view of the StaffGradedAssignmentXBlock, shown to students when viewing courses. @@ -727,7 +724,6 @@ def get_or_create_student_module(self, user): Returns: StudentModule: A StudentModule object """ - # pylint: disable=no-member student_module, created = StudentModule.objects.get_or_create( course_id=self.course_id, module_state_key=self.location, @@ -772,7 +768,6 @@ def student_state(self): solution = replace_urls_service.replace_urls(force_str(self.solution)) else: solution = "" - # pylint: disable=no-member return { "display_name": force_str(self.display_name), "uploaded": uploaded, @@ -895,7 +890,6 @@ def download(self, path, mime_type, filename, require_staff=False): def validate_score_message( self, course_id, username ): # lint-amnesty, pylint: disable=missing-function-docstring - # pylint: disable=no-member log.error( "enter_grade: invalid grade submitted for course:%s module:%s student:%s", course_id, @@ -962,7 +956,6 @@ def upload_allowed(self, submission_data=None): ) def file_storage_path(self, file_hash, original_filename): - # pylint: disable=no-member """ Helper method to get the path of an uploaded file """ @@ -972,7 +965,6 @@ def is_zip_file_available(self, user): """ returns True if zip file exists. """ - # pylint: disable=no-member zip_file_path = get_zip_file_path( user.username, self.block_course_id, self.block_id, self.location ) @@ -982,7 +974,6 @@ def count_archive_files(self, user): """ returns number of files archive in zip. """ - # pylint: disable=no-member zip_file_path = get_zip_file_path( user.username, self.block_course_id, self.block_id, self.location ) diff --git a/pylintrc b/pylintrc index c4643eda..449773cc 100644 --- a/pylintrc +++ b/pylintrc @@ -2,7 +2,7 @@ # ** DO NOT EDIT THIS FILE ** # *************************** # -# This file was generated by edx-lint: https://github.com/edx/edx-lint +# This file was generated by edx-lint: https://github.com/openedx/edx-lint # # If you want to change this file, you have two choices, depending on whether # you want to make a local change that applies only to this repo, or whether @@ -28,7 +28,7 @@ # CENTRAL CHANGE: # # 1. Edit the pylintrc file in the edx-lint repo at -# https://github.com/edx/edx-lint/blob/master/edx_lint/files/pylintrc +# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc # # 2. install the updated version of edx-lint (in edx-lint): # @@ -64,7 +64,7 @@ # SERIOUSLY. # # ------------------------------ -# Generated by edx-lint version: 5.2.2 +# Generated by edx-lint version: 5.3.4 # ------------------------------ [MASTER] ignore = @@ -102,20 +102,12 @@ enable = cell-var-from-loop, confusing-with-statement, continue-in-finally, - cyclical-import, dangerous-default-value, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, duplicate-argument-name, duplicate-bases, duplicate-except, duplicate-key, - eq-without-hash, - exception-escape, - exception-message-attribute, expression-not-assigned, - filter-builtin-not-iterating, format-combined-specification, format-needs-mapping, function-redefined, @@ -123,33 +115,26 @@ enable = import-error, import-self, inconsistent-mro, - indexing-exception, inherit-non-class, init-is-generator, invalid-all-object, - invalid-encoded-data, invalid-format-index, invalid-length-returned, invalid-sequence-index, invalid-slice-index, invalid-slots-object, invalid-slots, - invalid-str-codec, invalid-unary-operand-type, logging-too-few-args, logging-too-many-args, logging-unsupported-format, lost-exception, - map-builtin-not-iterating, method-hidden, misplaced-bare-raise, misplaced-future, missing-format-argument-key, missing-format-attribute, missing-format-string-key, - missing-super-argument, - mixed-fomat-string, - model-unicode-not-callable, no-member, no-method-argument, no-name-in-module, @@ -158,8 +143,6 @@ enable = non-iterator-returned, non-parent-method-called, nonexistent-operator, - nonimplemented-raised, - nonstandard-exception, not-a-mapping, not-an-iterable, not-callable, @@ -167,35 +150,25 @@ enable = not-in-loop, pointless-statement, pointless-string-statement, - property-on-old-class, raising-bad-type, raising-non-exception, - raising-string, - range-builtin-not-iterating, redefined-builtin, - redefined-in-handler, redefined-outer-name, - redefined-variable-type, redundant-keyword-arg, - relative-import, repeated-keyword, return-arg-in-generator, return-in-init, return-outside-function, signature-differs, - slots-on-old-class, super-init-not-called, super-method-not-called, - super-on-old-class, syntax-error, - sys-max-int, test-inherits-tests, too-few-format-args, too-many-format-args, too-many-function-args, translation-of-non-string, truncated-format-string, - unbalance-tuple-unpacking, undefined-all-variable, undefined-loop-variable, undefined-variable, @@ -211,11 +184,8 @@ enable = used-before-assignment, using-constant-test, yield-outside-function, - zip-builtin-not-iterating, astroid-error, - django-not-available-placeholder, - django-not-available, fatal, method-check-failed, parse-error, @@ -237,7 +207,6 @@ enable = bad-classmethod-argument, bad-mcs-classmethod-argument, bad-mcs-method-argument, - bad-whitespace, bare-except, broad-except, consider-iterating-dictionary, @@ -247,16 +216,10 @@ enable = literal-used-as-attribute, logging-format-interpolation, logging-not-lazy, - metaclass-assignment, - model-has-unicode, - model-missing-unicode, - model-no-explicit-unicode, multiple-imports, multiple-statements, no-classmethod-decorator, no-staticmethod-decorator, - old-raise-syntax, - old-style-class, protected-access, redundant-unittest-assert, reimported, @@ -284,7 +247,6 @@ enable = wrong-import-position, missing-final-newline, - mixed-indentation, mixed-line-endings, trailing-newlines, trailing-whitespace, @@ -295,26 +257,9 @@ enable = deprecated-pragma, unrecognized-inline-option, useless-suppression, - - cmp-method, - coerce-method, - delslice-method, - dict-iter-method, - dict-view-method, - div-method, - getslice-method, - hex-method, - idiv-method, - next-method-called, - next-method-defined, - nonzero-method, - oct-method, - rdiv-method, - setslice-method, - using-cmp-argument, disable = - bad-continuation, bad-indentation, + broad-exception-raised, consider-using-f-string, duplicate-code, file-ignored, @@ -322,12 +267,7 @@ disable = global-statement, invalid-name, locally-disabled, - locally-enabled, - lowercase-l-suffix, - misplaced-comparison-constant, no-else-return, - no-init, - no-self-use, suppressed-message, too-few-public-methods, too-many-ancestors, @@ -346,44 +286,6 @@ disable = feature-toggle-needs-doc, illegal-waffle-usage, - apply-builtin, - backtick, - bad-python3-import, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - deprecated-itertools-function, - deprecated-operator-function, - deprecated-str-translate-call, - deprecated-string-function, - deprecated-sys-function, - deprecated-types-field, - deprecated-urllib-function, - execfile-builtin, - file-builtin, - import-star-module-level, - input-builtin, - intern-builtin, - long-builtin, - long-suffix, - no-absolute-import, - non-ascii-bytes-literal, - old-division, - old-ne-operator, - old-octal-literal, - parameter-unpacking, - print-statement, - raw_input-builtin, - reduce-builtin, - reload-builtin, - round-builtin, - standarderror-builtin, - unichr-builtin, - unicode-builtin, - unpacking-in-except, - xrange-builtin, - logging-fstring-interpolation, import-error, no-name-in-module, @@ -391,12 +293,10 @@ disable = [REPORTS] output-format = text -files-output = no reports = no score = no [BASIC] -bad-functions = map,filter,apply,input module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ class-rgx = [A-Z_][a-zA-Z0-9]+$ @@ -416,7 +316,6 @@ docstring-min-length = 5 max-line-length = 120 ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ single-line-if-stmt = no -no-space-check = trailing-comma,dict-separator max-module-lines = 1000 indent-string = ' ' @@ -485,6 +384,6 @@ ext-import-graph = int-import-graph = [EXCEPTIONS] -overgeneral-exceptions = Exception +overgeneral-exceptions = builtins.Exception -# e4e48da2ae70ae33f341b6bd31cc537a054573d8 +# ff64c395f281daac14346c8f9bfe2e4b246e1b3b diff --git a/setup.py b/setup.py index 0785fb35..7e6b1dcd 100644 --- a/setup.py +++ b/setup.py @@ -35,16 +35,9 @@ def package_data(pkg, root_list): "Natural Language :: English", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Internet :: WWW/HTTP", "Topic :: Education", diff --git a/test_requirements.txt b/test_requirements.txt index 634420e5..a5207e4f 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,28 +1,28 @@ # maintain the latest version -celery==5.3.0 -coverage==7.2.7 -cryptography==41.0.1 +celery==5.3.4 +coverage==7.3.1 +cryptography==41.0.3 ddt==1.6.0 djangorestframework django-storages -edx-celeryutils==1.2.2 -edx-lint==5.2.4 +edx-celeryutils==1.2.3 +edx-lint==5.3.4 edx-opaque-keys -edx-submissions==3.5.5 +edx-submissions==3.6.0 jsonfield==3.1.0 mako==1.2.4 pdbpp==0.10.3 -pylint==2.12.2 +pylint==2.17.5 pylint-celery==0.3 pylint-django==2.5.3 pyOpenSSL==23.2.0 pytz==2023.3 -pytest==7.3.1 +pytest==7.4.2 pytest-cov==4.1.0 pytest-django==4.5.2 six==1.16.0 tox tox-battery -xblock-utils==3.1.0 -xblock-sdk==0.5.4 +xblock-utils==3.4.1 +xblock-sdk==0.7.0 diff --git a/tox.ini b/tox.ini index 6845bb1c..55c54a73 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38}-django{32} +envlist = py{38}-django{32,42} [testenv] passenv = @@ -8,6 +8,7 @@ passenv = deps = django32: Django>=3.2,<3.3 + django42: Django>=4.2,<5.0 -r test_requirements.txt commands = From c9ba9c16d82c30da69278dfa228f0558194f9f4f Mon Sep 17 00:00:00 2001 From: Muhammad Anas <88967643+Anas12091101@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:24:54 +0500 Subject: [PATCH 2/3] chore: enabling integration tests (#355) * chore: enabling integration tests * fix: fixed all integration tests * feat: added integration tests in CI workflow * fix: some cleanup * temp: added codecov.yml * temp: changed codecov.yml * temp: changed codecov.yml * temp: removed codecov.yml * refactor: removed the redundant submissions_api.get_scoresubmission call * chore: updated xblock-sdk version to 0.9.0 --- .github/workflows/ci.yml | 15 +- edx_sga/sga.py | 3 +- edx_sga/tests/integration_tests.py | 217 ++++++++++++++++------------- pytest.ini | 2 +- run_devstack_integration_tests.sh | 5 +- test_requirements.txt | 2 +- 6 files changed, 139 insertions(+), 105 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99b242b1..c4a6b562 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,8 @@ jobs: - name: Python setup uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} - + python-version: ${{ matrix.python-version }} + - name: tox install run: pip install tox @@ -36,9 +36,18 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox + - name: Run Integration Tests + run: | + cd .. + git clone https://github.com/openedx/devstack + cd devstack + sed -i 's/:cached//g' ./docker-compose-host.yml + make dev.clone.https + DEVSTACK_WORKSPACE=$PWD/.. docker-compose -f docker-compose.yml -f docker-compose-host.yml run -v $PWD/../edx-sga:/edx-sga lms /edx-sga/run_devstack_integration_tests.sh + - name: Upload coverage to CodeCov if: matrix.python-version == '3.8' && matrix.toxenv == 'py38-django42' uses: codecov/codecov-action@v3 with: file: ./coverage.xml - fail_ci_if_error: true + fail_ci_if_error: false diff --git a/edx_sga/sga.py b/edx_sga/sga.py index 529a8bd6..25f7b02a 100644 --- a/edx_sga/sga.py +++ b/edx_sga/sga.py @@ -182,6 +182,7 @@ def file_size_over_limit(cls, file_obj): @classmethod def parse_xml(cls, node, runtime, keys, id_generator): + # pylint: disable=arguments-differ,unused-argument """ Override default serialization to handle elements """ @@ -190,7 +191,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): for child in node: if child.tag == "solution": # convert child elements of into HTML for display - block.solution = "".join(etree.tostring(subchild) for subchild in child) + block.solution = "".join(etree.tostring(subchild, encoding=str) for subchild in child) # Attributes become fields. # Note that a solution attribute here will override any solution XML element diff --git a/edx_sga/tests/integration_tests.py b/edx_sga/tests/integration_tests.py index 40804f8f..49da0f6a 100644 --- a/edx_sga/tests/integration_tests.py +++ b/edx_sga/tests/integration_tests.py @@ -15,13 +15,15 @@ import six.moves.urllib.request import pytz from ddt import data, ddt, unpack +from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.db import transaction from django.test.utils import override_settings +from django.http.request import HttpRequest from lms.djangoapps.courseware import block_render as render from lms.djangoapps.courseware.models import StudentModule -from opaque_keys.edx.locations import Location +from opaque_keys.edx.locations import BlockUsageLocator from opaque_keys.edx.locator import CourseLocator from common.djangoapps.student.models import UserProfile, anonymous_id_for_user from common.djangoapps.student.tests.factories import AdminFactory, StaffFactory @@ -29,13 +31,14 @@ from submissions.models import StudentItem from xblock.field_data import DictFieldData from xblock.fields import ScopeIds +from xblock.runtime import DictKeyValueStore, KvsFieldData, Mixologist +from xblock.test.tools import TestRuntime from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml - from edx_sga.constants import ShowAnswer from edx_sga.sga import StaffGradedAssignmentXBlock from edx_sga.tests.common import ( @@ -70,7 +73,7 @@ def setUp(self): engine for use in all tests """ super().setUp() - self.course = CourseFactory.create(org="foo", number="bar", display_name="baz") + self.course = CourseFactory.create(org="SGAU", number="SGA101", display_name="course") self.block = BlockFactory(category="pure", parent=self.course) self.course_id = self.course.id self.instructor = StaffFactory.create(course_key=self.course_id) @@ -83,21 +86,17 @@ def make_runtime(self, **kwargs): """ Make a runtime """ - runtime, _ = render.get_module_system_for_user( - self.instructor, - self.student_data, - self.block, - self.course.id, - mock.Mock(), - mock.Mock(), - mock.Mock(), + render.prepare_runtime_for_user( + user=self.instructor, + student_data=self.student_data, + runtime=self.block.runtime, + course_id=self.course.id, + track_function=render.make_track_function(HttpRequest()), + request_token=mock.Mock(), course=self.course, - # not sure why this isn't working, if set to true it looks for - # 'display_name_with_default_escaped' field that doesn't exist in SGA - wrap_xblock_display=False, - **kwargs, + **kwargs ) - return runtime + return self.block.runtime def make_scope_ids(self, runtime): """ @@ -105,6 +104,7 @@ def make_scope_ids(self, runtime): """ # Not sure if this is a valid block type, might be sufficient for testing purposes block_type = "sga" + runtime = TestRuntime(services={'field-data': KvsFieldData(kvs=DictKeyValueStore())}) def_id = runtime.id_generator.create_definition(block_type) return ScopeIds("user", block_type, def_id, self.block.location) @@ -113,12 +113,12 @@ def make_one(self, display_name=None, **kw): Creates a XBlock SGA for testing purpose. """ field_data = DictFieldData(kw) - block = StaffGradedAssignmentXBlock(self.runtime, field_data, self.scope_ids) - block.location = Location("foo", "bar", "baz", "category", "name", "revision") - - block.xmodule_runtime = self.runtime - block.course_id = self.course_id - block.category = "problem" + mixologist = Mixologist(settings.XBLOCK_MIXINS) + class_ = mixologist.mix(StaffGradedAssignmentXBlock) + block = class_(self.runtime, field_data, self.scope_ids) + runtime = TestRuntime(services={'field-data': KvsFieldData(kvs=DictKeyValueStore())}) + def_id = runtime.id_generator.create_definition("sga") + block.location = BlockUsageLocator(CourseLocator("SGAU","SGA101","course"), "sga", def_id) if display_name: block.display_name = display_name @@ -183,7 +183,6 @@ def make_student(self, block, name, make_state=True, **state): if make_state: self.addCleanup(module.delete) return {"module": module, "item": item, "submission": submission} - return {"item": item, "submission": submission} def personalize(self, block, module, item, submission): @@ -195,7 +194,7 @@ def personalize(self, block, module, item, submission): state = json.loads(student_module.state) for key, value in state.items(): setattr(block, key, value) - self.runtime.anonymous_student_id = item.student_id + self.runtime.deprecated_anonymous_student_id = item.student_id def test_ctor(self): """ @@ -235,7 +234,7 @@ def test_student_view(self, fragment, render_template): self.assertEqual(template_arg, "templates/staff_graded_assignment/show.html") context = render_template.call_args[0][1] self.assertEqual(context["is_course_staff"], True) - self.assertEqual(context["id"], "name") + self.assertEqual(context["id"], "d_0") self.assertEqual(context["support_email"], "foo@example.com") student_state = json.loads(context["student_state"]) self.assertEqual(student_state["display_name"], "Custom name") @@ -244,7 +243,6 @@ def test_student_view(self, fragment, render_template): self.assertEqual(student_state["upload_allowed"], True) self.assertEqual(student_state["max_score"], 100) self.assertEqual(student_state["graded"], None) - # pylint: disable=no-member fragment.add_css.assert_called_once_with( DummyResource("static/css/edx_sga.css") ) @@ -259,13 +257,15 @@ def test_student_view_with_upload(self, fragment, render_template): Test student is able to upload assignment correctly. """ block = self.make_one() - self.personalize( - block, **self.make_student(block, 'fred"', sha1="foo", filename="foo.bar") - ) - block.student_view() - context = render_template.call_args[0][1] - student_state = json.loads(context["student_state"]) - self.assertEqual(student_state["uploaded"], {"filename": "foo.bar"}) + student = self.make_student(block, "fred", sha1="foo", filename="foo.bar") + self.personalize(block, **student) + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + with mock.patch.object(StaffGradedAssignmentXBlock.get_submission,"__defaults__",(student_id,)): + block.student_view() + context = render_template.call_args[0][1] + student_state = json.loads(context["student_state"]) + self.assertEqual(student_state["uploaded"], {"filename": "foo.bar"}) @mock.patch("edx_sga.sga._resource", DummyResource) @mock.patch("edx_sga.sga.render_template") @@ -276,11 +276,15 @@ def test_student_view_with_annotated(self, fragment, render_template): Test student view shows annotated files correctly. """ block = self.make_one(annotated_sha1="foo", annotated_filename="foo.bar") - self.personalize(block, **self.make_student(block, "fred")) - block.student_view() - context = render_template.call_args[0][1] - student_state = json.loads(context["student_state"]) - self.assertEqual(student_state["annotated"], {"filename": "foo.bar"}) + student = self.make_student(block, "fred") + self.personalize(block, **student) + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + with mock.patch.object(StaffGradedAssignmentXBlock.get_submission,"__defaults__",(student_id,)): + block.student_view() + context = render_template.call_args[0][1] + student_state = json.loads(context["student_state"]) + self.assertEqual(student_state["annotated"], {"filename": "foo.bar"}) @mock.patch("edx_sga.sga._resource", DummyResource) @mock.patch("edx_sga.sga.render_template") @@ -290,28 +294,31 @@ def test_student_view_with_score(self, fragment, render_template): Tests scores are displayed correctly on student view. """ block = self.make_one() - self.personalize( - block, **self.make_student(block, "fred", filename="foo.txt", score=10) - ) - fragment = block.student_view() - render_template.assert_called_once() - template_arg = render_template.call_args[0][0] - self.assertEqual(template_arg, "templates/staff_graded_assignment/show.html") - context = render_template.call_args[0][1] - self.assertEqual(context["is_course_staff"], True) - self.assertEqual(context["id"], "name") - student_state = json.loads(context["student_state"]) - self.assertEqual(student_state["display_name"], "Staff Graded Assignment") - self.assertEqual(student_state["uploaded"], {"filename": "foo.txt"}) - self.assertEqual(student_state["annotated"], None) - self.assertEqual(student_state["upload_allowed"], False) - self.assertEqual(student_state["max_score"], 100) - self.assertEqual(student_state["graded"], {"comment": "", "score": 10}) - # pylint: disable=no-member - fragment.add_css.assert_called_once_with( - DummyResource("static/css/edx_sga.css") - ) - fragment.initialize_js.assert_called_once_with("StaffGradedAssignmentXBlock") + student = self.make_student(block, "fred", filename="foo.txt", score=10) + self.personalize(block, **student) + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + + with mock.patch.object(StaffGradedAssignmentXBlock.get_submission,"__defaults__",(student_id,)),\ + mock.patch.object(StaffGradedAssignmentXBlock.get_score, "__defaults__",(student_id,)): + fragment = block.student_view() + render_template.assert_called_once() + template_arg = render_template.call_args[0][0] + self.assertEqual(template_arg, "templates/staff_graded_assignment/show.html") + context = render_template.call_args[0][1] + self.assertEqual(context["is_course_staff"], True) + self.assertEqual(context["id"], "d_0") + student_state = json.loads(context["student_state"]) + self.assertEqual(student_state["display_name"], "Staff Graded Assignment") + self.assertEqual(student_state["uploaded"], {"filename": "foo.txt"}) + self.assertEqual(student_state["annotated"], None) + self.assertEqual(student_state["upload_allowed"], False) + self.assertEqual(student_state["max_score"], 100) + self.assertEqual(student_state["graded"], {"comment": "", "score": 10}) + fragment.add_css.assert_called_once_with( + DummyResource("static/css/edx_sga.css") + ) + fragment.initialize_js.assert_called_once_with("StaffGradedAssignmentXBlock") def test_studio_view(self): """ @@ -341,7 +348,7 @@ def weights_positive_float_test(): method="POST", body=json.dumps( {"display_name": "Test Block", "points": "100", "weight": -10.0} - ), + ).encode("utf-8"), ) ) self.assertEqual(block.weight, orig_weight) @@ -352,7 +359,7 @@ def weights_positive_float_test(): method="POST", body=json.dumps( {"display_name": "Test Block", "points": "100", "weight": "a"} - ), + ).encode("utf-8"), ) ) self.assertEqual(block.weight, orig_weight) @@ -367,7 +374,7 @@ def point_positive_int_test(): method="POST", body=json.dumps( {"display_name": "Test Block", "points": "-10", "weight": 11} - ), + ).encode("utf-8"), ) ) self.assertEqual(block.points, orig_score) @@ -378,7 +385,7 @@ def point_positive_int_test(): method="POST", body=json.dumps( {"display_name": "Test Block", "points": "24.5", "weight": 11} - ), + ).encode("utf-8"), ) ) self.assertEqual(block.points, orig_score) @@ -398,7 +405,7 @@ def point_positive_int_test(): "points": str(orig_score), "weight": 11, } - ), + ).encode("utf-8"), ) ) self.assertEqual(block.display_name, "Test Block") @@ -438,12 +445,15 @@ def test_finalize_uploaded_assignment(self): ) self.personalize(block, **created_student_data) submission_data = created_student_data["submission"] - response = block.finalize_uploaded_assignment(mock.Mock(method="POST")) - recent_submission_data = block.get_submission() - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json, block.student_state()) - self.assertEqual(submission_data["uuid"], recent_submission_data["uuid"]) - self.assertTrue(recent_submission_data["answer"]["finalized"]) + user = created_student_data["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + with mock.patch.object(StaffGradedAssignmentXBlock.get_submission,"__defaults__",(student_id,)): + response = block.finalize_uploaded_assignment(mock.Mock(method="POST")) + recent_submission_data = block.get_submission(student_id) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, block.student_state()) + self.assertEqual(submission_data["uuid"], recent_submission_data["uuid"]) + self.assertTrue(recent_submission_data["answer"]["finalized"]) def test_staff_upload_download_annotated(self): """ @@ -516,7 +526,7 @@ def test_download_annotated(self): for student, text in students: self.personalize(block, **student) response = block.download_annotated(None) - self.assertEqual(response.body, text) + self.assertEqual(str(response.body,'utf-8'), text) with mock.patch( "edx_sga.sga.StaffGradedAssignmentXBlock.file_storage_path", @@ -540,7 +550,11 @@ def test_staff_download(self): for student_name, filename, text in students_info: student = self.make_student(block, student_name) self.personalize(block, **student) - with self.dummy_upload(filename, text) as (upload, __): + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(filename, text) as (upload, __): block.upload_assignment(mock.Mock(params={"assignment": upload})) students.append( ( @@ -553,7 +567,7 @@ def test_staff_download(self): response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) ) - self.assertEqual(response.body, text) + self.assertEqual(response.body.decode("utf-8"), text) # assert that staff cannot access invalid files for student, __ in students: @@ -599,7 +613,11 @@ def test_staff_download_unicode_filename(self): block = self.make_one() student = self.make_student(block, "fred") self.personalize(block, **student) - with self.dummy_upload("файл.txt") as (upload, expected): + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload("файл.txt") as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -625,7 +643,11 @@ def test_staff_download_filename_with_spaces(self): block = self.make_one() student = self.make_student(block, "fred") self.personalize(block, **student) - with self.dummy_upload(file_name) as (upload, expected): + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(file_name) as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -644,7 +666,11 @@ def test_file_download_comma_in_name(self, file_name): block = self.make_one() student = self.make_student(block, "fred") self.personalize(block, **student) - with self.dummy_upload(file_name) as (upload, expected): + user = student["module"].student + student_id = anonymous_id_for_user(user,self.course_id) + + with mock.patch.object(StaffGradedAssignmentXBlock.get_student_item_dict,"__defaults__",(student_id,)),\ + self.dummy_upload(file_name) as (upload, expected): block.upload_assignment(mock.Mock(params={"assignment": upload})) response = block.staff_download( mock.Mock(params={"student_id": student["item"].student_id}) @@ -659,9 +685,9 @@ def test_get_staff_grading_data_not_staff(self): """ test staff grading data for non staff members. """ - self.runtime.user_is_staff = False block = self.make_one() - with self.assertRaises(PermissionDenied): + with mock.patch("edx_sga.sga.StaffGradedAssignmentXBlock.is_course_staff", return_value=False),\ + self.assertRaises(PermissionDenied): block.get_staff_grading_data(None) def test_get_staff_grading_data(self): @@ -846,7 +872,7 @@ def test_showanswer(self, is_answer_available): "A solution" if is_answer_available else "" ) - @data((True, "/static/foo"), (False, "/c4x/foo/bar/asset")) + @data((True, "/static/foo"), (False, "/asset-v1:SGAU+SGA101+course+type@asset+block")) @unpack def test_replace_url(self, has_static_asset_path, path): """ @@ -869,7 +895,7 @@ def test_base_asset_url(self): The base asset url for the course should be passed to the javascript so it can replace static links """ block = self.make_one(solution="A solution") - assert block.student_state()["base_asset_url"] == "/c4x/foo/bar/asset/" + assert block.student_state()["base_asset_url"] == "/asset-v1:SGAU+SGA101+course+type@asset+block@" def test_correctness_available(self): """ @@ -900,22 +926,19 @@ def test_has_attempted(self): @data(True, False) def test_runtime_user_is_staff(self, is_staff): - course = CourseFactory.create(org="org", number="bar", display_name="baz") - block = BlockFactory(category="pure", parent=course) - staff = StaffFactory.create(course_key=course.id) - self.runtime, _ = render.get_module_system_for_user( - staff if is_staff else User.objects.create(), - self.student_data, - block, - course.id, - mock.Mock(), - mock.Mock(), - mock.Mock(), - course=course, + staff = StaffFactory.create(course_key=self.course.id) + + render.prepare_runtime_for_user( + user=staff if is_staff else User.objects.create(), + student_data=self.student_data, + runtime=self.block.runtime, + course_id=self.course.id, + track_function=render.make_track_function(HttpRequest()), + request_token=mock.Mock(), + course=self.course, ) - block = self.make_one() - assert block.runtime_user_is_staff() is is_staff + assert self.block.runtime.user_is_staff is is_staff @data(True, False) def test_grace_period(self, has_grace_period): @@ -938,7 +961,6 @@ def make_test_vertical(self, solution_attribute=None, solution_element=None): solution_element = ( f"{solution_element}" if solution_element else "" ) - return f""" {solution_element} @@ -970,8 +992,7 @@ def import_test_course(self, solution_attribute=None, solution_element=None): "sga_user", xml_dir, ) - - return store.get_course(CourseLocator.from_string("SGAU/SGA101/course")) + return store.get_course(CourseLocator.from_string("course-v1:SGAU+SGA101+course")) @data( *[ diff --git a/pytest.ini b/pytest.ini index 4add574d..54220678 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] DJANGO_SETTINGS_MODULE = edx_sga.test_settings addopts = --cov . --ds=edx_sga.test_settings -norecursedirs = .git .tox edx_sga.static edx_sga.locale edx_sga.templates {arch} *.egg +norecursedirs = .git .tox edx_sga.static edx_sga.locale edx_sga.templates {arch} *.egg \ No newline at end of file diff --git a/run_devstack_integration_tests.sh b/run_devstack_integration_tests.sh index 9b85c10e..edabf0ad 100755 --- a/run_devstack_integration_tests.sh +++ b/run_devstack_integration_tests.sh @@ -8,6 +8,8 @@ mkdir -p reports pip install -r requirements/edx/testing.txt +pip install -e . + cd /edx-sga pip uninstall edx-sga -y pip install -e . @@ -20,4 +22,5 @@ cp /edx/app/edxapp/edx-platform/setup.cfg . rm ./pytest.ini mkdir test_root # for edx -pytest ./edx_sga/tests/integration_tests.py +pytest ./edx_sga/tests/integration_tests.py --cov . +coverage xml diff --git a/test_requirements.txt b/test_requirements.txt index a5207e4f..7ffa9d6d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -25,4 +25,4 @@ tox tox-battery xblock-utils==3.4.1 -xblock-sdk==0.7.0 +xblock-sdk==0.9.0 From 0aa2292a88dce7085e9f42e3de9eeabeedfc0ed7 Mon Sep 17 00:00:00 2001 From: Doof Date: Thu, 28 Mar 2024 10:42:49 +0000 Subject: [PATCH 3/3] Release 0.24.0 --- RELEASE.rst | 6 ++++++ edx_sga/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 99fe3325..450c77c1 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,12 @@ Release Notes ============= +Version 0.24.0 +-------------- + +- chore: enabling integration tests (#355) +- feat: add support for django4.2 & update lint (#347) + Version 0.24.0 (Unreleased) -------------- diff --git a/edx_sga/__init__.py b/edx_sga/__init__.py index 089a7c4e..3be5eb16 100644 --- a/edx_sga/__init__.py +++ b/edx_sga/__init__.py @@ -2,4 +2,4 @@ Module for StaffGradedAssignmentXBlock. """ -__version__ = "0.23.1" +__version__ = "0.24.0"