diff --git a/.travis.yml b/.travis.yml
index 6aeb63e88aff86e38fabba78b5503c041e912d42..319a849de74fb2a714484aaa61bf5d6d4682fe54 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,13 @@
 language: python
 
 python:
-  - "2.7"
-  - "3.5"
+  - 3.5
+  - 3.8
 
 sudo: false
 
 install:
-  - "pip install six"
-  - "make install"
+  - pip install -r requirements/travis.txt
 
 script:
   - make quality
diff --git a/Makefile b/Makefile
index 8b5c07b9cfce12093d1d178361899a3619f66564..ba0a4066badfcfa1db40ad61e2219686bf2d39ee 100644
--- a/Makefile
+++ b/Makefile
@@ -13,3 +13,16 @@ quality:
 
 test:
 	./scripts/test.sh
+
+upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
+upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
+	pip install -q -r requirements/pip_tools.txt
+	pip-compile --upgrade -o requirements/pip_tools.txt requirements/pip_tools.in
+	pip-compile --upgrade -o requirements/base.txt requirements/base.in
+	pip-compile --upgrade -o requirements/test.txt requirements/test.in
+	pip-compile --upgrade -o requirements/tox.txt requirements/tox.in
+	pip-compile --upgrade -o requirements/travis.txt requirements/travis.in
+	# Let tox control the Django version version for tests
+	grep -e "^django==" requirements/test.txt > requirements/django.txt
+	sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp
+	mv requirements/test.tmp requirements/test.txt
\ No newline at end of file
diff --git a/openedx.yaml b/openedx.yaml
index fa679fd566f715daacb1950104e17eea5fd05aa0..575a621cb32f6fc3850ac0cd3611541e501b861f 100644
--- a/openedx.yaml
+++ b/openedx.yaml
@@ -5,7 +5,7 @@ nick: lti
 oeps:
     oep-2: true
     oep-7: true
