Skip to content

Commit

Permalink
Add enterprise_data app
Browse files Browse the repository at this point in the history
This django app is installed into the analytics-data-api to expose an
endpoint to allow enterprises to access their learner enrollment data
  • Loading branch information
georgebabey committed Mar 7, 2018
1 parent 6da8344 commit 695a5d4
Show file tree
Hide file tree
Showing 61 changed files with 1,807 additions and 49 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ node_modules/
pip-log.txt

# Unit test / coverage reports
.pytest_cache
.cache/
.coverage
.coverage.*
Expand Down
29 changes: 29 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
language: python
python:
- 2.7
- 3.5
env:
- TOXENV=master
matrix:
include:
- python: 3.5
env: TOXENV=quality
- python: 2.7
env: TOXENV=quality
cache:
- pip
install:
- pip install -r requirements/travis.txt
script:
- if env | grep RUNJSHINT; then make jshint; else tox; fi
after_success:
- codecov
deploy:
provider: pypi
user: edx
distributions: sdist bdist_wheel
on:
tags: true
condition: "$TOXENV = quality"
password:
secure: vWQHEuSdKajVDfw6C3BQ+hEiGm8RIhjE6bu13URaHAHNYnxaxlyUEaw0HX8dKPeQ5O4WIwL1XutClKAb/hyL8VP3u7Pj9ZyiQX05Ift2mN0TDn4xWV/IgaBHAU/o4MIzDyZPriogHfp0i+GtD35ByfqOj6MTB3ntrDc7twDMkgYYPTVfMElKNtY3X1C5N6Lx/vprzdd+JFf/NuuMz8mPBe/rqBCIQNZRE3IXfyHAQwERY833LHrCWrEGhkR5zlirHQPd9enQ/1WRxMaICptnSaNwNnDib+h3XzKmM2l9uGXJV+vUQ+PK7C5fyiqr99DPKlQ9vyOINyxKDH+RS28umLeGMWxTrf30h5u24VcKtnq3JmP5jzduy2dAoQohgdqlBb132j79hlUC7W/fES3wDnKYxEpniCTQHOpZLTMt170cyKxHJmdC9NVw3ZmiKXh7bAYbS9Ul/Yv46SV3PHt0ls6tqP3HzKj5TFNwWj9wqNWbIh3b5jtdjngeN6ibiYZ2IirEesW4PCtUX6SC1nAY3diiET+z/NVsUwVLIZ4Evz0MB+DtCM4x69ubN5B9TVVd0oAR9wfvBup3TgnqeePNF8HM4z+ru9JWxoAHmucR0mX3FzXvAxLT0IF7csm+KMAjuEoPMkSKRRCbcKov5lAjaIjjXnivrIvtWXCjWndpfZ0=
20 changes: 20 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Change Log
==========

