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"