-    oep-18: false
+    oep-18: true
 owner: scottrish
 track-pulls: true
 tags:
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 555b3eb2c3c6f97179a25a9f465d4fee1d3cb628..0000000000000000000000000000000000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-lxml
-bleach
-django==1.11.25
-oauthlib
-mako
-lazy
--e git+https://github.com/edx/XBlock.git#egg=XBlock
--e git+https://github.com/edx/xblock-utils.git#egg=xblock-utils
--e .
diff --git a/requirements/base.in b/requirements/base.in
new file mode 100644
index 0000000000000000000000000000000000000000..0f0577dcff7a5e67c0311736f1b28d71440586db
--- /dev/null
+++ b/requirements/base.in
@@ -0,0 +1,11 @@
+# Core requirements for using this package
+-c constraints.txt
+
+lxml
+bleach
+django
+oauthlib
+mako
+lazy
+XBlock
+xblock-utils
diff --git a/requirements/base.txt b/requirements/base.txt
new file mode 100644
index 0000000000000000000000000000000000000000..85536ceafb35e42ebb5a79c94e705bacb0b92412
--- /dev/null
+++ b/requirements/base.txt
@@ -0,0 +1,32 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+appdirs==1.4.3            # via fs
+bleach==3.1.5             # via -r requirements/base.in
+django==2.2.12            # via -c requirements/constraints.txt, -r requirements/base.in
+fs==2.4.11                # via xblock
+lazy==1.4                 # via -r requirements/base.in
+lxml==4.5.0               # via -r requirements/base.in, xblock
+mako==1.1.2               # via -r requirements/base.in, xblock-utils
+markupsafe==1.1.1         # via mako, xblock
+oauthlib==3.1.0           # via -r requirements/base.in
+packaging==20.3           # via bleach
+pyparsing==2.4.7          # via packaging
+python-dateutil==2.8.1    # via xblock
+pytz==2020.1              # via django, fs, xblock
+pyyaml==5.3.1             # via xblock
+simplejson==3.17.0        # via xblock-utils
+six==1.14.0               # via bleach, fs, packaging, python-dateutil, xblock
+sqlparse==0.3.1           # via django
+typing==3.7.4.1           # via fs
+web-fragments==0.3.1      # via xblock, xblock-utils
+webencodings==0.5.1       # via bleach
+webob==1.8.6              # via xblock
+xblock-utils==2.0.0       # via -r requirements/base.in
+xblock==1.3.1             # via -r requirements/base.in, xblock-utils
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bb6d02b5e25de428153725a6a8062b73584db8df
--- /dev/null
+++ b/requirements/constraints.txt
@@ -0,0 +1,17 @@
+# Version constraints for pip-installation.
+#
+# This file doesn't install any packages. It specifies version constraints
+# that will be applied if a package is needed.
+#
+# When pinning something here, please provide an explanation of why.  Ideally,
+# link to other information that will help people in the future to remove the
+# pin when possible.  Writing an issue against the offending project and
+# linking to it here is good.
+
+# TODO: Many pinned dependencies should be unpinned and/or moved to this constraints file.
+
+# Use latest Django LTS version
+Django<2.3.0
+
+# mock version 4.0.0 drops support for python 3.5
+mock<4.0.0
diff --git a/requirements/django.txt b/requirements/django.txt
new file mode 100644
index 0000000000000000000000000000000000000000..336e83d0abb0b3e968aaedb3817a463fc3fa102e
--- /dev/null
+++ b/requirements/django.txt
@@ -0,0 +1 @@
+django==2.2.12            # via -c requirements/constraints.txt, -r requirements/base.txt, django-pyfs, xblock-sdk
diff --git a/requirements/pip_tools.in b/requirements/pip_tools.in
new file mode 100644
index 0000000000000000000000000000000000000000..caf45a91aaea08458cf6bb07f9d9abfd95f48097
--- /dev/null
+++ b/requirements/pip_tools.in
@@ -0,0 +1,4 @@
+ # Dependencies to run compile tools
+-c constraints.txt
+
+pip-tools                           # Contains pip-compile, used to generate pip requirements files
diff --git a/requirements/pip_tools.txt b/requirements/pip_tools.txt
new file mode 100644
index 0000000000000000000000000000000000000000..937f1d616a215d43688d675823cfe42e6858df51
--- /dev/null
+++ b/requirements/pip_tools.txt
@@ -0,0 +1,12 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+click==7.1.2              # via pip-tools
+pip-tools==5.1.2          # via -r requirements/pip_tools.in
+six==1.14.0               # via pip-tools
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
diff --git a/requirements/test.in b/requirements/test.in
new file mode 100644
index 0000000000000000000000000000000000000000..0782a58ffe70cb909eceda33df9b3576ff152316
--- /dev/null
+++ b/requirements/test.in
@@ -0,0 +1,13 @@
+# Requirements for test runs
+-c constraints.txt
+
+-r base.txt               # Core dependencies for the cookiecutter
+
+django-nose
+ddt
+coveralls
+mock
+pep8
+django-pyfs
+edx_lint
+-e git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk
diff --git a/requirements/test.txt b/requirements/test.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9026f9eabe9c6e561af4274c642901ad30a8baeb
--- /dev/null
+++ b/requirements/test.txt
@@ -0,0 +1,65 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+-e git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk  # via -r requirements/test.in
+appdirs==1.4.3            # via -r requirements/base.txt, fs
+astroid==2.3.3            # via pylint, pylint-celery
+bleach==3.1.5             # via -r requirements/base.txt
+boto3==1.13.4             # via fs-s3fs
+botocore==1.16.4          # via boto3, s3transfer
+certifi==2020.4.5.1       # via requests
+chardet==3.0.4            # via requests
+click-log==0.3.2          # via edx-lint
+click==7.1.2              # via click-log, edx-lint
+coverage==5.1             # via coveralls
+coveralls==2.0.0          # via -r requirements/test.in
+ddt==1.3.1                # via -r requirements/test.in
+django-nose==1.4.6        # via -r requirements/test.in
+django-pyfs==2.1          # via -r requirements/test.in
+docopt==0.6.2             # via coveralls
+docutils==0.15.2          # via botocore
+edx-lint==1.4.1           # via -r requirements/test.in
+fs-s3fs==1.1.1            # via django-pyfs
+fs==2.4.11                # via -r requirements/base.txt, django-pyfs, fs-s3fs, xblock
+idna==2.9                 # via requests
+isort==4.3.21             # via pylint
+jmespath==0.9.5           # via boto3, botocore
+lazy-object-proxy==1.4.3  # via astroid
+lazy==1.4                 # via -r requirements/base.txt
+lxml==4.5.0               # via -r requirements/base.txt, xblock
+mako==1.1.2               # via -r requirements/base.txt, xblock-utils
+markupsafe==1.1.1         # via -r requirements/base.txt, mako, xblock
+mccabe==0.6.1             # via pylint
+mock==3.0.5               # via -c requirements/constraints.txt, -r requirements/test.in
+nose==1.3.7               # via django-nose
+oauthlib==3.1.0           # via -r requirements/base.txt
+packaging==20.3           # via -r requirements/base.txt, bleach
+pep8==1.7.1               # via -r requirements/test.in
+pylint-celery==0.3        # via edx-lint
+pylint-django==2.0.11     # via edx-lint
+pylint-plugin-utils==0.6  # via pylint-celery, pylint-django
+pylint==2.4.2             # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils
+pyparsing==2.4.7          # via -r requirements/base.txt, packaging
+python-dateutil==2.8.1    # via -r requirements/base.txt, botocore, xblock
+pytz==2020.1              # via -r requirements/base.txt, django, fs, xblock
+pyyaml==5.3.1             # via -r requirements/base.txt, xblock
+requests==2.23.0          # via coveralls
+s3transfer==0.3.3         # via boto3
+simplejson==3.17.0        # via -r requirements/base.txt, xblock-utils
+six==1.14.0               # via -r requirements/base.txt, astroid, bleach, django-pyfs, edx-lint, fs, fs-s3fs, mock, packaging, python-dateutil, xblock
+sqlparse==0.3.1           # via -r requirements/base.txt, django
+typed-ast==1.4.1          # via astroid
+typing==3.7.4.1           # via -r requirements/base.txt, fs
+urllib3==1.25.9           # via botocore, requests
+web-fragments==0.3.1      # via -r requirements/base.txt, xblock, xblock-utils
+webencodings==0.5.1       # via -r requirements/base.txt, bleach
+webob==1.8.6              # via -r requirements/base.txt, xblock
+wrapt==1.11.2             # via astroid
+xblock-utils==2.0.0       # via -r requirements/base.txt
+xblock==1.3.1             # via -r requirements/base.txt, xblock-utils
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/tox.in b/requirements/tox.in
new file mode 100644
index 0000000000000000000000000000000000000000..9a2869477c9c861482c5ab728c7960d220dceacc
--- /dev/null
+++ b/requirements/tox.in
@@ -0,0 +1,4 @@
+# Used for tests
+-c constraints.txt
+
+tox
diff --git a/requirements/tox.txt b/requirements/tox.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7d38c58c6ca770367c579c82a51352b447039d87
--- /dev/null
+++ b/requirements/tox.txt
@@ -0,0 +1,20 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+appdirs==1.4.3            # via virtualenv
+distlib==0.3.0            # via virtualenv
+filelock==3.0.12          # via tox, virtualenv
+importlib-metadata==1.6.0  # via importlib-resources, pluggy, tox, virtualenv
+importlib-resources==1.5.0  # via virtualenv
+packaging==20.3           # via tox
+pluggy==0.13.1            # via tox
+py==1.8.1                 # via tox
+pyparsing==2.4.7          # via packaging
+six==1.14.0               # via packaging, tox, virtualenv
+toml==0.10.0              # via tox
+tox==3.15.0               # via -r requirements/tox.in
+virtualenv==20.0.20       # via tox
+zipp==1.2.0               # via importlib-metadata, importlib-resources
diff --git a/requirements/travis.in b/requirements/travis.in
new file mode 100644
index 0000000000000000000000000000000000000000..5aa07acf8e0b7cab1cb72e564ca909961a8287cf
--- /dev/null
+++ b/requirements/travis.in
@@ -0,0 +1,7 @@
+# Requirements for running tests in Travis
+-c constraints.txt
+
+-r test.txt
+-r tox.txt
+
+six
diff --git a/requirements/travis.txt b/requirements/travis.txt
new file mode 100644
index 0000000000000000000000000000000000000000..63b342ee5aff2f00d868f8e117a09ad7fb5f827f
--- /dev/null
+++ b/requirements/travis.txt
@@ -0,0 +1,76 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    make upgrade
+#
+-e git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk  # via -r requirements/test.txt
+appdirs==1.4.3            # via -r requirements/test.txt, -r requirements/tox.txt, fs, virtualenv
+astroid==2.3.3            # via -r requirements/test.txt, pylint, pylint-celery
+bleach==3.1.5             # via -r requirements/test.txt
+boto3==1.13.4             # via -r requirements/test.txt, fs-s3fs
+botocore==1.16.4          # via -r requirements/test.txt, boto3, s3transfer
+certifi==2020.4.5.1       # via -r requirements/test.txt, requests
+chardet==3.0.4            # via -r requirements/test.txt, requests
+click-log==0.3.2          # via -r requirements/test.txt, edx-lint
+click==7.1.2              # via -r requirements/test.txt, click-log, edx-lint
+coverage==5.1             # via -r requirements/test.txt, coveralls
+coveralls==2.0.0          # via -r requirements/test.txt
+ddt==1.3.1                # via -r requirements/test.txt
+distlib==0.3.0            # via -r requirements/tox.txt, virtualenv
+django-nose==1.4.6        # via -r requirements/test.txt
+django-pyfs==2.1          # via -r requirements/test.txt
+django==2.2.12            # via -c requirements/constraints.txt, -r requirements/test.txt, django-pyfs, xblock-sdk
+docopt==0.6.2             # via -r requirements/test.txt, coveralls
+docutils==0.15.2          # via -r requirements/test.txt, botocore
+edx-lint==1.4.1           # via -r requirements/test.txt
+filelock==3.0.12          # via -r requirements/tox.txt, tox, virtualenv
+fs-s3fs==1.1.1            # via -r requirements/test.txt, django-pyfs
+fs==2.4.11                # via -r requirements/test.txt, django-pyfs, fs-s3fs, xblock
+idna==2.9                 # via -r requirements/test.txt, requests
+importlib-metadata==1.6.0  # via -r requirements/tox.txt, importlib-resources, pluggy, tox, virtualenv
+importlib-resources==1.5.0  # via -r requirements/tox.txt, virtualenv
+isort==4.3.21             # via -r requirements/test.txt, pylint
+jmespath==0.9.5           # via -r requirements/test.txt, boto3, botocore
+lazy-object-proxy==1.4.3  # via -r requirements/test.txt, astroid
+lazy==1.4                 # via -r requirements/test.txt
+lxml==4.5.0               # via -r requirements/test.txt, xblock
+mako==1.1.2               # via -r requirements/test.txt, xblock-utils
+markupsafe==1.1.1         # via -r requirements/test.txt, mako, xblock
+mccabe==0.6.1             # via -r requirements/test.txt, pylint
+mock==3.0.5               # via -c requirements/constraints.txt, -r requirements/test.txt
+nose==1.3.7               # via -r requirements/test.txt, django-nose
+oauthlib==3.1.0           # via -r requirements/test.txt
+packaging==20.3           # via -r requirements/test.txt, -r requirements/tox.txt, bleach, tox
+pep8==1.7.1               # via -r requirements/test.txt
+pluggy==0.13.1            # via -r requirements/tox.txt, tox
+py==1.8.1                 # via -r requirements/tox.txt, tox
+pylint-celery==0.3        # via -r requirements/test.txt, edx-lint
+pylint-django==2.0.11     # via -r requirements/test.txt, edx-lint
+pylint-plugin-utils==0.6  # via -r requirements/test.txt, pylint-celery, pylint-django
+pylint==2.4.2             # via -r requirements/test.txt, edx-lint, pylint-celery, pylint-django, pylint-plugin-utils
+pyparsing==2.4.7          # via -r requirements/test.txt, -r requirements/tox.txt, packaging
+python-dateutil==2.8.1    # via -r requirements/test.txt, botocore, xblock
+pytz==2020.1              # via -r requirements/test.txt, django, fs, xblock
+pyyaml==5.3.1             # via -r requirements/test.txt, xblock
+requests==2.23.0          # via -r requirements/test.txt, coveralls
+s3transfer==0.3.3         # via -r requirements/test.txt, boto3
+simplejson==3.17.0        # via -r requirements/test.txt, xblock-utils
+six==1.14.0               # via -r requirements/test.txt, -r requirements/tox.txt, -r requirements/travis.in, astroid, bleach, django-pyfs, edx-lint, fs, fs-s3fs, mock, packaging, python-dateutil, tox, virtualenv, xblock
+sqlparse==0.3.1           # via -r requirements/test.txt, django
+toml==0.10.0              # via -r requirements/tox.txt, tox
+tox==3.15.0               # via -r requirements/tox.txt
+typed-ast==1.4.1          # via -r requirements/test.txt, astroid
+typing==3.7.4.1           # via -r requirements/test.txt, fs
+urllib3==1.25.9           # via -r requirements/test.txt, botocore, requests
+virtualenv==20.0.20       # via -r requirements/tox.txt, tox
+web-fragments==0.3.1      # via -r requirements/test.txt, xblock, xblock-utils
+webencodings==0.5.1       # via -r requirements/test.txt, bleach
+webob==1.8.6              # via -r requirements/test.txt, xblock
+wrapt==1.11.2             # via -r requirements/test.txt, astroid
+xblock-utils==2.0.0       # via -r requirements/test.txt
+xblock==1.3.1             # via -r requirements/test.txt, xblock-utils
+zipp==1.2.0               # via -r requirements/tox.txt, importlib-metadata, importlib-resources
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/setup.py b/setup.py
index 0e78c3d77cda009102dc6ba07f53e4d419796c62..0f007a0142ced2cba48cc805baebc9b82d1e6ac4 100644
--- a/setup.py
+++ b/setup.py
@@ -22,22 +22,36 @@ def package_data(pkg, roots):
 
     return {pkg: data}
 
