diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 17fc6d68c..c2dc845db 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,10 +2,10 @@ name: Python CI
 
 on:
   push:
-    branches: [ master ]
+    branches: [master]
   pull_request:
     branches:
-      - '**'
+    - '**'
   workflow_dispatch:
 
 concurrency:
@@ -19,8 +19,8 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-20.04]
-        python-version: [3.8]
-        toxenv: [py38-django32, py38-django42, quality, translations-django32, translations-django42]
+        python-version: ['3.8', '3.12']
+        toxenv: [py38-django42, quality, translations-django42, django42]
 
     steps:
     - name: checkout repo
diff --git a/drag_and_drop_v2/compat.py b/drag_and_drop_v2/compat.py
index 60ef79e69..f4f809aee 100644
--- a/drag_and_drop_v2/compat.py
+++ b/drag_and_drop_v2/compat.py
@@ -29,4 +29,5 @@ def get_grading_ignore_decoys_waffle_flag():
         #  Ref: https://github.com/openedx/public-engineering/issues/28
         return CourseWaffleFlag(WAFFLE_NAMESPACE, GRADING_IGNORE_DECOYS, __name__)
     except ValueError:
+        # pylint: disable=toggle-missing-annotation
         return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{GRADING_IGNORE_DECOYS}', __name__)
diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py
index 800b582bd..ad97a0eb4 100644
--- a/drag_and_drop_v2/drag_and_drop_v2.py
+++ b/drag_and_drop_v2/drag_and_drop_v2.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*- # pylint: disable=too-many-lines
+# -*- coding: utf-8 -*-  # pylint: disable=too-many-lines
 #
 """ Drag and Drop v2 XBlock """
 
@@ -11,9 +11,9 @@
 import json
 import logging
 
-import six.moves.urllib.error  # pylint: disable=import-error
-import six.moves.urllib.parse  # pylint: disable=import-error
-import six.moves.urllib.request  # pylint: disable=import-error
+import six.moves.urllib.error
+import six.moves.urllib.parse
+import six.moves.urllib.request
 import six
 import webob
 
@@ -45,12 +45,11 @@
 # Classes ###########################################################
 
 
-# pylint: disable=bad-continuation
 @XBlock.wants('settings')
 @XBlock.wants('replace_urls')
 @XBlock.wants('user')  # Using `needs` breaks the Course Outline page in Maple.
 @XBlock.needs('i18n')
