diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 70a80a69b1fcded87ae76b68412f2268bb9961f3..fa5b7b390a99e3bd3a8ac4cedb781809319ade53 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,7 +14,7 @@ include:
 
   # Support uploading to PyPI
   - project: 'uis/devops/continuous-delivery/ci-templates'
-    ref: v1.5.0
+    ref: v3.7.1
     file: '/pypi-release.yml'
 
 variables:
@@ -26,7 +26,7 @@ coverage:
   extends: .test
   variables:
     TOX_ENVLIST: coverage
-    PYTHON_VERSION: "3.9"
+    PYTHON_VERSION: "3.11"
 
   # Look for the summary line output from coverage's text report. The
   # parentheses are used to indicate which portion of the report contains the
@@ -38,28 +38,33 @@ flake8:
   extends: .test
   variables:
     TOX_ENVLIST: flake8
-    PYTHON_VERSION: "3.9"
+    PYTHON_VERSION: "3.11"
 
 # Run test suite against supported Python/Django combinations.
-python36-django22:
-  extends: .py36
+python38-django42:
+  extends: .py38
   variables:
-    TOX_DJANGO_FRAGMENT: "django22"
+    TOX_DJANGO_FRAGMENT: "django42"
 
-python37-django22:
-  extends: .py37
+python39-django42:
+  extends: .py39
   variables:
-    TOX_DJANGO_FRAGMENT: "django22"
+    TOX_DJANGO_FRAGMENT: "django42"
 
-python38-django22:
-  extends: .py38
+python310-django42:
+  extends: .py310
   variables:
-    TOX_DJANGO_FRAGMENT: "django22"
+    TOX_DJANGO_FRAGMENT: "django42"
 
-python39-django22:
-  extends: .py39
+python311-django42:
+  extends: .py311
   variables:
-    TOX_DJANGO_FRAGMENT: "django22"
+    TOX_DJANGO_FRAGMENT: "django42"
+
+python312-django42:
+  extends: .py312
+  variables:
+    TOX_DJANGO_FRAGMENT: "django42"
 
 python37-django32:
   extends: .py37
@@ -77,11 +82,6 @@ python39-django32:
     TOX_DJANGO_FRAGMENT: "django32"
 
 # Template jobs which run tests in various Python versions.
-.py36:
-  extends: .test
-  variables:
-    PYTHON_VERSION: "3.6"
-    TOX_PY_FRAGMENT: "py36"
 
 .py37:
   extends: .test
@@ -101,6 +101,24 @@ python39-django32:
     PYTHON_VERSION: "3.9"
     TOX_PY_FRAGMENT: "py39"
 
+.py310:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.10"
+    TOX_PY_FRAGMENT: "py310"
+
+.py311:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.11"
+    TOX_PY_FRAGMENT: "py311"
+
+.py312:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.12"
+    TOX_PY_FRAGMENT: "py312"
+
 # Base test template job.
 .test:
   stage: test
diff --git a/CHANGELOG b/CHANGELOG
index 48335198b54c2ac38484872c074d8ab70e104e4a..a7c7e95ec122d781d39b909f91171a15806cd958 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,12 @@
 django-ucamlookup changelog
 =============================
 
+3.1.0 - 11/03/2024
+------------------
+
+- Upgraded to support django 4.2
+- Support for django 2.2 removed
+
 3.0.5 - 08/11/2021
 ------------------
 
diff --git a/requirements.txt b/requirements.txt
index a79bcb224d8aa0d13239a7460e8eb1f1716c6f07..d0c0562dc4f3eee483186f9327c4eb036780f8d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-django>=1.11
+django>=4.2
 tox
 ibisclient~=1.3
diff --git a/setup.py b/setup.py
index fcf69b442157e98550f832b168b8797d3fdf0c6e..367110ae6fd8a3cf2f7223bd716caa82d865a7e6 100755
--- a/setup.py
+++ b/setup.py
@@ -7,13 +7,13 @@ setup(
     long_description=open('README.md').read(),
     long_description_content_type='text/markdown',
     url='https://gitlab.developers.cam.ac.uk/uis/devops/django/ucamlookup',
-    version='3.0.5',
+    version='3.1.0',
     license='MIT',
     author='DevOps Division, University Information Services, University of Cambridge',
     author_email='devops@uis.cam.ac.uk',
     packages=find_packages(),
     include_package_data=True,
-    install_requires=['django>=2.2', 'ibisclient~=1.3'],
+    install_requires=['django>=3.2', 'ibisclient~=1.3'],
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Web Environment',
diff --git a/tox.ini b/tox.ini
index 11b4878dd2584b14eb53173d87dafde12439c996..384b0a3a7773f2b2efbe8a1b056dd2c971c0fad9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,8 +10,8 @@
 [tox]
 # Envs which should be run by default. This will execute a matrix of tests
 envlist =
-    py{36,37,38,39}-django22
     py{36,37,38,39}-django32
+    py{38,39,310,311,312}-django42
     coverage
     flake8
 # Allow overriding toxworkdir via environment variable
@@ -38,7 +38,7 @@ deps=
     .
     mock
     coverage
-    django22: Django>=2.2,<3.0
+    django42: Django>=4.2,<5.0
     django32: Django>=3.2,<3.3
 # Specify the default environment.
 commands=
@@ -53,7 +53,7 @@ deps=
 #   regressions when new checks are introduced. The version of flake8 used may
 #   be overridden via the TOXINI_FLAKE8_VERSION environment variable.
     mock
-    flake8=={env:TOXINI_FLAKE8_VERSION:3.9.2}
+    flake8=={env:TOXINI_FLAKE8_VERSION:7.0.0}
 commands=
     flake8 --version
     flake8 .
diff --git a/ucamlookup/tests.py b/ucamlookup/tests.py
index e6e8743e831ca153a9f86b549544e14ff870c9ad..b2fca7b5d3921b2194a8768b241a064c657d5add 100644
--- a/ucamlookup/tests.py
+++ b/ucamlookup/tests.py
@@ -187,7 +187,7 @@ class UcamLookupTests(TestCase):
     def test_get_institutions_with_user(self):
         amc203 = User.objects.create_user(username="amc203")
         results = get_institutions(user=amc203)
-        self.assertEquals(("CL", "Department of Computer Science and Technology"), results[0])
+        self.assertEqual(("CL", "Department of Computer Science and Technology"), results[0])
         self.assertIn(("UIS", "University Information Services"), results)
 
     def test_get_institutions_with_non_existant_user(self):
diff --git a/ucamlookup/urls.py b/ucamlookup/urls.py
index 9a620f4dcba8412e0112d7087fe3b73a04d89ca5..59c9b5a4690f72978f8f60742ff69fd0487420df 100644
--- a/ucamlookup/urls.py
+++ b/ucamlookup/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import url
+from django.urls import re_path
 from ucamlookup.views import find_people, find_groups
 
 
 urlpatterns = [
-    url(r'findPeople$', find_people, name='ucamlookup_find_people'),
-    url(r'findGroups$', find_groups, name='ucamlookup_find_groups'),
+    re_path(r'findPeople$', find_people, name='ucamlookup_find_people'),
+    re_path(r'findGroups$', find_groups, name='ucamlookup_find_groups'),
 ]