+def load_requirements(*requirements_paths):
+    """
+    Load all requirements from the specified requirements files.
+    Returns a list of requirement strings.
+    """
+    requirements = set()
+    for path in requirements_paths:
+        with open(path) as reqs:
+            requirements.update(
+                line.split('#')[0].strip() for line in reqs
+                if is_requirement(line.strip())
+            )
+    return list(requirements)
+
+
+def is_requirement(line):
+    """
+    Return True if the requirement line is a package requirement;
+    that is, it is not blank, a comment, a URL, or an included file.
+    """
+    return line and not line.startswith(('-r', '#', '-e', 'git+', '-c'))
 
 setup(
     name='lti_consumer-xblock',
-    version='1.2.6',
+    version='1.3.0',
     description='This XBlock implements the consumer side of the LTI specification.',
     packages=[
         'lti_consumer',
     ],
-    install_requires=[
-        'lxml',
-        'bleach',
-        'oauthlib',
-        'mako',
-        'XBlock',
-        'xblock-utils>=v1.0.0',
-    ],
+    install_requires=load_requirements('requirements/base.in'),
     dependency_links=[
         'https://github.com/edx/xblock-utils/tarball/c39bf653e4f27fb3798662ef64cde99f57603f79#egg=xblock-utils',
     ],
@@ -47,4 +61,15 @@ setup(
         ]
     },
     package_data=package_data("lti_consumer", ["static", "templates", "public", "translations"]),
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Framework :: Django',
+        'Framework :: Django :: 2.2',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Natural Language :: English',
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.8",
+    ]
 )
diff --git a/test_requirements.txt b/test_requirements.txt
deleted file mode 100644
index ea45f3f80d92298005181abc31a3fafbc5ba9c20..0000000000000000000000000000000000000000
--- a/test_requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
--r requirements.txt
-
-django-nose==1.4.6
-ddt
-coveralls
-mock
-pep8
-git+https://github.com/edx/django-pyfs.git@2.0#egg=django-pyfs==2.0
-git+https://github.com/edx/edx-lint.git@1.4.0#egg=edx_lint==1.4.0
--e git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk