Skip to content

Commit 3624677

Browse files
committed
PLAT-1060 Add REST API and other enhancements
1 parent a7c7b09 commit 3624677

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1697
-100
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ output/*/index.html
6060
# Sphinx
6161
docs/_build
6262
docs/modules.rst
63+
docs/rest_api.rst
6364
docs/user_tasks.rst
6465

6566
# Private requirements
@@ -68,3 +69,6 @@ requirements/private.txt
6869

6970
# tox environment temporary artifacts
7071
tests/__init__.py
72+
73+
# Development task artifacts
74+
default.db

Makefile

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
.PHONY: clean compile_translations coverage docs dummy_translations extract_translations \
2-
fake_translations help pull_translations push_translations quality requirements test test-all validate
2+
fake_translations help pull_translations push_translations quality \
3+
requirements swagger-ui test test-all upgrade validate
34

45
.DEFAULT_GOAL := help
56

67
define BROWSER_PYSCRIPT
78
import os, webbrowser, sys
9+
from time import sleep
810
try:
911
from urllib import pathname2url
1012
except:
1113
from urllib.request import pathname2url
1214

13-
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
15+
path_or_url = sys.argv[1]
16+
delay = int(sys.argv[2]) if len(sys.argv) > 2 else 0
17+
if delay > 0:
18+
sleep(delay)
19+
if '://' in path_or_url:
20+
webbrowser.open(path_or_url)
21+
else:
22+
webbrowser.open("file://" + pathname2url(os.path.abspath(path_or_url)))
1423
endef
1524
export BROWSER_PYSCRIPT
1625
BROWSER := python -c "$$BROWSER_PYSCRIPT"
@@ -32,7 +41,7 @@ compile_translations: ## compile translation files, outputting .po files for eac
3241
./manage.py compilemessages
3342

3443
coverage: clean ## generate and view HTML coverage report
35-
py.test --cov-report html
44+
pytest --cov-report html
3645
$(BROWSER) htmlcov/index.html
3746

3847
docs: ## generate Sphinx HTML documentation, including API docs
@@ -48,11 +57,11 @@ extract_translations: ## extract strings to be translated, outputting .mo files
4857

4958
fake_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files
5059

51-
pip-compile: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
60+
upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
5261
pip install -q pip-tools
5362
pip-compile --upgrade -o requirements/base.txt requirements/base.in
54-
pip-compile --upgrade -o requirements/dev.txt requirements/dev.in
55-
pip-compile --upgrade -o requirements/doc.txt requirements/doc.in
63+
pip-compile --upgrade -o requirements/dev.txt requirements/dev.in requirements/quality.in
64+
pip-compile --upgrade -o requirements/doc.txt requirements/base.in requirements/doc.in
5665
pip-compile --upgrade -o requirements/quality.txt requirements/quality.in
5766
pip-compile --upgrade -o requirements/test.txt requirements/base.in requirements/test.in
5867
pip-compile --upgrade -o requirements/travis.txt requirements/travis.in
@@ -73,8 +82,14 @@ requirements: ## install development environment requirements
7382
pip install -qr requirements/dev.txt --exists-action w
7483
pip-sync requirements/base.txt requirements/dev.txt requirements/private.* requirements/test.txt
7584

85+
swagger-ui: ## view Swagger UI for the REST API documentation
86+
tox -e docs
87+
$(BROWSER) http://localhost:8000 5 &
88+
echo "The REST API documentation should open in your browser within a few seconds"
89+
. .tox/docs/bin/activate; ./manage.py migrate --settings=schema.settings; SWAGGER_JSON_PATH=docs/swagger.json ./manage.py runserver --settings=schema.settings
90+
7691
test: clean ## run tests in the current virtualenv
77-
py.test
92+
pytest
7893

7994
test-all: ## run tests on every supported Python/Django combination
8095
tox -e quality

docs/authorization.rst

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
Authorization
2+
=============
3+
4+
Access to task artifacts and status records via the REST API is restricted via Django's authorization mechanism.
5+
The following permissions are checked when assorted operations are attempted:
6+
7+
* user_tasks.cancel_usertaskstatus
8+
* user_tasks.change_usertaskstatus
9+
* user_tasks.delete_usertaskstatus
10+
* user_tasks.view_usertaskstatus
11+
* user_tasks.change_usertaskartifact
12+
* user_tasks.delete_usertaskartifact
13+
* user_tasks.view_usertaskartifact
14+
15+
These permissions can be managed via Django's default database-backed authorization implementation, but using
16+
an alternative authorization backend can be easier to manage and support object-level permissions (for example, to
17+
determine if a user has permission to view and cancel a particular task but not others). There is an
18+
`Authorization grid on Django Packages`_ listing several such backends; the test suite for ``django-user-tasks`` uses
19+
the `rules`_ package to define rule-based permissions which require no additional database setup. These rules can
20+
be used in other applications via :py:func:`user_tasks.rules.add_rules`, if desired.
21+
22+
.. _Authorization grid on Django Packages: https://djangopackages.org/grids/g/authorization/
23+
.. _rules: https://github.com/dfunckt/django-rules
24+
25+
Restriction of status and artifact listings in the REST API to only those which the requesting user has permission
26+
to view can be done via the ``USER_TASKS_ARTIFACT_FILTERS`` and ``USER_TASKS_STATUS_FILTERS`` settings. See the
27+
:doc:`settings documentation <settings>` for more information on how those work.
28+
29+
Artifact URL Access
30+
-------------------
31+
32+
Although the permission checks above cover most attempts to interact with a task, the artifacts associated with it
33+
may be located at URLs with less restricted access:
34+
35+
* :py:attr:`UserTaskArtifact.text` is actually stored as part of the model instance, and hence is covered by
36+
whichever authorization backend you've chosen to use. But it's limited to text content only, and is not recommended
37+
for large amounts of data.
38+
* :py:attr:`UserTaskArtifact.url` has no inherent security; the data at that URL is only as secure as the access
39+
restrictions placed on it by the hosting system (which may or may not be the same service which is using
40+
``django-user-tasks``). The URL may be hard to guess, but once discovered might be accessible to users other than
41+
those with view permission for the artifact instance unless appropriate measures are taken.
42+
* :py:attr:`UserTaskArtifact.file` uses URLs generated by the Django file storage system in use. The default
43+
implementation imposes no particular access restrictions on the generated files, but alternatives with better
44+
security are available. For example, if you use the ``s3boto`` or ``s3boto3`` backend from `django-storages`_ with
45+
a private Amazon S3 bucket, the artifact content URL will be presigned with query parameters which allow access to
46+
the file for a limited time (determined by the ``AWS_QUERYSTRING_EXPIRE`` setting), so the content cannot be
47+
accessed just by guessing the URL or gaining access to a previously served link with expired signature query
48+
parameters.
49+
50+
.. _django-storages: https://github.com/jschneier/django-storages

docs/conf.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from __future__ import absolute_import, unicode_literals
1717

1818
import os
19+
import sys
20+
from subprocess import check_call
1921

2022
# Configure Django for autodoc usage
2123
import django
@@ -459,6 +461,32 @@
459461
}
460462

461463

464+
def on_init(app): # pylint: disable=unused-argument
465+
"""
466+
Run sphinx-apidoc and swg2rst after Sphinx initialization.
467+
468+
Read the Docs won't run tox or custom shell commands, so we need this to
469+
avoid checking in the generated reStructuredText files.
470+
"""
471+
docs_path = os.path.abspath(os.path.dirname(__file__))
472+
root_path = os.path.abspath(os.path.join(docs_path, '..'))
473+
apidoc_path = 'sphinx-apidoc'
474+
swg2rst_path = 'swg2rst'
475+
if hasattr(sys, 'real_prefix'): # Check to see if we are in a virtualenv
476+
# If we are, assemble the path manually
477+
bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin'))
478+
apidoc_path = os.path.join(bin_path, apidoc_path)
479+
swg2rst_path = os.path.join(bin_path, swg2rst_path)
480+
check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'user_tasks'),
481+
os.path.join(root_path, 'user_tasks/migrations')])
482+
json_path = os.path.join(docs_path, 'swagger.json')
483+
rst_path = os.path.join(docs_path, 'rest_api.rst')
484+
check_call([swg2rst_path, json_path, '-f', 'rst', '-o', rst_path])
485+
486+
462487
def setup(app):
463-
"""Sphinx extension for applying some CSS overrides to the output theme."""
488+
"""
489+
Sphinx extension: run sphinx-apidoc and apply some CSS overrides to the output theme.
490+
"""
491+
app.connect('builder-inited', on_init)
464492
app.add_stylesheet('theme_overrides.css')

docs/data_cleanup.rst

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Data Cleanup
2+
============
3+
4+
After running django-user-tasks for a while, you'll typically have an
5+
assortment of :py:class:`UserTaskStatus` and :py:class:`UserTaskArtifact`
6+
records sitting around in the database. These can be either explicitly
7+
removed or automatically cleaned up.
8+
9+
Explicit
10+
--------
11+
12+
A :py:class:`UserTaskStatus` and any associated :py:class:`UserTaskArtifact`
13+
instances can be deleted via the REST API by a user with appropriate
14+
permissions. It's usually appropriate to put a button for this in the UI
15+
wherever a status is shown as long as the task has reached a final state:
16+
``Canceled``, ``Failed``, or ``Succeeded``. Note that if you delete a
17+
status record before the task has finished processing, it may be recreated
18+
upon the next state transition.
19+
20+
.. _automatic-cleanup:
21+
22+
Automatic
23+
---------
24+
25+
:py:func:`user_tasks.tasks.purge_old_user_tasks` is a Celery task which
26+
deletes any :py:class:`UserTaskArtifact` records (and any associated
27+
artifacts) which were created more than a certain duration ago. The
28+
task can be run explicitly, but you'd typically want it to run periodically
29+
by adding it to Celery's ``CELERYBEAT_SCHEDULE`` setting.
30+
31+
The maximum age for status records defaults to 30 days, but can be
32+
customized by assigning a suitable ``timedelta`` to the
33+
``USER_TASKS_MAX_AGE`` setting.

docs/getting_started.rst

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ for a single type of task:
77
1. Install ``django-user-tasks`` (typically via
88
``pip install django-user-tasks``, but use whatever mechanism you've chosen
99
for dependency management).
10-
2. Add ``django-user-tasks`` to the ``INSTALLED_APPS`` Django setting.
10+
2. Add ``django-user-tasks`` and ``rest_framework`` to the ``INSTALLED_APPS`` Django setting.
1111
3. Run migrations to create the required database tables.
1212
4. Create a subclass of :py:class:`user_tasks.tasks.UserTask` and override one or
1313
two of its methods.
@@ -75,3 +75,29 @@ Now the name of the task includes the name of the course being exported,
7575
there's an indication of how far along execution of the task has progressed,
7676
and while in progress the status reflects the name of the section currently
7777
being exported.
78+
79+
URL Configuration
80+
-----------------
81+
82+
Out of the box, ``django-user-tasks`` provides a ``urls`` module containing a URLconf which places the REST API
83+
endpoints under ``tasks/`` and ``artifacts/``. You can include ``user_tasks.urls.urlpatterns`` in your
84+
service's overall URL configuration, or create a custom configuration which uses paths of your choice. For example:
85+
86+
.. code-block:: python
87+
88+
from rest_framework.routers import SimpleRouter
89+
from user_tasks.views import ArtifactViewSet, StatusViewSet
90+
91+
ROUTER = SimpleRouter()
92+
ROUTER.register(r'user_task_artifacts', ArtifactViewSet, base_name='usertaskartifact')
93+
ROUTER.register(r'user_tasks/', StatusViewSet, base_name='usertaskstatus')
94+
95+
urlpatterns = ROUTER.urls
96+
97+
Task Status Signal
98+
------------------
99+
100+
When a subclass of :py:class:`user_tasks.tasks.UserTaskMixin` reaches any end state (``Canceled``, ``Failed``, or
101+
``Succeeded``), a ``user_tasks.user_task_stopped`` signal is sent. Listeners can use this signal to notify users of
102+
the status change, log relevant statistics, etc. The signal's ``sender`` is the :py:class:`UserTaskStatus` class,
103+
and its ``status`` argument is the instance of that class for which the signal was sent.

docs/index.rst

+4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ Contents:
1414

1515
readme
1616
getting_started
17+
authorization
18+
rest_api
19+
data_cleanup
1720
linked_tasks
21+
settings
1822
modules
1923
testing
2024
internationalization

docs/settings.rst

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Settings
2+
========
3+
4+
.. autoclass:: user_tasks.conf.LazySettings
5+
:members:
6+
:noindex:

0 commit comments

Comments
 (0)