Commit b3a4558e by Peter Fogg

Merge pull request #123 from edx/peter-fogg/documentation

Add developer documentation.
parents d8c867ae ca814dfd
...@@ -16,10 +16,10 @@ class PermissionsFilter(DRYPermissionFiltersBase): ...@@ -16,10 +16,10 @@ class PermissionsFilter(DRYPermissionFiltersBase):
Raises: Raises:
PermissionDenied -- If a username querystring parameter is specified, but the user is not a staff user. PermissionDenied -- If a username querystring parameter is specified, but the user is not a staff user.
Http404 -- If no `User` corresponding to the given username exists. Http404 -- If no User corresponding to the given username exists.
Returns: Returns:
`QuerySet` QuerySet
""" """
perm = queryset.model.get_permission('view') perm = queryset.model.get_permission('view')
user = request.user user = request.user
......
...@@ -33,10 +33,12 @@ def get_marketing_url_for_user(user, marketing_url): ...@@ -33,10 +33,12 @@ def get_marketing_url_for_user(user, marketing_url):
class TimestampModelSerializer(serializers.ModelSerializer): class TimestampModelSerializer(serializers.ModelSerializer):
"""Serializer for timestamped models."""
modified = serializers.DateTimeField() modified = serializers.DateTimeField()
class NamedModelSerializer(serializers.ModelSerializer): class NamedModelSerializer(serializers.ModelSerializer):
"""Serializer for models inheriting from ``AbstractNamedModel``."""
name = serializers.CharField() name = serializers.CharField()
class Meta(object): class Meta(object):
...@@ -44,21 +46,25 @@ class NamedModelSerializer(serializers.ModelSerializer): ...@@ -44,21 +46,25 @@ class NamedModelSerializer(serializers.ModelSerializer):
class SubjectSerializer(NamedModelSerializer): class SubjectSerializer(NamedModelSerializer):
"""Serializer for the ``Subject`` model."""
class Meta(NamedModelSerializer.Meta): class Meta(NamedModelSerializer.Meta):
model = Subject model = Subject
class PrerequisiteSerializer(NamedModelSerializer): class PrerequisiteSerializer(NamedModelSerializer):
"""Serializer for the ``Prerequisite`` model."""
class Meta(NamedModelSerializer.Meta): class Meta(NamedModelSerializer.Meta):
model = Prerequisite model = Prerequisite
class MediaSerializer(serializers.ModelSerializer): class MediaSerializer(serializers.ModelSerializer):
"""Serializer for models inheriting from ``AbstractMediaModel``."""
src = serializers.CharField() src = serializers.CharField()
description = serializers.CharField() description = serializers.CharField()
class ImageSerializer(MediaSerializer): class ImageSerializer(MediaSerializer):
"""Serializer for the ``Image`` model."""
height = serializers.IntegerField() height = serializers.IntegerField()
width = serializers.IntegerField() width = serializers.IntegerField()
...@@ -68,6 +74,7 @@ class ImageSerializer(MediaSerializer): ...@@ -68,6 +74,7 @@ class ImageSerializer(MediaSerializer):
class VideoSerializer(MediaSerializer): class VideoSerializer(MediaSerializer):
"""Serializer for the ``Video`` model."""
image = ImageSerializer() image = ImageSerializer()
class Meta(object): class Meta(object):
...@@ -76,6 +83,7 @@ class VideoSerializer(MediaSerializer): ...@@ -76,6 +83,7 @@ class VideoSerializer(MediaSerializer):
class SeatSerializer(serializers.ModelSerializer): class SeatSerializer(serializers.ModelSerializer):
"""Serializer for the ``Seat`` model."""
type = serializers.ChoiceField( type = serializers.ChoiceField(
choices=[name for name, __ in Seat.SEAT_TYPE_CHOICES] choices=[name for name, __ in Seat.SEAT_TYPE_CHOICES]
) )
...@@ -94,6 +102,7 @@ class SeatSerializer(serializers.ModelSerializer): ...@@ -94,6 +102,7 @@ class SeatSerializer(serializers.ModelSerializer):
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
"""Serializer for the ``Person`` model."""
profile_image = ImageSerializer() profile_image = ImageSerializer()
class Meta(object): class Meta(object):
...@@ -102,6 +111,7 @@ class PersonSerializer(serializers.ModelSerializer): ...@@ -102,6 +111,7 @@ class PersonSerializer(serializers.ModelSerializer):
class OrganizationSerializer(serializers.ModelSerializer): class OrganizationSerializer(serializers.ModelSerializer):
"""Serializer for the ``Organization`` model."""
logo_image = ImageSerializer() logo_image = ImageSerializer()
class Meta(object): class Meta(object):
...@@ -110,6 +120,7 @@ class OrganizationSerializer(serializers.ModelSerializer): ...@@ -110,6 +120,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
class CatalogSerializer(serializers.ModelSerializer): class CatalogSerializer(serializers.ModelSerializer):
"""Serializer for the ``Catalog`` model."""
courses_count = serializers.IntegerField(read_only=True, help_text=_('Number of courses contained in this catalog')) courses_count = serializers.IntegerField(read_only=True, help_text=_('Number of courses contained in this catalog'))
viewers = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all(), many=True, viewers = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all(), many=True,
allow_null=True, allow_empty=True, required=False, allow_null=True, allow_empty=True, required=False,
...@@ -131,6 +142,7 @@ class CatalogSerializer(serializers.ModelSerializer): ...@@ -131,6 +142,7 @@ class CatalogSerializer(serializers.ModelSerializer):
class CourseRunSerializer(TimestampModelSerializer): class CourseRunSerializer(TimestampModelSerializer):
"""Serializer for the ``CourseRun`` model."""
course = serializers.SlugRelatedField(read_only=True, slug_field='key') course = serializers.SlugRelatedField(read_only=True, slug_field='key')
content_language = serializers.SlugRelatedField( content_language = serializers.SlugRelatedField(
read_only=True, slug_field='code', source='language', read_only=True, slug_field='code', source='language',
...@@ -158,6 +170,7 @@ class CourseRunSerializer(TimestampModelSerializer): ...@@ -158,6 +170,7 @@ class CourseRunSerializer(TimestampModelSerializer):
class ContainedCourseRunsSerializer(serializers.Serializer): # pylint: disable=abstract-method class ContainedCourseRunsSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializer used to represent course runs contained by a catalog."""
course_runs = serializers.DictField( course_runs = serializers.DictField(
child=serializers.BooleanField(), child=serializers.BooleanField(),
help_text=_('Dictionary mapping course run IDs to boolean values') help_text=_('Dictionary mapping course run IDs to boolean values')
...@@ -165,6 +178,7 @@ class ContainedCourseRunsSerializer(serializers.Serializer): # pylint: disable= ...@@ -165,6 +178,7 @@ class ContainedCourseRunsSerializer(serializers.Serializer): # pylint: disable=
class CourseSerializer(TimestampModelSerializer): class CourseSerializer(TimestampModelSerializer):
"""Serializer for the ``Course`` model."""
level_type = serializers.SlugRelatedField(read_only=True, slug_field='name') level_type = serializers.SlugRelatedField(read_only=True, slug_field='name')
subjects = SubjectSerializer(many=True) subjects = SubjectSerializer(many=True)
prerequisites = PrerequisiteSerializer(many=True) prerequisites = PrerequisiteSerializer(many=True)
...@@ -189,10 +203,12 @@ class CourseSerializer(TimestampModelSerializer): ...@@ -189,10 +203,12 @@ class CourseSerializer(TimestampModelSerializer):
class CourseSerializerExcludingClosedRuns(CourseSerializer): class CourseSerializerExcludingClosedRuns(CourseSerializer):
"""A ``CourseSerializer`` which only includes active course runs, as determined by ``CourseQuerySet``."""
course_runs = CourseRunSerializer(many=True, source='active_course_runs') course_runs = CourseRunSerializer(many=True, source='active_course_runs')
class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abstract-method class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializer used to represent courses contained by a catalog."""
courses = serializers.DictField( courses = serializers.DictField(
child=serializers.BooleanField(), child=serializers.BooleanField(),
help_text=_('Dictionary mapping course IDs to boolean values') help_text=_('Dictionary mapping course IDs to boolean values')
......
# The bare minimum needed for Sphinx to import each file and generate documentation.
from course_discovery.settings.base import INSTALLED_APPS
DATABASES = {
'default': {
'ENGINE': '',
'NAME': '',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
SECRET_KEY = 'secret'
STATIC_URL = '/static/'
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': '',
'URL': '',
'INDEX_NAME': '',
}
}
API
===
The meat of the Course Catalog API is located in this app. ``v1`` is the current version of the API.
API Views
---------
.. automodule:: course_discovery.apps.api.v1.views
:members:
Serializers
-----------
.. automodule:: course_discovery.apps.api.serializers
:members:
Renderers
---------
.. automodule:: course_discovery.apps.api.renderers
:members:
Permissions
-----------
.. autoclass:: course_discovery.apps.api.filters.PermissionsFilter
:members:
Catalogs
========
A catalog is modeled as an Elasticsearch query (see :doc:`elasticsearch`) returning a
list of courses. The ``Catalog`` model lives in :file:`course_discovery/apps/catalogs/models.py`.
.. autoclass:: course_discovery.apps.catalogs.models.Catalog
:members:
Permissions
-----------
The ``viewers`` property of a catalog gives the users who are allowed to view the catalog and the courses it
contains. We use `django-guardian`_ for per-object permissions.
.. _django-guardian: https://django-guardian.readthedocs.io/en/stable/
Administration
--------------
Catalog administration is primarily done through the LMS at ``/api-admin/catalogs``. However, if you need to modify
a catalog or its query directly, you can do so using the course catalog Django admin at ``/admin/catalogs/``. The
admin interface provides a preview button to directly view the list of courses contained in a catalog, as well as
``django-guardian``'s standard admin for user permissions.
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
# serve to show the default. # serve to show the default.
import os import os
import sys
# on_rtd is whether we are on readthedocs.org # on_rtd is whether we are on readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
...@@ -25,6 +26,14 @@ if not on_rtd: # only import and set the theme if we're building docs locally ...@@ -25,6 +26,14 @@ if not on_rtd: # only import and set the theme if we're building docs locally
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(REPO_ROOT)
# Specify settings module (which will be picked up from the sandbox)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'course_discovery.settings.docs_settings')
import django
django.setup()
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
...@@ -33,7 +42,7 @@ if not on_rtd: # only import and set the theme if we're building docs locally ...@@ -33,7 +42,7 @@ if not on_rtd: # only import and set the theme if we're building docs locally
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [] extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
......
Course Metadata
===============
The ``course_metadata`` app at :file:`course_discovery/apps/course_metadata` primarily deals with getting data from
various external systems and pulling it into the course catalog.
Data Loaders
------------
:file:`course_discovery/apps/course_metadata/data_loaders.py` contains code for retrieving data from different
sources. The ``AbstractDataLoader`` class defines the basic interface, and is currently subclassed by the three
concrete implementations shown below. In the future we may add more data loaders as the requirements of the course
catalog change.
.. automodule:: course_discovery.apps.course_metadata.data_loaders
:members:
Retrieving Course Metadata
--------------------------
The ``refresh_course_metadata`` command in :file:`course_discovery/apps/course_metadata/management/commands/refresh_course_metadata.py` is used to retrieve metadata. This is run daily in production through a Jenkins job, and can be manually run to set up your local environment. The data loaders are each run in series by the command. The data
loaders should be idempotent -- that is, running this command once will populate the database, and if nothing has
changed upstream then running it again should not change the database.
QuerySets
---------
We use a custom ``QuerySet`` for retrieving active courses, based on the definition of "active" that the LMS uses.
.. autoclass:: course_discovery.apps.course_metadata.query.CourseQuerySet
:members:
Views
-----
The ``QueryPreviewView`` provides a simple interface to test a query before saving it to a catalog.
.. autoclass:: course_discovery.apps.course_metadata.views.QueryPreviewView
:members:
Models
------
The ``course_metadata`` contains most of the models used in the course catalog API.
.. automodule:: course_discovery.apps.course_metadata.models
:members:
:undoc-members:
Searching for Courses
---------------------
The fields of ``CourseIndex`` and ``CourseRunIndex`` are the fields that can be used in ES queries.
.. automodule:: course_discovery.apps.course_metadata.search_indexes
:members:
:undoc-members:
...@@ -24,3 +24,11 @@ created; and, the alias will be assigned to the new index. ...@@ -24,3 +24,11 @@ created; and, the alias will be assigned to the new index.
.. code-block:: bash .. code-block:: bash
$ ./manage.py install_es_indexes $ ./manage.py install_es_indexes
Query String Syntax
-------------------
We use the query string syntax to search for courses. See `the Elasticsearch documentation`_ for a guide to the
query string syntax, and :doc:`course_metadata` for a list of fields which can be searched.
.. _the Elasticsearch documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
...@@ -11,6 +11,9 @@ A service for serving course discovery and marketing information to partners, mo ...@@ -11,6 +11,9 @@ A service for serving course discovery and marketing information to partners, mo
:maxdepth: 2 :maxdepth: 2
getting_started getting_started
api
catalogs
course_metadata
elasticsearch elasticsearch
testing testing
features features
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment