diff --git a/src/rapid_response_xblock/apps.py b/src/rapid_response_xblock/apps.py index 2389133f..e7700e94 100644 --- a/src/rapid_response_xblock/apps.py +++ b/src/rapid_response_xblock/apps.py @@ -1,4 +1,5 @@ """AppConfig for rapid response""" + from django.apps import AppConfig from edx_django_utils.plugins import PluginSettings, PluginURLs from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType @@ -22,7 +23,7 @@ class RapidResponseAppConfig(AppConfig): SettingsType.COMMON: { PluginSettings.RELATIVE_PATH: "settings.cms_settings" }, - } + }, }, PluginURLs.CONFIG: { ProjectType.CMS: { diff --git a/src/rapid_response_xblock/block.py b/src/rapid_response_xblock/block.py index 2db02630..c78c4460 100644 --- a/src/rapid_response_xblock/block.py +++ b/src/rapid_response_xblock/block.py @@ -1,4 +1,5 @@ """Rapid-response functionality""" + import logging from datetime import datetime from functools import wraps @@ -51,14 +52,13 @@ def staff_only(handler_method): """ Wrapper that ensures a handler method is enabled for staff users only """ # noqa: D401 + @wraps(handler_method) def wrapper(aside_instance, *args, **kwargs): if not aside_instance.is_staff(): - return Response( - status=403, - json_body="Unauthorized (staff only)" - ) + return Response(status=403, json_body="Unauthorized (staff only)") return handler_method(aside_instance, *args, **kwargs) + return wrapper @@ -75,11 +75,11 @@ class RapidResponseAside(XBlockAside): display_name=_("Rapid response enabled status"), default=False, scope=Scope.settings, - help=_("Indicates whether or not a problem is enabled for rapid response") + help=_("Indicates whether or not a problem is enabled for rapid response"), ) @XBlockAside.aside_for("student_view") - def student_view_aside(self, block, context=None): # pylint: disable=unused-argument # noqa: ARG002 + def student_view_aside(self, block, context=None): # noqa: ARG002 """ Renders the aside contents for the student view """ # noqa: D401 @@ -87,12 +87,7 @@ def student_view_aside(self, block, context=None): # pylint: disable=unused-arg if not self.is_staff() or not self.enabled: return fragment fragment.add_content( - render_template( - "static/html/rapid.html", - { - "is_open": self.has_open_run - } - ) + render_template("static/html/rapid.html", {"is_open": self.has_open_run}) ) fragment.add_css(get_resource_bytes("static/css/rapid.css")) fragment.add_javascript(get_resource_bytes("static/js/rapid.js")) @@ -101,7 +96,7 @@ def student_view_aside(self, block, context=None): # pylint: disable=unused-arg return fragment @XBlockAside.aside_for("author_view") - def author_view_aside(self, block, context=None): # pylint: disable=unused-argument # noqa: ARG002 + def author_view_aside(self, block, context=None): # noqa: ARG002 """ Renders the aside contents for the author view """ # noqa: D401 @@ -110,7 +105,7 @@ def author_view_aside(self, block, context=None): # pylint: disable=unused-argu return Fragment("") @XBlockAside.aside_for("studio_view") - def studio_view_aside(self, block, context=None): # pylint: disable=unused-argument # noqa: ARG002 + def studio_view_aside(self, block, context=None): # noqa: ARG002 """ Renders the aside contents for the studio view """ # noqa: D401 @@ -118,7 +113,7 @@ def studio_view_aside(self, block, context=None): # pylint: disable=unused-argu @XBlock.handler @staff_only - def toggle_block_open_status(self, request=None, suffix=None): # pylint: disable=unused-argument # noqa: ARG002 + def toggle_block_open_status(self, request=None, suffix=None): # noqa: ARG002 """ Toggles the open/closed status for the rapid-response-enabled block """ @@ -144,7 +139,7 @@ def toggle_block_open_status(self, request=None, suffix=None): # pylint: disabl ) @XBlock.handler - def toggle_block_enabled(self, request=None, suffix=None): # pylint: disable=unused-argument # noqa: ARG002 + def toggle_block_enabled(self, request=None, suffix=None): # noqa: ARG002 """ Toggles the enabled status for the rapid-response-enabled block """ @@ -153,7 +148,7 @@ def toggle_block_enabled(self, request=None, suffix=None): # pylint: disable=un @XBlock.handler @staff_only - def responses(self, request=None, suffix=None): # pylint: disable=unused-argument # noqa: ARG002 + def responses(self, request=None, suffix=None): # noqa: ARG002 """ Returns student responses for rapid-response-enabled block """ # noqa: D401 @@ -173,19 +168,20 @@ def responses(self, request=None, suffix=None): # pylint: disable=unused-argume ) total_counts = { - run["id"]: sum( - counts[choice["answer_id"]][run["id"]] for choice in choices - ) for run in runs + run["id"]: sum(counts[choice["answer_id"]][run["id"]] for choice in choices) + for run in runs } - return Response(json_body={ - "is_open": is_open, - "runs": runs, - "choices": choices, - "counts": counts, - "total_counts": total_counts, - "server_now": datetime.now(tz=pytz.utc).isoformat(), - }) + return Response( + json_body={ + "is_open": is_open, + "runs": runs, + "choices": choices, + "counts": counts, + "total_counts": total_counts, + "server_now": datetime.now(tz=pytz.utc).isoformat(), + } + ) @classmethod def should_apply_to_block(cls, block): @@ -219,8 +215,7 @@ def get_studio_fragment(self): fragment = Fragment("") fragment.add_content( render_template( - "static/html/rapid_studio.html", - {"is_enabled": self.enabled} + "static/html/rapid_studio.html", {"is_enabled": self.enabled} ) ) fragment.add_css(get_resource_bytes("static/css/rapid.css")) @@ -247,10 +242,14 @@ def has_open_run(self): """ Check if there is an open run for this problem """ - run = RapidResponseRun.objects.filter( - problem_usage_key=self.wrapped_block_usage_key, - course_key=self.course_key, - ).order_by("-created").first() + run = ( + RapidResponseRun.objects.filter( + problem_usage_key=self.wrapped_block_usage_key, + course_key=self.course_key, + ) + .order_by("-created") + .first() + ) return run and run.open @property @@ -268,7 +267,9 @@ def choices(self): return [ { "answer_id": choice.get("name"), - "answer_text": next(iter(choice.itertext())) if list(choice.itertext()) else "" # noqa: E501 + "answer_text": ( + next(iter(choice.itertext())) if list(choice.itertext()) else "" + ), } for choice in choice_elements ] @@ -289,7 +290,8 @@ def serialize_runs(runs): "id": run.id, "created": run.created.isoformat(), "open": run.open, - } for run in runs + } + for run in runs ] @staticmethod @@ -305,15 +307,20 @@ def get_counts_for_problem(run_ids, choices): dict: A mapping of answer id => run id => count for that run """ - response_data = RapidResponseSubmission.objects.filter( - run__id__in=run_ids - ).values("answer_id", "run").annotate(count=Count("answer_id")) + response_data = ( + RapidResponseSubmission.objects.filter(run__id__in=run_ids) + .values("answer_id", "run") + .annotate(count=Count("answer_id")) + ) # Make sure every answer has a count and convert to JSON serializable format - response_counts = {(item["answer_id"], item["run"]): item["count"] for item in response_data} # noqa: E501 + response_counts = { + (item["answer_id"], item["run"]): item["count"] for item in response_data + } return { choice["answer_id"]: { run_id: response_counts.get((choice["answer_id"], run_id), 0) for run_id in run_ids - } for choice in choices + } + for choice in choices } diff --git a/src/rapid_response_xblock/logger.py b/src/rapid_response_xblock/logger.py index 6cb67dce..6c726b8d 100644 --- a/src/rapid_response_xblock/logger.py +++ b/src/rapid_response_xblock/logger.py @@ -1,6 +1,7 @@ """ Capture events """ + import logging from typing import NamedTuple @@ -17,7 +18,14 @@ log = logging.getLogger(__name__) SubmissionEvent = NamedTuple( "SubmissionEvent", - ["raw_data", "user_id", "problem_usage_key", "course_key", "answer_text", "answer_id"] # noqa: E501 + [ + "raw_data", + "user_id", + "problem_usage_key", + "course_key", + "answer_text", + "answer_id", + ], ) @@ -68,14 +76,10 @@ def parse_submission_event(event): return SubmissionEvent( raw_data=event, user_id=event["context"]["user_id"], - problem_usage_key=UsageKey.from_string( - event_data["problem_id"] - ), - course_key=CourseLocator.from_string( - event["context"]["course_id"] - ), + problem_usage_key=UsageKey.from_string(event_data["problem_id"]), + course_key=CourseLocator.from_string(event["context"]["course_id"]), answer_text=submission["answer"], - answer_id=event_data["answers"][submission_key] + answer_id=event_data["answers"][submission_key], ) except: # pylint: disable=bare-except # noqa: E722 log.exception("Unable to parse event data as a submission: %s", event) @@ -86,10 +90,13 @@ def send(self, event): if sub is None: return - open_run = RapidResponseRun.objects.filter( - problem_usage_key=sub.problem_usage_key, - course_key=sub.course_key - ).order_by("-created").first() + open_run = ( + RapidResponseRun.objects.filter( + problem_usage_key=sub.problem_usage_key, course_key=sub.course_key + ) + .order_by("-created") + .first() + ) if not open_run or not open_run.open: # Problem is not open return diff --git a/src/rapid_response_xblock/migrations/0001_initial.py b/src/rapid_response_xblock/migrations/0001_initial.py index 5f71330b..0fc6940f 100644 --- a/src/rapid_response_xblock/migrations/0001_initial.py +++ b/src/rapid_response_xblock/migrations/0001_initial.py @@ -8,6 +8,7 @@ # pylint: skip-file + class Migration(migrations.Migration): dependencies = [ @@ -18,44 +19,68 @@ class Migration(migrations.Migration): migrations.CreateModel( name="RapidResponseSubmission", 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, - )), - ("modified", model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - verbose_name="modified", - editable=False, - )), - ("problem_id", opaque_keys.edx.django.models.UsageKeyField( - max_length=255, - db_index=True, - )), - ("course_id", opaque_keys.edx.django.models.CourseKeyField( - max_length=255, - db_index=True, - )), - ("answer_id", models.CharField( - max_length=255, - null=True, - )), - ("answer_text", models.CharField( - max_length=4096, - null=True, - )), + ( + "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, + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + verbose_name="modified", + editable=False, + ), + ), + ( + "problem_id", + opaque_keys.edx.django.models.UsageKeyField( + max_length=255, + db_index=True, + ), + ), + ( + "course_id", + opaque_keys.edx.django.models.CourseKeyField( + max_length=255, + db_index=True, + ), + ), + ( + "answer_id", + models.CharField( + max_length=255, + null=True, + ), + ), + ( + "answer_text", + models.CharField( + max_length=4096, + null=True, + ), + ), ("event", jsonfield.fields.JSONField()), - ("user", models.ForeignKey( - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - null=True, - )), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + null=True, + ), + ), ], options={ "abstract": False, diff --git a/src/rapid_response_xblock/migrations/0002_block_status.py b/src/rapid_response_xblock/migrations/0002_block_status.py index fb3f3a0f..5dea1698 100644 --- a/src/rapid_response_xblock/migrations/0002_block_status.py +++ b/src/rapid_response_xblock/migrations/0002_block_status.py @@ -12,10 +12,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name="RapidResponseBlockStatus", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), # noqa: E501 - ("usage_key", opaque_keys.edx.django.models.UsageKeyField(max_length=255, db_index=True)), # noqa: E501 + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "usage_key", + opaque_keys.edx.django.models.UsageKeyField( + max_length=255, db_index=True + ), + ), ("open", models.BooleanField(default=False)), - ("course_key", opaque_keys.edx.django.models.CourseKeyField(max_length=255, db_index=True)), # noqa: E501 + ( + "course_key", + opaque_keys.edx.django.models.CourseKeyField( + max_length=255, db_index=True + ), + ), ], ), ] diff --git a/src/rapid_response_xblock/migrations/0004_run.py b/src/rapid_response_xblock/migrations/0004_run.py index 226185aa..d46ae7be 100644 --- a/src/rapid_response_xblock/migrations/0004_run.py +++ b/src/rapid_response_xblock/migrations/0004_run.py @@ -15,12 +15,44 @@ class Migration(migrations.Migration): migrations.CreateModel( name="RapidResponseRun", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), # noqa: E501 - ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name="created", editable=False)), # noqa: E501 - ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name="modified", editable=False)), # noqa: E501 + ( + "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, + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + verbose_name="modified", + editable=False, + ), + ), ("name", models.TextField()), - ("problem_usage_key", opaque_keys.edx.django.models.UsageKeyField(max_length=255, db_index=True)), # noqa: E501 - ("course_key", opaque_keys.edx.django.models.CourseKeyField(max_length=255, db_index=True)), # noqa: E501 + ( + "problem_usage_key", + opaque_keys.edx.django.models.UsageKeyField( + max_length=255, db_index=True + ), + ), + ( + "course_key", + opaque_keys.edx.django.models.CourseKeyField( + max_length=255, db_index=True + ), + ), ("open", models.BooleanField(default=False)), ], options={ @@ -41,6 +73,10 @@ class Migration(migrations.Migration): migrations.AddField( model_name="rapidresponsesubmission", name="run", - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to="rapid_response_xblock.RapidResponseRun", null=True), # noqa: E501 + field=models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + to="rapid_response_xblock.RapidResponseRun", + null=True, + ), ), ] diff --git a/src/rapid_response_xblock/models.py b/src/rapid_response_xblock/models.py index c047c564..a0bdbeba 100644 --- a/src/rapid_response_xblock/models.py +++ b/src/rapid_response_xblock/models.py @@ -2,7 +2,6 @@ Rapid Response block models """ - from django.conf import settings from django.db import models from jsonfield import JSONField @@ -48,16 +47,11 @@ class RapidResponseSubmission(TimeStampedModel): db_index=True, ) run = models.ForeignKey( - RapidResponseRun, - on_delete=models.SET_NULL, - null=True, - db_index=True + RapidResponseRun, on_delete=models.SET_NULL, null=True, db_index=True ) answer_id = models.CharField(null=True, max_length=255) # noqa: DJ001 answer_text = models.CharField(null=True, max_length=4096) # noqa: DJ001 event = JSONField() def __str__(self): - return ( - f"user={self.user} run={self.run} answer_id={self.answer_id}" - ) + return f"user={self.user} run={self.run} answer_id={self.answer_id}" diff --git a/src/rapid_response_xblock/settings/cms_settings.py b/src/rapid_response_xblock/settings/cms_settings.py index f23edaa3..97a72dcd 100644 --- a/src/rapid_response_xblock/settings/cms_settings.py +++ b/src/rapid_response_xblock/settings/cms_settings.py @@ -1,10 +1,12 @@ # noqa: INP001 """Settings to provide to edX""" + def plugin_settings(settings): """ Populate CMS settings """ settings.ENABLE_RAPID_RESPONSE_AUTHOR_VIEW = False + DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/src/rapid_response_xblock/settings/settings.py b/src/rapid_response_xblock/settings/settings.py index 972505f5..cc20d0b5 100644 --- a/src/rapid_response_xblock/settings/settings.py +++ b/src/rapid_response_xblock/settings/settings.py @@ -10,7 +10,8 @@ def plugin_settings(settings): "ENGINE": "rapid_response_xblock.logger.SubmissionRecorder", "OPTIONS": { "name": "rapid_response", - } + }, } + DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/src/rapid_response_xblock/tests/conftest.py b/src/rapid_response_xblock/tests/conftest.py index 689602d7..f0b931e4 100644 --- a/src/rapid_response_xblock/tests/conftest.py +++ b/src/rapid_response_xblock/tests/conftest.py @@ -1,4 +1,5 @@ """Pytest config""" + import json import logging from pathlib import Path @@ -11,12 +12,16 @@ def pytest_addoption(parser): """Pytest hook that adds command line options""" parser.addoption( - "--disable-logging", action="store_true", default=False, - help="Disable all logging during test run" + "--disable-logging", + action="store_true", + default=False, + help="Disable all logging during test run", ) parser.addoption( - "--error-log-only", action="store_true", default=False, - help="Disable all logging output below 'error' level during test run" + "--error-log-only", + action="store_true", + default=False, + help="Disable all logging output below 'error' level during test run", ) @@ -31,6 +36,6 @@ def pytest_configure(config): @pytest.fixture() def example_event(request): # noqa: PT004 """An example real event captured previously""" # noqa: D401 - with Path.open(BASE_DIR / ".." / "test_data"/ "example_event.json") as f: + with Path.open(BASE_DIR / ".." / "test_data" / "example_event.json") as f: request.cls.example_event = json.load(f) yield diff --git a/src/rapid_response_xblock/tests/test_aside.py b/src/rapid_response_xblock/tests/test_aside.py index 81d885ae..d426e5db 100644 --- a/src/rapid_response_xblock/tests/test_aside.py +++ b/src/rapid_response_xblock/tests/test_aside.py @@ -1,4 +1,5 @@ """Tests for the rapid-response aside logic""" + from collections import defaultdict from datetime import datetime, timedelta from unittest.mock import Mock, PropertyMock, patch @@ -36,14 +37,15 @@ def setUp(self): ) self.scope_ids = make_scope_ids(self.aside_usage_key) self.aside_instance = RapidResponseAside( - scope_ids=self.scope_ids, - runtime=self.runtime + scope_ids=self.scope_ids, runtime=self.runtime ) - @data(*[ - [True, True], - [False, False], - ]) + @data( + *[ + [True, True], + [False, False], + ] + ) @unpack def test_student_view(self, enabled_value, should_render_aside): """ @@ -60,7 +62,9 @@ def test_student_view(self, enabled_value, should_render_aside): # If the block is enabled for rapid response, it should return a fragment # with non-empty content and should specify a JS initialization function assert bool(fragment.content) is should_render_aside - assert (fragment.js_init_fn == "RapidResponseAsideInit") is should_render_aside # noqa: E501 + assert ( + fragment.js_init_fn == "RapidResponseAsideInit" + ) is should_render_aside @data(True, False) def test_student_view_context(self, is_open): @@ -72,28 +76,45 @@ def test_student_view_context(self, is_open): "rapid_response_xblock.block.RapidResponseAside.has_open_run", new_callable=PropertyMock, ) as has_open_run_mock, patch( - "rapid_response_xblock.block.RapidResponseAside.enabled", - new=True + "rapid_response_xblock.block.RapidResponseAside.enabled", new=True ): has_open_run_mock.return_value = is_open fragment = self.aside_instance.student_view_aside(Mock()) assert f'data-open="{is_open}"' in fragment.content - @data(*[ - [BLOCK_PROBLEM_CATEGORY, {MULTIPLE_CHOICE_TYPE}, None, True], - [BLOCK_PROBLEM_CATEGORY, None, Mock(problem_types={MULTIPLE_CHOICE_TYPE}), True], # noqa: E501 - [None, {MULTIPLE_CHOICE_TYPE}, None, False], - ["invalid_category", {MULTIPLE_CHOICE_TYPE}, None, False], - [BLOCK_PROBLEM_CATEGORY, {"invalid_problem_type"}, None, False], - [BLOCK_PROBLEM_CATEGORY, {MULTIPLE_CHOICE_TYPE, "invalid_problem_type"}, None, False], # noqa: E501 - ]) + @data( + *[ + [BLOCK_PROBLEM_CATEGORY, {MULTIPLE_CHOICE_TYPE}, None, True], + [ + BLOCK_PROBLEM_CATEGORY, + None, + Mock(problem_types={MULTIPLE_CHOICE_TYPE}), + True, + ], + [None, {MULTIPLE_CHOICE_TYPE}, None, False], + ["invalid_category", {MULTIPLE_CHOICE_TYPE}, None, False], + [BLOCK_PROBLEM_CATEGORY, {"invalid_problem_type"}, None, False], + [ + BLOCK_PROBLEM_CATEGORY, + {MULTIPLE_CHOICE_TYPE, "invalid_problem_type"}, + None, + False, + ], + ] + ) @unpack - def test_should_apply_to_block(self, block_category, block_problem_types, block_descriptor, should_apply): # noqa: E501 + def test_should_apply_to_block( + self, block_category, block_problem_types, block_descriptor, should_apply + ): """ Test that should_apply_to_block only returns True for multiple choice problem blocks """ - block = Mock(category=block_category, problem_types=block_problem_types, descriptor=block_descriptor) # noqa: E501 + block = Mock( + category=block_category, + problem_types=block_problem_types, + descriptor=block_descriptor, + ) # `should_apply_to_block` uses `hasattr` to inspect the block object, and that # always returns True for Mock objects unless the attribute names are explicitly deleted. # noqa: E501 for block_attr in ["category", "descriptor", "problem_types"]: @@ -142,27 +163,32 @@ def test_toggle_block_open(self): self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 2 # noqa: PLR2004 - assert RapidResponseRun.objects.filter( - problem_usage_key=usage_key, - course_key=course_key, - open=True - ).exists() is True + assert ( + RapidResponseRun.objects.filter( + problem_usage_key=usage_key, course_key=course_key, open=True + ).exists() + is True + ) self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 2 # noqa: PLR2004 - assert RapidResponseRun.objects.filter( - problem_usage_key=usage_key, - course_key=course_key, - open=True - ).exists() is False + assert ( + RapidResponseRun.objects.filter( + problem_usage_key=usage_key, course_key=course_key, open=True + ).exists() + is False + ) self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 3 # noqa: PLR2004 - assert RapidResponseRun.objects.filter( - problem_usage_key=usage_key, - course_key=course_key, - open=True, - ).exists() is True + assert ( + RapidResponseRun.objects.filter( + problem_usage_key=usage_key, + course_key=course_key, + open=True, + ).exists() + is True + ) def test_toggle_block_open_duplicate(self): """Test that toggle_block_open_status only looks at the last run's open status""" # noqa: E501 @@ -181,12 +207,20 @@ def test_toggle_block_open_duplicate(self): self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 3 # noqa: PLR2004 - assert RapidResponseRun.objects.filter( - problem_usage_key=usage_key, - course_key=course_key, - ).order_by("-created").first().open is True + assert ( + RapidResponseRun.objects.filter( + problem_usage_key=usage_key, + course_key=course_key, + ) + .order_by("-created") + .first() + .open + is True + ) - @pytest.mark.skip(reason="Somehow the test runtime doesn't allow accessing xblock keys") # noqa: E501 + @pytest.mark.skip( + reason="Somehow the test runtime doesn't allow accessing xblock keys" + ) def test_toggle_block_enabled(self): """ Test that toggle_block_enabled changes 'enabled' field value @@ -199,10 +233,7 @@ def test_toggle_block_enabled(self): assert self.aside_instance.enabled is expected_enabled_value assert resp.json["is_enabled"] == self.aside_instance.enabled - @data(*[ - [True, 200], - [False, 403] - ]) + @data(*[[True, 200], [False, 403]]) @unpack def test_toggle_block_open_staff_only(self, is_staff, expected_status): """Test that toggle_block_open_status is only enabled for staff""" @@ -210,16 +241,15 @@ def test_toggle_block_open_staff_only(self, is_staff, expected_status): resp = self.aside_instance.toggle_block_open_status() assert resp.status_code == expected_status - @data(*[ - [True, 200], - [False, 403] - ]) + @data(*[[True, 200], [False, 403]]) @unpack def test_responses_staff_only(self, is_staff, expected_status): """ Test that only staff users should access the API """ - with patch.object(self.aside_instance, "is_staff", return_value=is_staff), self.patch_modulestore(): # noqa: E501 + with patch.object( + self.aside_instance, "is_staff", return_value=is_staff + ), self.patch_modulestore(): resp = self.aside_instance.responses() assert resp.status_code == expected_status @@ -247,24 +277,23 @@ def test_responses(self, has_runs): course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key - choices = [{ - "answer_id": "choice_0", - "answer_text": "First answer", - }, { - "answer_id": "choice_1", - "answer_text": "Second answer", - }] + choices = [ + { + "answer_id": "choice_0", + "answer_text": "First answer", + }, + { + "answer_id": "choice_1", + "answer_text": "Second answer", + }, + ] if has_runs: run1 = RapidResponseRun.objects.create( - course_key=course_id, - problem_usage_key=problem_id, - open=False + course_key=course_id, problem_usage_key=problem_id, open=False ) run2 = RapidResponseRun.objects.create( - course_key=course_id, - problem_usage_key=problem_id, - open=True + course_key=course_id, problem_usage_key=problem_id, open=True ) counts = { @@ -275,7 +304,7 @@ def test_responses(self, has_runs): choices[1]["answer_id"]: { run1.id: 5, run2.id: 6, - } + }, } expected_total_counts = { str(run1.id): 8, @@ -286,10 +315,11 @@ def test_responses(self, has_runs): expected_total_counts = {} with patch( - "rapid_response_xblock.block.RapidResponseAside.get_counts_for_problem", return_value=counts, # noqa: E501 + "rapid_response_xblock.block.RapidResponseAside.get_counts_for_problem", + return_value=counts, ) as get_counts_mock, patch( "rapid_response_xblock.block.RapidResponseAside.choices", - new_callable=PropertyMock + new_callable=PropertyMock, ) as get_choices_mock: get_choices_mock.return_value = choices resp = self.aside_instance.responses() @@ -312,7 +342,9 @@ def test_responses(self, has_runs): assert (now - minute) < parse_datetime(resp.json["server_now"]) < (now + minute) get_choices_mock.assert_called_once_with() - get_counts_mock.assert_called_once_with([run.id for run in run_queryset], choices) # noqa: E501 + get_counts_mock.assert_called_once_with( + [run.id for run in run_queryset], choices + ) def test_choices(self): """ @@ -323,7 +355,10 @@ def test_choices(self): assert self.aside_instance.choices == [ {"answer_id": "choice_0", "answer_text": "an incorrect answer"}, {"answer_id": "choice_1", "answer_text": "the correct answer"}, - {"answer_id": "choice_2", "answer_text": "a different incorrect answer"}, # noqa: E501 + { + "answer_id": "choice_2", + "answer_text": "a different incorrect answer", + }, ] def test_get_counts_for_problem(self): @@ -334,30 +369,32 @@ def test_get_counts_for_problem(self): problem_id = self.aside_instance.wrapped_block_usage_key run1 = RapidResponseRun.objects.create( - course_key=course_id, - problem_usage_key=problem_id, - open=False + course_key=course_id, problem_usage_key=problem_id, open=False ) run2 = RapidResponseRun.objects.create( - course_key=course_id, - problem_usage_key=problem_id, - open=True + course_key=course_id, problem_usage_key=problem_id, open=True ) choices = [ {"answer_id": "choice_0", "answer_text": "an incorrect answer"}, {"answer_id": "choice_1", "answer_text": "the correct answer"}, {"answer_id": "choice_2", "answer_text": "a different incorrect answer"}, ] - choices_lookup = {choice["answer_id"]: choice["answer_text"] for choice in choices} # noqa: E501 - counts = list(zip( - [choices[i]["answer_id"] for i in range(3)], - list(range(2, 5)), - [run1.id for _ in range(3)], - )) + list(zip( - [choices[i]["answer_id"] for i in range(3)], - [3, 0, 7], - [run2.id for _ in range(3)], - )) + choices_lookup = { + choice["answer_id"]: choice["answer_text"] for choice in choices + } + counts = list( + zip( + [choices[i]["answer_id"] for i in range(3)], + list(range(2, 5)), + [run1.id for _ in range(3)], + ) + ) + list( + zip( + [choices[i]["answer_id"] for i in range(3)], + [3, 0, 7], + [run2.id for _ in range(3)], + ) + ) counts_dict = defaultdict(dict) for answer_id, num_submissions, run_id in counts: @@ -373,12 +410,14 @@ def test_get_counts_for_problem(self): user_id=user.id, answer_id=answer_id, answer_text=answer_text, - event={} + event={}, ) run_ids = [run2.id, run1.id] - assert RapidResponseAside.get_counts_for_problem(run_ids, choices) == counts_dict # noqa: E501 + assert ( + RapidResponseAside.get_counts_for_problem(run_ids, choices) == counts_dict + ) def test_serialize_runs(self): """ @@ -399,8 +438,11 @@ def test_serialize_runs(self): open=True, ) - assert RapidResponseAside.serialize_runs([run2, run1]) == [{ - "id": run.id, - "created": run.created.isoformat(), - "open": run.open, - } for run in [run2, run1]] + assert RapidResponseAside.serialize_runs([run2, run1]) == [ + { + "id": run.id, + "created": run.created.isoformat(), + "open": run.open, + } + for run in [run2, run1] + ] diff --git a/src/rapid_response_xblock/tests/test_events.py b/src/rapid_response_xblock/tests/test_events.py index c661eeb9..12312bfc 100644 --- a/src/rapid_response_xblock/tests/test_events.py +++ b/src/rapid_response_xblock/tests/test_events.py @@ -1,4 +1,5 @@ """Just here to verify tests are running""" + from unittest import mock import pytest @@ -34,9 +35,7 @@ def setUp(self): problem_usage_key=UsageKey.from_string( "block-v1:SGAU+SGA101+2017_SGA+type@problem+block@2582bbb68672426297e525b49a383eb8" ), - course_key=CourseLocator.from_string( - "course-v1:SGAU+SGA101+2017_SGA" - ), + course_key=CourseLocator.from_string("course-v1:SGAU+SGA101+2017_SGA"), open=True, ) @@ -57,7 +56,8 @@ def get_problem(self): course = self.course store = modulestore() problem = next( - item for item in store.get_items(course.course_id) + item + for item in store.get_items(course.course_id) if item.__class__.__name__ == "ProblemBlockWithMixins" ) problem.bind_for_student(self.instructor) @@ -71,7 +71,7 @@ def get_problem(self): course_id=str(self.course_id), user_id=self.instructor.id, usage_key_string=str(problem.location), - will_recheck_access=True + will_recheck_access=True, ) def test_publish(self): @@ -87,7 +87,9 @@ def test_publish(self): # to all registered loggers. block = self.course with mock.patch.object( - SubmissionRecorder, "send", autospec=True, + SubmissionRecorder, + "send", + autospec=True, ) as send_patch: self.runtime.publish(block, event_type, event_object) # If call_count is 0, make sure you installed @@ -98,13 +100,18 @@ def test_publish(self): assert event["name"] == "event_name" assert event["context"]["event_source"] == "server" assert event["data"] == event_object - assert event["context"]["course_id"] == f"course-v1:{block.location.org}+{block.location.course}+{block.location.run}" # noqa: E501 + assert ( + event["context"]["course_id"] + == f"course-v1:{block.location.org}+{block.location.course}+{block.location.run}" # noqa: E501 + ) - @data(*[ - ["choice_0", "an incorrect answer"], - ["choice_1", "the correct answer"], - ["choice_2", "a different incorrect answer"], - ]) + @data( + *[ + ["choice_0", "an incorrect answer"], + ["choice_1", "the correct answer"], + ["choice_2", "a different incorrect answer"], + ] + ) @unpack def test_problem(self, clicked_answer_id, expected_answer_text): """ @@ -112,16 +119,18 @@ def test_problem(self, clicked_answer_id, expected_answer_text): """ problem = self.get_problem() - problem.handle_ajax("problem_check", { - "input_2582bbb68672426297e525b49a383eb8_2_1": clicked_answer_id - }) + problem.handle_ajax( + "problem_check", + {"input_2582bbb68672426297e525b49a383eb8_2_1": clicked_answer_id}, + ) assert RapidResponseSubmission.objects.count() == 1 obj = RapidResponseSubmission.objects.first() assert obj.user_id == self.instructor.id assert obj.run.course_key == self.course.course_id - assert obj.run.problem_usage_key.map_into_course( - self.course.course_id - ) == problem.location + assert ( + obj.run.problem_usage_key.map_into_course(self.course.course_id) + == problem.location + ) assert obj.answer_text == expected_answer_text assert obj.answer_id == clicked_answer_id @@ -131,17 +140,18 @@ def test_multiple_submissions(self): """ problem = self.get_problem() for answer in ("choice_0", "choice_1", "choice_2"): - problem.handle_ajax("problem_check", { - "input_2582bbb68672426297e525b49a383eb8_2_1": answer - }) + problem.handle_ajax( + "problem_check", {"input_2582bbb68672426297e525b49a383eb8_2_1": answer} + ) assert RapidResponseSubmission.objects.count() == 1 obj = RapidResponseSubmission.objects.first() assert obj.user_id == self.instructor.id assert obj.run.course_key == self.course.course_id - assert obj.run.problem_usage_key.map_into_course( - self.course.course_id - ) == problem.location + assert ( + obj.run.problem_usage_key.map_into_course(self.course.course_id) + == problem.location + ) # Answer is the first one clicked assert obj.answer_text == "a different incorrect answer" assert obj.answer_id == "choice_2" # the last one picked diff --git a/src/rapid_response_xblock/tests/test_utils.py b/src/rapid_response_xblock/tests/test_utils.py index dbc448dc..156a4425 100644 --- a/src/rapid_response_xblock/tests/test_utils.py +++ b/src/rapid_response_xblock/tests/test_utils.py @@ -1,4 +1,5 @@ """Tests for the util methods""" + import pytest from common.djangoapps.student.tests.factories import UserFactory from opaque_keys.edx.keys import UsageKey @@ -13,7 +14,9 @@ class TestUtils(RuntimeEnabledTestCase): def setUp(self): super().setUp() self.problem_run = RapidResponseRun.objects.create( - problem_usage_key=UsageKey.from_string("i4x://SGAU/SGA101/problem/2582bbb68672426297e525b49a383eb8"), + problem_usage_key=UsageKey.from_string( + "i4x://SGAU/SGA101/problem/2582bbb68672426297e525b49a383eb8" + ), course_key=self.course_id, open=True, ) @@ -25,7 +28,7 @@ def test_get_run_data_for_course(self): { "id": self.problem_run.id, "created": self.problem_run.created, - "problem_usage_key": self.problem_run.problem_usage_key + "problem_usage_key": self.problem_run.problem_usage_key, } ] @@ -45,10 +48,18 @@ def test_get_run_submission_data(self): } } - submission = RapidResponseSubmission.objects.create(run=self.problem_run, user=user, event=event_data) # noqa: E501 - expected = [[ - submission.created, submission.answer_text, submission.user.username, submission.user.email, answer # noqa: E501 - ]] + submission = RapidResponseSubmission.objects.create( + run=self.problem_run, user=user, event=event_data + ) + expected = [ + [ + submission.created, + submission.answer_text, + submission.user.username, + submission.user.email, + answer, + ] + ] submissions_data = get_run_submission_data(self.problem_run.id) assert submissions_data == expected diff --git a/src/rapid_response_xblock/tests/utils.py b/src/rapid_response_xblock/tests/utils.py index b8e86c24..66fa9427 100644 --- a/src/rapid_response_xblock/tests/utils.py +++ b/src/rapid_response_xblock/tests/utils.py @@ -1,4 +1,5 @@ """Utility functions and classes for the rapid response test suite""" + import shutil import tempfile from contextlib import contextmanager @@ -35,11 +36,11 @@ def make_scope_ids(usage_key): xblock.fields.ScopeIds: A ScopeIds object for the block for usage_key """ block_type = "fake" - runtime = TestRuntime(services={"field-data": KvsFieldData(kvs=DictKeyValueStore())}) # noqa: E501 - def_id = runtime.id_generator.create_definition(block_type) - return ScopeIds( - "user", block_type, def_id, usage_key + runtime = TestRuntime( + services={"field-data": KvsFieldData(kvs=DictKeyValueStore())} ) + def_id = runtime.id_generator.create_definition(block_type) + return ScopeIds("user", block_type, def_id, usage_key) def combine_dicts(dictionary, extras): @@ -89,7 +90,7 @@ def make_runtime(self, **kwargs): track_function=self.track_function, request_token=Mock(), course=self.course, - **kwargs + **kwargs, ) return self.block.runtime @@ -137,12 +138,14 @@ def wrap_runtime(*args, **kwargs): ProblemBlock, scope_ids=block.scope_ids, field_data=block._field_data, # pylint: disable=protected-access # noqa: SLF001 - for_parent=block.get_parent() + for_parent=block.get_parent(), ) return block - with patch("rapid_response_xblock.block.modulestore", autospec=True) as modulestore_mock: # noqa: E501 + with patch( + "rapid_response_xblock.block.modulestore", autospec=True + ) as modulestore_mock: modulestore_mock.return_value.get_item.side_effect = wrap_runtime yield modulestore_mock diff --git a/src/rapid_response_xblock/utils.py b/src/rapid_response_xblock/utils.py index d7c3bad5..118424d7 100644 --- a/src/rapid_response_xblock/utils.py +++ b/src/rapid_response_xblock/utils.py @@ -5,7 +5,9 @@ def get_run_data_for_course(course_key): """Util method to return problem runs corresponding to given course key""" - return RapidResponseRun.objects.filter(course_key=course_key).values("id", "created", "problem_usage_key") # noqa: E501 + return RapidResponseRun.objects.filter(course_key=course_key).values( + "id", "created", "problem_usage_key" + ) def get_run_submission_data(run_id): @@ -14,7 +16,13 @@ def get_run_submission_data(run_id): """ submissions = RapidResponseSubmission.objects.filter(run_id=run_id) return [ - [s.created, s.answer_text, s.user.username, s.user.email, get_answer_result(s.event)] # noqa: E501 + [ + s.created, + s.answer_text, + s.user.username, + s.user.email, + get_answer_result(s.event), + ] for s in submissions ] diff --git a/src/rapid_response_xblock/views.py b/src/rapid_response_xblock/views.py index 3e6d221d..10c95dcd 100644 --- a/src/rapid_response_xblock/views.py +++ b/src/rapid_response_xblock/views.py @@ -57,7 +57,10 @@ def toggle_rapid_response(request): except Exception as ex: # pylint: disable=broad-except # Updating and publishing item might throw errors when the initial state of a block is draft (Unpublished). # noqa: E501 # Let them flow silently - log.exception("Something went wrong with updating/publishing rapid response block." # noqa: E501 - " Most likely the block is in draft %s", ex) # noqa: TRY401 + log.exception( + "Something went wrong with updating/publishing rapid response block." + " Most likely the block is in draft %s", + ex, # noqa: TRY401 + ) return JsonResponse({"is_enabled": handler_block.enabled})