django-dataporten 0.4.0
A simple Django app to fetch and parse data from Dataporten.
License: MIT License

=================
Django-dataporten
=================

Django-dataporten is a simple Django app which fetches data from dataporten and
attaches it to your Django user objects. It implements the dataporten groups
API, allowing you to easily access group memberships through a pythonic API, without
worrying about parsing raw JSON content.

Setup
=====

1. Add "dataporten" to your :code:`INSTALLED_APPS` setting like this

.. code:: python

    INSTALLED_APPS = [
        ...
        'dataporten',
        ...
    ]


2. Run `python manage.py migrate` to create the dataporten proxy models.

3. In your `settings.py` file, add the variable :code:`DATAPORTEN_TOKEN_FUNCTION`,
which should be a dotted path to the function that will retrieve user tokens.
Dataporten uses this "importable string" in order to retrieve the OAuth2
authentication token for a given user. For instance,

.. code:: python

    DATAPORTEN_TOKEN_FUNCTION = 'myapp.oauth.allauth_token'

The function should accept a :code:`User` and return a :code:`str`, if the
token exists, else :code:`None`.
Here is a python3.6/3.7 example that will work if you use `django-allauth`_:

.. code:: python

    def allauth_token(user: User) -> Optional[str]:
        try:
            return SocialToken.objects.get(
                account__user=user,
                account__provider='dataporten',
            ).token
        except SocialToken.DoesNotExist:
            return None

4. Add the dataporten middleware. This middleware adds a :code:`dataporten`
attribute to :code:`request.user` for users with an associated
dataporten token. Take care to place it after
:code:`django.contrib.auth.middleware.AuthenticationMiddleware`.

.. code:: python

    MIDDLEWARE = (
        ...
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        ...
        'dataporten.middleware.DataportenGroupsMiddleware',
        ...
    )

5. Optionally, enable caching for API queries. Take care to create the directory
set in :code:`DATAPORTEN_CACHE_PATH` before starting the Django server.

.. code:: python

    # Cache requests to the dataporten API
    DATAPORTEN_CACHE_REQUESTS = True

    # Where to save the sqlite3 cache backend
    DATAPORTEN_CACHE_PATH = 'tmp/'

.. _django-allauth: https://github.com/pennersr/django-allauth:


Usage
=====

The :code:`DataportenGroupsMiddleware` adds an instance of
:code:`DataportenGroupsManager` assigned to :code:`request.user.dataporten` for
every valid dataporten user making a request. This object contains attributes
for accessing different types of group memberships, such as courses, organization
units, study programmes, main profiles, generic groups, and *all* groups.


Groups
------

*All* groups are accessible through :code:`request.user.dataporten.groups`.
This is a dictionary keyed by group ids, with :code:`Group` objects as values.
Let's use the Applied Physics and Mathematics master degree at NTNU as an example
for common attributes available for all group types

.. code:: python

    uid = 'fc:fs:fs:prg:ntnu.no:MTFYMA'
    group = request.user.dataporten.groups[uid]
    assert group.uid == uid
    assert group.name == 'Fysikk og matematikk - masterstudium (5-\u00e5rig)'
    assert group.url == 'http://www.ntnu.no/studier/mtfyma'
    assert group.group_type == 'fc:fs:prg'

Membership objects
------------------

All groups have an associated :code:`Membership` object which can be used for
further querying of membership properties for that particular group.
The original membership JSON can be accessed from the :code:`Membership.json`
attribute:

.. code:: python

    group = request.user.dataporten.groups[uid]
    membership = group.membership

    print(membership.json)
    >>> {
    >>>     'title': ['fast ansatt'],
    >>>     'affiliation': ['employee', 'member', 'affiliate', 'student'],
    >>>     'primaryAffiliation': 'employee',
    >>>     'basic': 'admin',
    >>>     'displayName': 'Ansatt',
    >>> }


Some additional, common properties are available:

.. code:: python

    # Membership objects are "truthy" if they are considered active
    assert membership

    # Not all group memberships have a set end time
    assert isinstance(membership.end_time, [datetime.datetime, None])

    # The displayName value is used as the membership string representation
    assert str(membership) == 'Ansatt'

    # Primary affiliation to the group
    assert membership.primary_affiliation == 'employee'

    # And all affiliations to the group
    assert membership.affiliations == [
        'employee',
        'member',
        'affiliate',
        'student',
    ]


Group membership checks
~~~~~~~~~~~~~~~~~~~~~~~

You can also check if a user is an **active** member of a specific dataporten group
by providing the group :code:`id` to the :code:`DataportenGroupsManager.is_member_of`
method. This is offered as a more ergonomic alternative to
:code:`bool(request.user.dataporten.groups[uid].membership)`. For instance,

.. code:: python

    assert request.user.dataporten.is_member_of(
        uid='fc:org:ntnu.no:unit:167500',
        active=True,
    )

If :code:`active` is set to :code:`False`, the method only checks if the user
has been a member of the group at any time, not necessarily if the user is
an **active** member.

Semester objects
----------------

Membership objects also have an associated :code:`Semester` object which
can be used to determine the year and season of the membership.

.. code:: python

    from dataporten.parsers import Semester

    semester = request.user.groups[uid].membership.semester
    assert semester.year == 2019
    assert semester.season in (Semester.SPRING, Semester.AUTUMN)

The :code:`Semester` class also implements :code:`__sub__`, which
returns "semester delta" between two semesters. For instance,
the spring semester of 2019 minus the autumn semester of 2017 would
return :code:`3`.

Courses
-------

Course enrollment can be queryed from the :code:`CourseManager` object, attributed to
:code:`request.user.dataporten.course`.

You can check if a user has an affiliation to a course, only given
its course code, and not its dataporten ID,

.. code:: python

    # Already finished the course
    assert 'TMA4150' in request.user.dataporten.courses.finished

    # Currently enrolled in the course
    assert 'TMA4150' in request.user.dataporten.courses.active

    # Either
    assert 'TMA4150' in request.user.dataporten.courses.all


More
----

There is still lots of more undocumented (but well tested!) attributes of
:code:`DataportenGroupsManager`. Take a look at :code:`dataporten/parsers.py`.
Each parser has a class variable :code:`NAME`, and they are attached to
the user as :code:`request.user.dataporten.NAME`.

If you have a specific usecase, please open a GitHub issue, and I will
document and/or implement it for you.

Run tests
=========

.. code:: bash

    export DJANGO_SETTINGS_MODULE=dataporten.settings
    pytest