..
All enhancements and patches to edx-enteprise-data will be documented
in this file. It adheres to the structure of http://keepachangelog.com/ ,
but in reStructuredText instead of Markdown (for ease of incorporation into
Sphinx documentation and the PyPI description).
This project adheres to Semantic Versioning (http://semver.org/).

.. There should always be an "Unreleased" section for changes pending release.
Unreleased
----------

[0.1.0] - 2018-03-07
---------------------

* Add new app `enterprise_api`. This django app is used to expose a REST endpoint in th eex-analytics-data-api project.
8 changes: 8 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include AUTHORS
include CHANGELOG.rst
include CONTRIBUTING.rst
include LICENSE.txt
include README.rst
recursive-include enterprise_data *.html *.png *.gif *js *.css *jpg *jpeg *svg *py *.txt
recursive-include enterprise_reporting *.html *.png *.gif *js *.css *jpg *jpeg *svg *py *.txt
recursive-include requirements *.txt
36 changes: 34 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,45 @@ help: ## display this help message
@echo "Please use \`make <target>' where <target> is one of"
@perl -nle'print $& if m{^[\.a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}'

clean: ## remove generated byte code, coverage reports, and build artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '__pycache__' -exec rm -rf {} +
find . -name '*~' -exec rm -f {} +
coverage erase
rm -fr build/
rm -fr dist/
rm -fr *.egg-info

coverage: clean ## generate and view HTML coverage report
py.test --cov-report html
$(BROWSER) htmlcov/index.html

upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip install -q pip-tools
pip-compile --upgrade -o requirements/base.txt requirements/base.in
pip-compile --upgrade -o requirements/dev.txt requirements/base.in requirements/dev.in
pip-compile --upgrade -o requirements/dev.txt requirements/base.in requirements/dev-enterprise_data.in requirements/dev-enterprise_reporting.in requirements/quality.in
pip-compile --upgrade -o requirements/quality.txt requirements/base.in requirements/dev-enterprise_data.in requirements/quality.in requirements/test.in
pip-compile --upgrade -o requirements/travis.txt requirements/travis.in
pip-compile --upgrade -o requirements/test.txt requirements/base.in requirements/test.in

requirements: ## install development environment requirements
pip install -qr requirements/base.txt --exists-action w
pip-sync requirements/base.txt
pip-sync requirements/base.txt requirements/dev.txt requirements/test.txt

test: clean ## run tests in the current virtualenv
pip install -qr requirements/test.txt --exists-action w
py.test

test-all: clean jshint static ## run tests on every supported Python/Django combination
tox
tox -e quality

validate: test ## run tests and quality checks
tox -e quality

isort: ## call isort on packages/files that are checked in quality tests
isort --recursive tests enterprise_reporting enterprise_data manage.py setup.py


.PHONY: requirements upgrade help
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# edx-enterprise-data
The edX Enterprise Data repo is the home to tools and products related to providing access to Enterprise related data.

This repository is currently split into 2 folders: enterprise_reporting and enterprise_data

## enterprise_data app
This django app exposes a REST api endpoint to access enterprise learner activity. The enterprise-data app is published
to pypi as a library and installed into the [edx-analytics-data-api](github.com/edx/edx-analytics-data-api/) project
and uses OAuth JWT authentication from [edx-drf-extensions](https://github.com/edx/edx-drf-extensions/blob/master/edx_rest_framework_extensions/authentication.py).

## enterprise_reporting scripts
This folder contains a set of scripts used to push enterprise data reports.
It supports multiple delivery methods (email, sftp) and is triggered through jenkins scheduled jobs.
9 changes: 9 additions & 0 deletions enterprise_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Enterprise data api application. This Django app exposes API endpoints used by enterprises.
"""

from __future__ import absolute_import, unicode_literals

__version__ = "0.1.0"

default_app_config = "enterprise_data.apps.EnterpriseDataAppConfig" # pylint: disable=invalid-name
Empty file added enterprise_data/api/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions enterprise_data/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
URL definitions for enterprise data API endpoint.
"""
from __future__ import absolute_import, unicode_literals

from rest_framework.urlpatterns import format_suffix_patterns

from django.conf.urls import include, url

urlpatterns = [
url(r'^v0/', include('enterprise_data.api.v0.urls', 'v0'), name='api_v0'),
]

urlpatterns = format_suffix_patterns(urlpatterns)
7 changes: 7 additions & 0 deletions enterprise_data/api/v0/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
API endpoint for enterprise data app.
"""
from __future__ import unicode_literals

default_app_config = 'enterprise_data.api.v0.apps.ApiAppConfig' # pylint: disable=invalid-name
19 changes: 19 additions & 0 deletions enterprise_data/api/v0/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Serializers for enterprise api version 0 endpoint.
"""
from __future__ import absolute_import, unicode_literals

from rest_framework import serializers

from enterprise_data.models import EnterpriseEnrollment


class EnterpriseEnrollmentSerializer(serializers.ModelSerializer):
"""
Serializer for EnterpriseEnrollment model.
"""

class Meta:
model = EnterpriseEnrollment
fields = '__all__'
15 changes: 15 additions & 0 deletions enterprise_data/api/v0/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
URL definitions for enterprise data api version 1 endpoint.
"""
from __future__ import absolute_import, unicode_literals

from django.conf.urls import url

from enterprise_data.api.v0 import views

urlpatterns = [
url(r'^enterprise/(?P<enterprise_id>.+)/enrollments/$',
views.EnterpriseEnrollmentsView.as_view(),
name='enterprise_enrollments'),
]
30 changes: 30 additions & 0 deletions enterprise_data/api/v0/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""
Views for enterprise api version 0 endpoint.
"""
from __future__ import absolute_import, unicode_literals

from edx_rest_framework_extensions.authentication import JwtAuthentication
from edx_rest_framework_extensions.paginators import DefaultPagination
from rest_framework import generics

from enterprise_data.api.v0 import serializers
from enterprise_data.filters import ConsentGrantedFilterBackend
from enterprise_data.models import EnterpriseEnrollment
from enterprise_data.permissions import IsStaffOrEnterpriseUser


class EnterpriseEnrollmentsView(generics.ListAPIView):
"""
The EnterpriseEnrollments view returns all learner enrollment records for a given enterprise
"""
serializer_class = serializers.EnterpriseEnrollmentSerializer
pagination_class = DefaultPagination
authentication_classes = (JwtAuthentication,)
permission_classes = (IsStaffOrEnterpriseUser,)
filter_backends = (ConsentGrantedFilterBackend,)
CONSENT_GRANTED_FILTER = 'consent_granted'

def get_queryset(self):
enterprise_id = self.kwargs['enterprise_id']
return EnterpriseEnrollment.objects.filter(enterprise_id=enterprise_id)
16 changes: 16 additions & 0 deletions enterprise_data/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
"""
Enterprise Data Django application initialization.
"""

from __future__ import absolute_import, unicode_literals

from django.apps import AppConfig


class EnterpriseDataAppConfig(AppConfig):
"""
Configuration for the enterprise Django application.
"""

name = "enterprise_data"
58 changes: 58 additions & 0 deletions enterprise_data/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Clients used to connect to other systems.
"""
from __future__ import absolute_import, unicode_literals

import logging

from edx_rest_api_client.client import EdxRestApiClient
from edx_rest_api_client.exceptions import HttpClientError, HttpServerError
from rest_framework.exceptions import NotFound, ParseError

from django.conf import settings

LOGGER = logging.getLogger('enterprise_data')


class EnterpriseApiClient(EdxRestApiClient):
"""
The EnterpriseApiClient is used to communicate with the enterprise API endpoints in the LMS.
This class is a sub-class of the edX Rest API Client
(https://github.com/edx/edx-rest-api-client).
"""

API_BASE_URL = settings.LMS_BASE_URL + 'enterprise/api/v1/'

def __init__(self, jwt):
"""
Initialize client with given jwt.
"""
super(EnterpriseApiClient, self).__init__(self.API_BASE_URL, jwt=jwt)

def get_enterprise_learner(self, user):
"""
Get an enterprise learner record for a given user with the enterprise association.
Returns: learner record or None if unable to retrieve or no Enterprise Learner exists
"""
try:
querystring = {'username': user.username}
endpoint = getattr(self, 'enterprise-learner')
response = endpoint.get(**querystring)
except (HttpClientError, HttpServerError) as exc:
LOGGER.warning("Unable to retrieve Enterprise Customer Learner details for user {}: {}"
.format(user.username, exc))
raise exc

if response.get('results', None) is None:
raise NotFound('Unable to process Enterprise Customer Learner details for user {}: No Results Found'
.format(user.username))

if response['count'] > 1:
raise ParseError('Multiple Enterprise Customer Learners found for user {}'.format(user.username))

if response['count'] == 0:
return None

return response['results'][0]
22 changes: 22 additions & 0 deletions enterprise_data/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Filters for enterprise data views.
"""
from __future__ import absolute_import, unicode_literals

from rest_framework import filters


class ConsentGrantedFilterBackend(filters.BaseFilterBackend):
"""
Filter backend for any view that needs to filter results where consent has not been granted.
This requires that `CONSENT_GRANTED_FILTER` be set in the view as a class variable, to identify
the object's relationship to the consent_granted field.
"""

def filter_queryset(self, request, queryset, view):
"""
Filter a queryset for results where consent has been granted.
"""
filter_kwargs = {view.CONSENT_GRANTED_FILTER: True}
return queryset.filter(**filter_kwargs)
Loading

0 comments on commit 695a5d4

Please sign in to comment.