-class DragAndDropBlock(
+class DragAndDropBlock(  # pylint: disable=too-many-instance-attributes
     ScorableXBlockMixin,
     XBlock,
     XBlockWithSettingsMixin,
@@ -257,7 +256,8 @@ def score(self):
         """
         return Score(self.raw_earned, self.raw_possible)
 
-    def max_score(self):  # pylint: disable=no-self-use
+    def max_score(self):
+
         """
         Return the problem's max score, which for DnDv2 always equals 1.
         Required by the grading system in the LMS.
@@ -293,6 +293,7 @@ def has_submitted_answer(self):
         """
         Returns True if the user has made a submission.
         """
+        # pylint: disable=unsubscriptable-object
         return self.fields['raw_earned'].is_set_on(self) or self.fields['grade'].is_set_on(self)
 
     def weighted_grade(self):
@@ -330,7 +331,7 @@ def _get_statici18n_js_url(self):
         return None
 
     @XBlock.supports("multi_device")  # Enable this block for use in the mobile app via webview
-    def student_view(self, context):
+    def student_view(self, context):    # pylint: disable=unused-argument
         """
         Player view, displayed to the student
         """
@@ -361,7 +362,7 @@ def student_view(self, context):
 
         return fragment
 
-    def student_view_data(self, context=None):
+    def student_view_data(self, context=None):  # pylint: disable=unused-argument
         """
         Get the configuration data for the student_view.
         The configuration is all the settings defined by the author, except for correct answers
@@ -419,7 +420,7 @@ def items_without_answers():
             # final feedback (data.feedback.finish) is not included - it may give away answers.
         }
 
-    def studio_view(self, context):
+    def studio_view(self, context):     # pylint: disable=unused-argument,useless-suppression
         """
         Editing view in Studio
         """
@@ -483,7 +484,7 @@ def studio_view(self, context):
         return fragment
 
     @XBlock.json_handler
-    def studio_submit(self, submissions, suffix=''):
+    def studio_submit(self, submissions, suffix=''):    # pylint: disable=unused-argument
         """
         Handles studio save.
         """
@@ -515,7 +516,7 @@ def _get_block_id(self):
         - In the workbench, use the usage_id.
         """
         if hasattr(self, 'location'):
-            return self.location.html_id()  # pylint: disable=no-member
+            return self.location.html_id()  # pylint: disable=no-member,useless-suppression
         else:
             return six.text_type(self.scope_ids.usage_id)
 
@@ -553,7 +554,7 @@ def _get_max_items_per_zone(submissions):
             return None
 
     @XBlock.json_handler
-    def drop_item(self, item_attempt, suffix=''):
+    def drop_item(self, item_attempt, suffix=''):   # pylint: disable=unused-argument
         """
         Handles dropping item into a zone.
         """
@@ -570,7 +571,7 @@ def drop_item(self, item_attempt, suffix=''):
             )
 
     @XBlock.json_handler
-    def do_attempt(self, data, suffix=''):
+    def do_attempt(self, data, suffix=''):      # pylint: disable=unused-argument
         """
         Checks submitted solution and returns feedback.
 
@@ -581,7 +582,7 @@ def do_attempt(self, data, suffix=''):
         self._validate_do_attempt()
 
         self.attempts += 1
-        # pylint: disable=fixme
+        # pylint: disable=fixme,useless-suppression
         # TODO: Refactor this method to "freeze" item_state and pass it to methods that need access to it.
         # These implicit dependencies between methods exist because most of them use `item_state` or other
         # fields, either as an "input" (i.e. read value) or as output (i.e. set value) or both. As a result,
@@ -610,7 +611,7 @@ def do_attempt(self, data, suffix=''):
         }
 
     @XBlock.json_handler
-    def publish_event(self, data, suffix=''):
+    def publish_event(self, data, suffix=''):       # pylint: disable=unused-argument
         """
         Handler to publish XBlock event from frontend
         """
@@ -623,7 +624,7 @@ def publish_event(self, data, suffix=''):
         return {'result': 'success'}
 
     @XBlock.json_handler
-    def reset(self, data, suffix=''):
+    def reset(self, data, suffix=''):       # pylint: disable=unused-argument
         """
         Resets problem to initial state
         """
@@ -631,7 +632,7 @@ def reset(self, data, suffix=''):
         return self._get_user_state()
 
     @XBlock.json_handler
-    def show_answer(self, data, suffix=''):
+    def show_answer(self, data, suffix=''):     # pylint: disable=unused-argument
         """
         Returns correct answer in assessment mode.
 
@@ -662,7 +663,7 @@ def show_answer(self, data, suffix=''):
         return answer
 
     @XBlock.json_handler
-    def expand_static_url(self, url, suffix=''):
+    def expand_static_url(self, url, suffix=''):        # pylint: disable=unused-argument
         """ AJAX-accessible handler for expanding URLs to static [image] files """
         return {'url': self._expand_static_url(url)}
 
@@ -712,7 +713,7 @@ def has_submission_deadline_passed(self):
         functionality.
         """
         if hasattr(self, "has_deadline_passed"):
-            return self.has_deadline_passed()  # pylint: disable=no-member
+            return self.has_deadline_passed()  # pylint: disable=no-member,useless-suppression
         else:
             return False
 
@@ -776,7 +777,7 @@ def is_answer_available(self):
         return check_permissions_function()
 
     @XBlock.handler
-    def student_view_user_state(self, request, suffix=''):
+    def student_view_user_state(self, request, suffix=''):      # pylint: disable=unused-argument
         """ GET all user-specific data, and any applicable feedback """
         data = self._get_user_state()
         return webob.Response(body=json.dumps(data).encode('utf-8'), content_type='application/json')
@@ -949,7 +950,7 @@ def _mark_complete_and_publish_grade(self):
         """
         Helper method to update `self.completed` and submit grade event if appropriate conditions met.
         """
-        # pylint: disable=fixme
+        # pylint: disable=fixme,useless-suppression
         # TODO: (arguable) split this method into "clean" functions (with no side effects and implicit state)
         # This method implicitly depends on self.item_state (via is_correct and _learner_raw_score)
         # and also updates self.raw_earned if some conditions are met. As a result this method implies some order of
@@ -1052,7 +1053,9 @@ def _expand_static_url(self, url):
             # edX Studio uses a different runtime for 'studio_view' than 'student_view',
             # and the 'studio_view' runtime doesn't provide the replace_urls API.
             try:
-                from common.djangoapps.static_replace import replace_static_urls  # pylint: disable=import-error
+                # pylint: disable=import-outside-toplevel
+                from common.djangoapps.static_replace import replace_static_urls
+                # pylint: disable=redundant-u-string-prefix
                 url = replace_static_urls(u'"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
             except ImportError:
                 pass
@@ -1108,7 +1111,7 @@ def _get_preferred_zone(zone_count, zones):
             return preferred_zone
 
         # Set states of all items dropped in correct zones
-        for item_id in self.item_state:
+        for item_id in self.item_state:     # pylint: disable=consider-using-dict-items
             if self.item_state[item_id]['correct']:
                 state[item_id] = self.item_state[item_id]
                 correct_items.add(item_id)
@@ -1252,7 +1255,7 @@ def _get_raw_earned_if_set(self):
         Returns student's grade if already explicitly set, otherwise returns None.
         This is different from self.raw_earned which returns 0 by default.
         """
-        if self.fields['raw_earned'].is_set_on(self):
+        if self.fields['raw_earned'].is_set_on(self):   # pylint: disable=unsubscriptable-object
             return self.raw_earned
         else:
             return None
@@ -1262,7 +1265,7 @@ def _get_weighted_earned_if_set(self):
         Returns student's grade with the problem weight applied if set, otherwise
         None.
         """
-        if self.fields['raw_earned'].is_set_on(self):
+        if self.fields['raw_earned'].is_set_on(self):   # pylint: disable=unsubscriptable-object
             return self.weighted_grade()
         else:
             return None
@@ -1319,7 +1322,7 @@ def index_dictionary(self):
         # values may be numeric / string or dict
         # default implementation is an empty dict
 
-        xblock_body = super(DragAndDropBlock, self).index_dictionary()
+        xblock_body = super(DragAndDropBlock, self).index_dictionary()  # pylint: disable=super-with-arguments
 
         zones_display_names = {
             "zone_{}_display_name".format(zone_i):
diff --git a/drag_and_drop_v2/utils.py b/drag_and_drop_v2/utils.py
index 1ede9da06..e9075ee67 100644
--- a/drag_and_drop_v2/utils.py
+++ b/drag_and_drop_v2/utils.py
@@ -80,7 +80,7 @@ def sanitize_html(raw_body: str) -> str:
     """
     Remove not allowed HTML tags to mitigate XSS vulnerabilities.
     """
-    bleach_options = dict(
+    bleach_options = dict(      # pylint: disable=use-dict-literal
         tags=ALLOWED_TAGS,
         protocols=bleach.ALLOWED_PROTOCOLS,
         strip=True,
@@ -165,8 +165,8 @@ def not_placed(number, ngettext=ngettext_fallback):
         ).format(missing_count=number)
 
 
-FeedbackMessage = namedtuple("FeedbackMessage", ["message", "message_class"])  # pylint: disable=invalid-name
-ItemStats = namedtuple(  # pylint: disable=invalid-name
+FeedbackMessage = namedtuple("FeedbackMessage", ["message", "message_class"])
+ItemStats = namedtuple(
     'ItemStats',
     ["required", "placed", "correctly_placed", "decoy", "decoy_in_bank"]
 )
@@ -238,7 +238,7 @@ def apply_item_state_migrations(self, item_id, item_state):
         return self._apply_migration(item_id, item_state, migrations)
 
     @classmethod
-    def _zone_v1_to_v2(cls, unused_zone_id, zone):
+    def _zone_v1_to_v2(cls, unused_zone_id, zone):      # pylint: disable=unused-argument
         """
         Migrates zone data from v1.0 format to v2.0 format.
 
@@ -257,7 +257,7 @@ def _zone_v1_to_v2(cls, unused_zone_id, zone):
         return zone
 
     @classmethod
-    def _zone_v2_to_v2p1(cls, unused_zone_id, zone):
+    def _zone_v2_to_v2p1(cls, unused_zone_id, zone):    # pylint: disable=unused-argument
         """
         Migrates zone data from v2.0 to v2.1
 
@@ -279,7 +279,7 @@ def _zone_v2_to_v2p1(cls, unused_zone_id, zone):
         return zone
 
     @classmethod
-    def _item_state_v1_to_v1p5(cls, unused_item_id, item):
+    def _item_state_v1_to_v1p5(cls, unused_item_id, item):  # pylint: disable=unused-argument
         """
         Migrates item_state from v1.0 to v1.5
 
@@ -295,7 +295,7 @@ def _item_state_v1_to_v1p5(cls, unused_item_id, item):
             return {'top': item[0], 'left': item[1]}
 
     @classmethod
-    def _item_state_v1p5_to_v2(cls, unused_item_id, item):
+    def _item_state_v1p5_to_v2(cls, unused_item_id, item):  # pylint: disable=unused-argument
         """
         Migrates item_state from v1.5 to v2.0
 
diff --git a/pylintrc b/pylintrc
index f1dcfe842..4ea6a01ab 100644
--- a/pylintrc
+++ b/pylintrc
@@ -3,25 +3,34 @@ reports=no
 
 [FORMAT]
 max-line-length=120
-max-module-lines=1500
 
 [MESSAGES CONTROL]
 disable=
+    attribute-defined-outside-init,
     locally-disabled,
+    missing-docstring,
+    abstract-class-little-used,
     too-many-ancestors,
-    too-many-instance-attributes,
     too-few-public-methods,
     too-many-public-methods,
-    unused-argument,
+    invalid-name,
+    no-member,
+    star-args,
+    no-else-return,
+    useless-object-inheritance,
     unsubscriptable-object,
-    no-else-return
+    bad-option-value,
+    len-as-condition,
+    useless-super-delegation,
+    bad-option-value,
+    missing-docstring,
+    no-member,
+    wrong-import-order,
+    line-too-long,
+    consider-using-f-string
 
 [SIMILARITIES]
-min-similarity-lines=4
+min-similarity-lines=6
 
 [OPTIONS]
-good-names=_,__,logger,loader
-method-rgx=_?[a-z_][a-z0-9_]{2,40}$
-function-rgx=_?[a-z_][a-z0-9_]{2,40}$
-method-name-hint=_?[a-z_][a-z0-9_]{2,40}$
-function-name-hint=_?[a-z_][a-z0-9_]{2,40}$
+max-args=6
diff --git a/pylintrc_tweaks b/pylintrc_tweaks
new file mode 100644
index 000000000..57a87d6ff
--- /dev/null
+++ b/pylintrc_tweaks
@@ -0,0 +1,19 @@
+[MASTER]
+load-plugins = edx_lint.pylint
+
+[FORMAT]
+max-line-length=120
+max-module-lines=1500
+
+[MESSAGES CONTROL]
+disable =
+    locally-disabled,
+    too-many-ancestors,
+    too-many-instance-attributes,
+    too-few-public-methods,
+    too-many-public-methods,
+    unused-argument,
+    unsubscriptable-object,
+    no-else-return,
+    consider-using-f-string,
+    feature-toggle-needs-doc
diff --git a/requirements/base.txt b/requirements/base.txt
index 62a6ed0f6..a9ff58c50 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -8,15 +8,19 @@ appdirs==1.4.4
     # via fs
 asgiref==3.7.2
     # via django
+backports-zoneinfo==0.2.1 ; python_version < "3.9"
+    # via
+    #   -c requirements/constraints.txt
+    #   django
 bleach[css]==6.1.0
     # via -r requirements/base.in
-boto3==1.34.49
+boto3==1.34.66
     # via fs-s3fs
-botocore==1.34.49
+botocore==1.34.66
     # via
     #   boto3
     #   s3transfer
-django==3.2.24
+django==4.2.11
     # via
     #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
     #   django-appconf
@@ -49,17 +53,15 @@ markupsafe==2.1.5
     #   xblock
 openedx-django-pyfs==3.5.0
     # via xblock
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
     # via
     #   botocore
     #   xblock
 pytz==2024.1
-    # via
-    #   django
-    #   xblock
+    # via xblock
 pyyaml==6.0.1
     # via xblock
-s3transfer==0.10.0
+s3transfer==0.10.1
     # via boto3
 simplejson==3.19.2
     # via xblock
@@ -86,7 +88,9 @@ webencodings==0.5.1
 webob==1.8.7
     # via xblock
 xblock[django]==1.10.0
-    # via -r requirements/base.in
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/base.in
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools
diff --git a/requirements/ci.txt b/requirements/ci.txt
index e7a876093..9f335db1e 100644
--- a/requirements/ci.txt
+++ b/requirements/ci.txt
@@ -4,7 +4,7 @@
 #
 #    make upgrade
 #
-cachetools==5.3.2
+cachetools==5.3.3
     # via tox
 chardet==5.2.0
     # via tox
@@ -16,7 +16,7 @@ filelock==3.13.1
     # via
     #   tox
     #   virtualenv
-packaging==23.2
+packaging==24.0
     # via
     #   pyproject-api
     #   tox
@@ -32,7 +32,7 @@ tomli==2.0.1
     # via
     #   pyproject-api
     #   tox
-tox==4.13.0
+tox==4.14.1
     # via -r requirements/ci.in
 virtualenv==20.25.1
     # via tox
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 289e45347..ee9de83a7 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -11,4 +11,7 @@
 # Common constraints for edx repos
 -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
 
-pylint==2.4.2
+# greater version has some breaking changes. Fix this in separate PR.
+xblock[django]==1.10.0
+
+backports-zoneinfo[tzdata]==0.2.1 ; python_version < "3.9"
\ No newline at end of file
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 90445782a..3fe6f4835 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -16,31 +16,36 @@ asgiref==3.7.2
     # via
     #   -r requirements/quality.txt
     #   django
-astroid==2.3.3
+astroid==3.1.0
     # via
     #   -r requirements/quality.txt
     #   pylint
     #   pylint-celery
+backports-zoneinfo==0.2.1 ; python_version < "3.9"
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/quality.txt
+    #   django
 binaryornot==0.4.4
     # via
     #   -r requirements/quality.txt
     #   cookiecutter
 bleach[css]==6.1.0
     # via -r requirements/quality.txt
-boto3==1.34.49
+boto3==1.34.66
     # via
     #   -r requirements/quality.txt
     #   fs-s3fs
-botocore==1.34.49
+botocore==1.34.66
     # via
     #   -r requirements/quality.txt
     #   boto3
     #   s3transfer
-build==1.0.3
+build==1.1.1
     # via
     #   -r requirements/pip-tools.txt
     #   pip-tools
-cachetools==5.3.2
+cachetools==5.3.3
     # via
     #   -r requirements/ci.txt
     #   tox
@@ -71,7 +76,7 @@ click-log==0.4.0
     # via
     #   -r requirements/quality.txt
     #   edx-lint
-code-annotations==1.6.0
+code-annotations==1.7.0
     # via
     #   -r requirements/quality.txt
     #   edx-lint
@@ -83,17 +88,21 @@ cookiecutter==2.6.0
     # via
     #   -r requirements/quality.txt
     #   xblock-sdk
-coverage[toml]==7.4.3
+coverage[toml]==7.4.4
     # via
     #   -r requirements/quality.txt
     #   pytest-cov
-ddt==1.7.1
+ddt==1.7.2
     # via -r requirements/quality.txt
+dill==0.3.8
+    # via
+    #   -r requirements/quality.txt
+    #   pylint
 distlib==0.3.8
     # via
     #   -r requirements/ci.txt
     #   virtualenv
-django==3.2.24
+django==4.2.11
     # via
     #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
     #   -r requirements/quality.txt
@@ -136,15 +145,16 @@ idna==3.6
     # via
     #   -r requirements/quality.txt
     #   requests
-importlib-metadata==7.0.1
+importlib-metadata==6.11.0
     # via
+    #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
     #   -r requirements/pip-tools.txt
     #   build
 iniconfig==2.0.0
     # via
     #   -r requirements/quality.txt
     #   pytest
-isort==4.3.21
+isort==5.13.2
     # via
     #   -r requirements/quality.txt
     #   pylint
@@ -162,10 +172,6 @@ lazy==1.6
     # via
     #   -r requirements/quality.txt
     #   xblock
-lazy-object-proxy==1.4.3
-    # via
-    #   -r requirements/quality.txt
-    #   astroid
 lxml==5.1.0
     # via
     #   -r requirements/quality.txt
@@ -186,7 +192,7 @@ markupsafe==2.1.5
     #   jinja2
     #   mako
     #   xblock
-mccabe==0.6.1
+mccabe==0.7.0
     # via
     #   -r requirements/quality.txt
     #   pylint
@@ -200,7 +206,7 @@ openedx-django-pyfs==3.5.0
     # via
     #   -r requirements/quality.txt
     #   xblock
-packaging==23.2
+packaging==24.0
     # via
     #   -r requirements/ci.txt
     #   -r requirements/pip-tools.txt
@@ -217,11 +223,13 @@ pbr==6.0.0
     # via
     #   -r requirements/quality.txt
     #   stevedore
-pip-tools==7.4.0
+pip-tools==7.4.1
     # via -r requirements/pip-tools.txt
 platformdirs==4.2.0
     # via
     #   -r requirements/ci.txt
+    #   -r requirements/quality.txt
+    #   pylint
     #   tox
     #   virtualenv
 pluggy==1.4.0
@@ -240,9 +248,8 @@ pygments==2.17.2
     # via
     #   -r requirements/quality.txt
     #   rich
-pylint==2.4.2
+pylint==3.1.0
     # via
-    #   -c requirements/constraints.txt
     #   -r requirements/quality.txt
     #   edx-lint
     #   pylint-celery
@@ -274,7 +281,7 @@ pyproject-hooks==1.0.0
     #   -r requirements/pip-tools.txt
     #   build
     #   pip-tools
-pytest==8.0.2
+pytest==8.1.1
     # via
     #   -r requirements/quality.txt
     #   pytest-cov
@@ -283,7 +290,7 @@ pytest-cov==4.1.0
     # via -r requirements/quality.txt
 pytest-django==4.8.0
     # via -r requirements/quality.txt
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
     # via
     #   -r requirements/quality.txt
     #   arrow
@@ -297,7 +304,6 @@ python-slugify==8.0.4
 pytz==2024.1
     # via
     #   -r requirements/quality.txt
-    #   django
     #   xblock
 pyyaml==6.0.1
     # via
@@ -311,11 +317,11 @@ requests==2.31.0
     #   -r requirements/quality.txt
     #   cookiecutter
     #   xblock-sdk
-rich==13.7.0
+rich==13.7.1
     # via
     #   -r requirements/quality.txt
     #   cookiecutter
-s3transfer==0.10.0
+s3transfer==0.10.1
     # via
     #   -r requirements/quality.txt
     #   boto3
@@ -327,7 +333,6 @@ simplejson==3.19.2
 six==1.16.0
     # via
     #   -r requirements/quality.txt
-    #   astroid
     #   bleach
     #   edx-lint
     #   fs
@@ -357,13 +362,18 @@ tomli==2.0.1
     #   build
     #   coverage
     #   pip-tools
+    #   pylint
     #   pyproject-api
     #   pyproject-hooks
     #   pytest
     #   tox
-tox==4.13.0
+tomlkit==0.12.4
+    # via
+    #   -r requirements/quality.txt
+    #   pylint
+tox==4.14.1
     # via -r requirements/ci.txt
-types-python-dateutil==2.8.19.20240106
+types-python-dateutil==2.9.0.20240316
     # via
     #   -r requirements/quality.txt
     #   arrow
@@ -371,6 +381,8 @@ typing-extensions==4.10.0
     # via
     #   -r requirements/quality.txt
     #   asgiref
+    #   astroid
+    #   pylint
     #   rich
 urllib3==1.26.18
     # via
@@ -396,21 +408,18 @@ webob==1.8.7
     #   -r requirements/quality.txt
     #   xblock
     #   xblock-sdk
-wheel==0.42.0
+wheel==0.43.0
     # via
     #   -r requirements/pip-tools.txt
     #   pip-tools
-wrapt==1.11.2
-    # via
-    #   -r requirements/quality.txt
-    #   astroid
 xblock[django]==1.10.0
     # via
+    #   -c requirements/constraints.txt
     #   -r requirements/quality.txt
     #   xblock-sdk
 xblock-sdk==0.7.0
     # via -r requirements/quality.txt
-zipp==3.17.0
+zipp==3.18.1
     # via
     #   -r requirements/pip-tools.txt
     #   importlib-metadata
diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt
index 44c48d996..aad9d3825 100644
--- a/requirements/pip-tools.txt
+++ b/requirements/pip-tools.txt
@@ -4,15 +4,17 @@
 #
 #    make upgrade
 #
-build==1.0.3
+build==1.1.1
     # via pip-tools
 click==8.1.7
     # via pip-tools
-importlib-metadata==7.0.1
-    # via build
-packaging==23.2
+importlib-metadata==6.11.0
+    # via
+    #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+    #   build
+packaging==24.0
     # via build
-pip-tools==7.4.0
+pip-tools==7.4.1
     # via -r requirements/pip-tools.in
 pyproject-hooks==1.0.0
     # via
@@ -23,9 +25,9 @@ tomli==2.0.1
     #   build
     #   pip-tools
     #   pyproject-hooks
-wheel==0.42.0
+wheel==0.43.0
     # via pip-tools
-zipp==3.17.0
+zipp==3.18.1
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 66656035b..cf449024a 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -4,11 +4,11 @@
 #
 #    make upgrade
 #
-wheel==0.42.0
+wheel==0.43.0
     # via -r requirements/pip.in
 
 # The following packages are considered to be unsafe in a requirements file:
 pip==24.0
     # via -r requirements/pip.in
-setuptools==69.1.1
+setuptools==69.2.0
     # via -r requirements/pip.in
diff --git a/requirements/quality.txt b/requirements/quality.txt
index 6a8d97eee..43187d715 100644
--- a/requirements/quality.txt
+++ b/requirements/quality.txt
@@ -16,21 +16,26 @@ asgiref==3.7.2
     # via
     #   -r requirements/test.txt
     #   django
-astroid==2.3.3
+astroid==3.1.0
     # via
     #   pylint
     #   pylint-celery
+backports-zoneinfo==0.2.1 ; python_version < "3.9"
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/test.txt
+    #   django
 binaryornot==0.4.4
     # via
     #   -r requirements/test.txt
     #   cookiecutter
 bleach[css]==6.1.0
     # via -r requirements/test.txt
-boto3==1.34.49
+boto3==1.34.66
     # via
     #   -r requirements/test.txt
     #   fs-s3fs
-botocore==1.34.49
+botocore==1.34.66
     # via
     #   -r requirements/test.txt
     #   boto3
@@ -56,19 +61,21 @@ click==8.1.7
     #   edx-lint
 click-log==0.4.0
     # via edx-lint
-code-annotations==1.6.0
+code-annotations==1.7.0
     # via edx-lint
 cookiecutter==2.6.0
     # via
     #   -r requirements/test.txt
     #   xblock-sdk
-coverage[toml]==7.4.3
+coverage[toml]==7.4.4
     # via
     #   -r requirements/test.txt
     #   pytest-cov
-ddt==1.7.1
+ddt==1.7.2
     # via -r requirements/test.txt
-django==3.2.24
+dill==0.3.8
+    # via pylint
+django==4.2.11
     # via
     #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
     #   -r requirements/test.txt
@@ -110,7 +117,7 @@ iniconfig==2.0.0
     # via
     #   -r requirements/test.txt
     #   pytest
-isort==4.3.21
+isort==5.13.2
     # via pylint
 jinja2==3.1.3
     # via
@@ -126,8 +133,6 @@ lazy==1.6
     # via
     #   -r requirements/test.txt
     #   xblock
-lazy-object-proxy==1.4.3
-    # via astroid
 lxml==5.1.0
     # via
     #   -r requirements/test.txt
@@ -148,7 +153,7 @@ markupsafe==2.1.5
     #   jinja2
     #   mako
     #   xblock
-mccabe==0.6.1
+mccabe==0.7.0
     # via pylint
 mdurl==0.1.2
     # via
@@ -160,7 +165,7 @@ openedx-django-pyfs==3.5.0
     # via
     #   -r requirements/test.txt
     #   xblock
-packaging==23.2
+packaging==24.0
     # via
     #   -r requirements/test.txt
     #   pytest
@@ -170,6 +175,8 @@ path==16.10.0
     #   edx-i18n-tools
 pbr==6.0.0
     # via stevedore
+platformdirs==4.2.0
+    # via pylint
 pluggy==1.4.0
     # via
     #   -r requirements/test.txt
@@ -184,9 +191,8 @@ pygments==2.17.2
     # via
     #   -r requirements/test.txt
     #   rich
-pylint==2.4.2
+pylint==3.1.0
     # via
-    #   -c requirements/constraints.txt
     #   edx-lint
     #   pylint-celery
     #   pylint-django
@@ -203,7 +209,7 @@ pypng==0.20220715.0
     # via
     #   -r requirements/test.txt
     #   xblock-sdk
-pytest==8.0.2
+pytest==8.1.1
     # via
     #   -r requirements/test.txt
     #   pytest-cov
@@ -212,7 +218,7 @@ pytest-cov==4.1.0
     # via -r requirements/test.txt
 pytest-django==4.8.0
     # via -r requirements/test.txt
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
     # via
     #   -r requirements/test.txt
     #   arrow
@@ -226,7 +232,6 @@ python-slugify==8.0.4
 pytz==2024.1
     # via
     #   -r requirements/test.txt
-    #   django
     #   xblock
 pyyaml==6.0.1
     # via
@@ -240,11 +245,11 @@ requests==2.31.0
     #   -r requirements/test.txt
     #   cookiecutter
     #   xblock-sdk
-rich==13.7.0
+rich==13.7.1
     # via
     #   -r requirements/test.txt
     #   cookiecutter
-s3transfer==0.10.0
+s3transfer==0.10.1
     # via
     #   -r requirements/test.txt
     #   boto3
@@ -256,7 +261,6 @@ simplejson==3.19.2
 six==1.16.0
     # via
     #   -r requirements/test.txt
-    #   astroid
     #   bleach
     #   edx-lint
     #   fs
@@ -280,8 +284,11 @@ tomli==2.0.1
     # via
     #   -r requirements/test.txt
     #   coverage
+    #   pylint
     #   pytest
-types-python-dateutil==2.8.19.20240106
+tomlkit==0.12.4
+    # via pylint
+types-python-dateutil==2.9.0.20240316
     # via
     #   -r requirements/test.txt
     #   arrow
@@ -289,6 +296,8 @@ typing-extensions==4.10.0
     # via
     #   -r requirements/test.txt
     #   asgiref
+    #   astroid
+    #   pylint
     #   rich
 urllib3==1.26.18
     # via
@@ -310,10 +319,9 @@ webob==1.8.7
     #   -r requirements/test.txt
     #   xblock
     #   xblock-sdk
-wrapt==1.11.2
-    # via astroid
 xblock[django]==1.10.0
     # via
+    #   -c requirements/constraints.txt
     #   -r requirements/test.txt
     #   xblock-sdk
 xblock-sdk==0.7.0
diff --git a/requirements/test.txt b/requirements/test.txt
index 2c6a0b980..cada273a2 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -14,15 +14,20 @@ asgiref==3.7.2
     # via
     #   -r requirements/base.txt
     #   django
+backports-zoneinfo==0.2.1 ; python_version < "3.9"
+    # via
+    #   -c requirements/constraints.txt
+    #   -r requirements/base.txt
+    #   django
 binaryornot==0.4.4
     # via cookiecutter
 bleach[css]==6.1.0
     # via -r requirements/base.txt
-boto3==1.34.49
+boto3==1.34.66
     # via
     #   -r requirements/base.txt
     #   fs-s3fs
-botocore==1.34.49
+botocore==1.34.66
     # via
     #   -r requirements/base.txt
     #   boto3
@@ -37,10 +42,11 @@ click==8.1.7
     # via cookiecutter
 cookiecutter==2.6.0
     # via xblock-sdk
-coverage[toml]==7.4.3
+coverage[toml]==7.4.4
     # via pytest-cov
-ddt==1.7.1
+ddt==1.7.2
     # via -r requirements/test.in
+django==4.2.11
     # via
     #   -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
     #   -r requirements/base.txt
@@ -112,7 +118,7 @@ openedx-django-pyfs==3.5.0
     #   -r requirements/base.txt
     #   -r requirements/test.in
     #   xblock
-packaging==23.2
+packaging==24.0
     # via pytest
 path==16.10.0
     # via edx-i18n-tools
@@ -124,7 +130,7 @@ pygments==2.17.2
     # via rich
 pypng==0.20220715.0
     # via xblock-sdk
-pytest==8.0.2
+pytest==8.1.1
     # via
     #   pytest-cov
     #   pytest-django
@@ -132,7 +138,7 @@ pytest-cov==4.1.0
     # via -r requirements/test.in
 pytest-django==4.8.0
     # via -r requirements/test.in
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
     # via
     #   -r requirements/base.txt
     #   arrow
@@ -143,7 +149,6 @@ python-slugify==8.0.4
 pytz==2024.1
     # via
     #   -r requirements/base.txt
-    #   django
     #   xblock
 pyyaml==6.0.1
     # via
@@ -155,9 +160,9 @@ requests==2.31.0
     # via
     #   cookiecutter
     #   xblock-sdk
-rich==13.7.0
+rich==13.7.1
     # via cookiecutter
-s3transfer==0.10.0
+s3transfer==0.10.1
     # via
     #   -r requirements/base.txt
     #   boto3
@@ -187,7 +192,7 @@ tomli==2.0.1
     # via
     #   coverage
     #   pytest
-types-python-dateutil==2.8.19.20240106
+types-python-dateutil==2.9.0.20240316
     # via arrow
 typing-extensions==4.10.0
     # via
@@ -216,6 +221,7 @@ webob==1.8.7
     #   xblock-sdk
 xblock[django]==1.10.0
     # via
+    #   -c requirements/constraints.txt
     #   -r requirements/base.txt
     #   xblock-sdk
 xblock-sdk==0.7.0
diff --git a/setup.py b/setup.py
index c4ea4b4ed..0a8aa5ad2 100644
--- a/setup.py
+++ b/setup.py
@@ -118,8 +118,8 @@ def package_data(pkg, root_list):
     classifiers=[
         'Programming Language :: Python',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.12',
         'Framework :: Django',
-        'Framework :: Django :: 3.2',
         'Framework :: Django :: 4.2',
     ],
     url='https://github.com/openedx/xblock-drag-and-drop-v2',
diff --git a/tests/pylintrc b/tests/pylintrc
index 65b832400..4ea6a01ab 100644
--- a/tests/pylintrc
+++ b/tests/pylintrc
@@ -26,7 +26,8 @@ disable=
     missing-docstring,
     no-member,
     wrong-import-order,
-    line-too-long
+    line-too-long,
+    consider-using-f-string
 
 [SIMILARITIES]
 min-similarity-lines=6
diff --git a/tox.ini b/tox.ini
index 87c416a16..dde764c71 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,13 @@
 [tox]
-envlist = py38-django{32,42},quality,translations-django{32,42}
+envlist = py{38, 312},quality,translations-django{42}
 
 [pycodestyle]
 exclude = .git,.tox
 
 [pytest]
-# Use the workbench settings file.
 DJANGO_SETTINGS_MODULE = workbench.settings
 addopts = --cov-report term-missing --cov-report xml
-filterwarnings =
+filterwarnings = 
     ignore::DeprecationWarning
     ignore::FutureWarning
 
@@ -16,39 +15,40 @@ filterwarnings =
 omit = drag_and_drop_v2/translations/settings.py
 
 [testenv]
-allowlist_externals =
+allowlist_externals = 
     make
     mkdir
-deps =
-    django32: Django>=3.2,<4.0
+deps = 
     django42: Django>=4.2,<4.3
     -r{toxinidir}/requirements/test.txt
-commands =
+commands = 
     mkdir -p var
     pytest {posargs:tests/unit/ --cov drag_and_drop_v2}
 
 [testenv:quality]
-deps =
+DJANGO_SETTINGS_MODULE = workbench.settings
+deps = 
     -r{toxinidir}/requirements/quality.txt
-commands =
+commands = 
     pycodestyle drag_and_drop_v2 tests manage.py setup.py --max-line-length=120
-	pylint drag_and_drop_v2
-	pylint tests --rcfile=tests/pylintrc
+    pylint drag_and_drop_v2
+    pylint tests --rcfile=tests/pylintrc
 
 [testenv:translations-django32]
-allowlist_externals =
+allowlist_externals = 
     make
-deps =
+deps = 
     Django>=3.2,<4.0
     -r{toxinidir}/requirements/test.txt
-commands =
+commands = 
     make check_translations_up_to_date
 
 [testenv:translations-django42]
-allowlist_externals =
+allowlist_externals = 
     make
-deps =
+deps = 
     Django>=4.2,<4.3
     -r{toxinidir}/requirements/test.txt
-commands =
+commands = 
     make check_translations_up_to_date
+