Tutorial: DLKit Learning Service Basics¶
This tutorial is under development. It currently focuses on aspects of the Learning
service. At the
time of this writing, MIT’s Office of Digital Learning is supporting a production learning objective management
service called the MIT Core Concept Catalog (MC3). DLKit includes an underlying implementation that uses MC3 for
persistence. As a result, this tutorial uses examples primarily from this particular service, which deals with
managing learning objectives, learning paths and relationships between learning objectives and educational
assets, assessments, etc, since there is data available for testing.
All of the other DLKit Interface Specifications build on most of the same patterns outlined in this tutorial, beginning with loading managers. Make sure that the dlkit package is in your python path or install the library.
The Runtime Manager and Proxy Authentication¶
Service managers are instantiated through a Runtime Manger, which are designed to work with certain runtime environments,
like Django or edX/XBlock runtimes. To get access to these runtime environments please contact dlkit-info@mit.edu. Install the
runtime environment you want to use and make sure that your Django project’s settings.py includes dlkit_django
or
dlkit_xblock
as appropriate.
Now you can get the RuntimeManager
root instance for your runtime environment. Note that there is only one, and
it gets instantiated at environment launch time, it is thread-safe and used by all consumer application sessions:
from dlkit_django import RUNTIME
This runtime
object is your gateway to access all the underlying service managers and their respective service sessions and functionality
The django runtime knows about Django’s own services for users. You will have access to an HTTPRequest object that includes an user authentication (the request variable in the examples below). This needs to be encapsulated in a Proxy object:
from dlkit_django import PROXY_SESSION
condition = PROXY_SESSION.get_proxy_condition()
condition.set_http_request(request)
proxy = PROXY_SESSION.get_proxy(condition)
Or, if you are standing up dlkit in edX, get an XBlockUser() object from the xblock runtime. That object is assumed to be stored the ‘xblock_user’ variable below:
from dlkit_xblock import PROXY_SESSION
condition = PROXY_SESSION.get_proxy_condition()
condition.set_xblock_user(xblock_user)
proxy = PROXY_SESSION.get_proxy(condition)
Now you have a Proxy object that holds the user data and eventually other stuff, like locale information, etc, that can be used to instantiate new service Managers, which you can also insert into your HttpRequest.session:
from dlkit_django import RUNTIME
request.session.lm = RUNTIME.get_service_manager('LEARNING', proxy)
For the duration of the session you can use this for all the other things. that you normally do.
There is a lot more you can do with the RuntimeManager
, but getting service managers will be the most common task. One of the other
important tasks of this manager, is configuration the underlying service stack based on the configs.py
file and associated helpers. We
will cover this later.
Loading the Learning Manager¶
All consumer applications wishing to use the DLKit Learning service should start by instantiating
a LearningManager
(don’t worry about the proxy for now):
lm = runtime.get_service_manager('LEARNING')
Everything you need to do within the learning service can now be
accessed through this manager. An OSID service Manager
is used like a factory, providing all
the other objects necessary for using the service. You should never try to instantiate any
other OSID object directly, even if you know where its class definition resides.
The simplest thing you can do with a manager is to inspect its display_name
and description
methods. Note that DLKit always returns user-readable strings as DisplayText
objects. The actual text is available via the get_text()
method.
Other DisplayText
methods return the LanguageType
, ScriptType
and
FormatType
of the text to be displayed:
print "Learning Manager successfully instantiated:"
print " " + lm.get_display_name().get_text()
print " " + lm.get_description().get_text()
print (" (this description was written using the " +
lm.get_description().get_language_type().get_display_label().get_text() +
" language)\n")
Results in something that looks like this:
Learning Manager successfully instantiated:
MC3 Learning Service
OSID learning service implementation of the MIT Core Concept Catalog (MC3)
(this description was written using the English language)
# Note that the implementation name and description may be different for you.
# It will depend on which underlying service implementation your dlkit library is
# configured to point to. More on this later
Alternatively, the Python OSID service interfaces also specify property attributes for all basic “getter” methods, so the above could also be written more Pythonically as:
print "Learning Manager successfully instantiated:"
print " " + lm.display_name.text
print " " + lm.description.text
print (" (this description was written using the " +
lm.description.language_type.display_label.text + " language)\n")
For the remainder of this tutorial we will use the property attributes wherever available.
Looking up Objective Banks¶
Managers encapsulate service profile information, allowing a consumer application to ask questions about which functions are supported in the underlying service implementations that it manages:
if lm.supports_objective_bank_lookup():
print "This Learning Manager can be used to lookup ObjectiveBanks"
else:
print "What a lame Learning Manager. It can't even lookup ObjectiveBanks"
The LearningManager
also provides methods for getting ObjectiveBanks
.
One of the most useful is get_objective_banks(), which will return an iterator
containing all the banks known to the underlying implementations. This is
also available as a property, so treating objective_banks
as an
attribute works here too:
if lm.supports_objective_bank_lookup():
banks = lm.objective_banks
for bank in banks:
print bank.display_name.text
else:
print "Objective bank lookup is not supported."
This will print a list of the names of all the banks, which can be thought of as catalogs for organizing learning objectives and other related information. At the time of this writing the following resulted:
Crosslinks
Chemistry Bridge
i2.002
Python Test Sandbox
x.xxx
Note that the OSIDs specify to first ask whether a functional area is supported
before trying to use it. However, if you wish to adhere to the Pythonic EAFP (easier
to ask forgiveness than permission) programming style, managers will throw an
Unimplemented
exception if support is not available:
try:
banks = lm.objective_banks
except Unimplemented:
print "Objective bank lookup is not supported."
else:
for bank in banks:
print bank.display_name.text
The object returned from the call to get_objective_banks()
is an
OsidList
object, which as you can see from the example is just a Python iterator.
Like all iterators it is “wasting”, meaning that, unlike a Python list
it
will be completely used up and empty after all the elements have been retrieved.
Like any iterator an OsidList
object can be cast as a more persistent Python
list, like so:
banks = list(obls.objective_banks)
Which is useful if the consuming application needs to keep it around for a while.
However, when we start dealing with OsidLists
from service implementations which
may return very large result sets, or where the underlying data changes often, casting
as a list
may not be wise. Developers are encouraged to treat these as
iterators to the extent possible, and refresh from the session as necessary.
You can also inspect the number of available elements in the expected way:
len(obls.objective_banks)
# or
banks = obls.objective_banks
len(banks)
And walk through the list one-at-a-time, in for
statements, or by calling next()
:
banks = lm.objective_banks
crosslinks_bank = banks.next() # At the time of this writing, Crosslinks was first
chem_bridge_bank = banks.next() # and Chemistry Bridge was second
OSID Ids¶
To begin working with OSID objects, like ObjectiveBanks
it is important to understand
how the OSIDs deal with identity. When an OSID object is asked for its id
an OSID Id
object is returned. This is not a ``string``. It is the unique identifier object
for the OSID object. Any requests for getting a specific object by its unique identifier will be
accomplished through passing this Id
object back through the service.
Ids
are obtained by calling an OSID object’s get_id()
method,
which also provides an ident
attribute property associated with it for convenience
(id
is a reserved word in Python so it is not used)
OsidObject.ident |
Gets the Id associated with this instance of this OSID object. |
So we can try this out:
crosslinks_bank_id = crosslinks_bank.ident
chem_bridge_bank_id = chem_bridge_bank.ident
Ids
can be compared for equality:
crosslinks_bank_id == chem_bridge_bank_id
# should return False
crosslinks_bank_id in [crosslinks_bank_id, chem_bridge_bank_id]
# should return True
If a consumer wishes to persist the identifier then it should serialize the
returned Id
object, and all Ids can provide a string representation for this purpose:
id_str_to_perist = str(crosslinks_bank_id)
A consumer application can also stand up an Id from a persisted string. There is an implementation of the Id primitive object available through the runtime environment for this purpose. For instance, from the dlkit_django package:
from dlkit_django.primordium import Id
crosslinks_bank_id = Id(id_str_to_persist)
Once an application has its hands on an Id
object it can go ahead and
retrieve the corresponding Osid Object through a Lookup Session:
crosslinks_bank_redux = obls.get_objective_bank(crosslinks_bank_id)
We now have two different objects representing the same Crosslinks bank,
which can be determined by comparing Ids
:
crosslinks_bank_redux == crosslinks_bank
# should be False
crosslinks_bank_redux.ident == crosslinks_bank_id
# should be True
Looking up Objectives¶
ObjectiveBanks provide methods for looking up and retrieving learning
Objectives
, in bulk, by Id
, or by Type
. Some of the more useful
methods include:
ObjectiveBank.can_lookup_objectives () |
Tests if this user can perform Objective lookups. |
ObjectiveBank.objectives |
Gets all Objectives . |
ObjectiveBank.get_objective (objective_id) |
Gets the Objective specified by its Id . |
ObjectiveBank.get_objectives_by_genus_type (...) |
Gets an ObjectiveList corresponding to the given objective genus Type which does not include objectives of genus types derived from the specified Type . |
So let’s try to find an Objective
in the Crosslinks bank with a display name of
“Definite integral”. (Note, that there are also methods for
querying Objectives
by various attributes. More on that later.):
for objective in crosslinks_bank:
if objective.display_name.text == 'Definite integral':
def_int_obj = objective
Now we have our hands on an honest-to-goodness learning objective as defined by an honest-to-goodness professor at MIT!
Authorization Hints¶
Many service implementations will require authentication and authorization for
security purposes (authn/authz). Authorization checks will be done when the consuming application
actually tries to invoke a method for which authz is required, and if
its found that the currently logged-in user is not authorized, then the implementation
will raise a PermissionDenied
error.
However, sometimes its nice to be able to check in advance whether or not the user
is likely to be denied access. This way a consuming application can decide, for
instance, to hide or “gray out” UI widgets for doing un-permitted functions. This
is what the methods like can_lookup_objectives
are for. They simply return a
boolean
.
The Objective Object¶
Objectives
inherit from OsidObjects
(ObjectiveBanks
do too, by the way),
which means there are a few methods they share with all other OsidObjects
defined by
the specification
OsidObject.display_name |
Gets the preferred display name associated with this instance of this OSID object appropriate for display to the user. |
OsidObject.description |
Gets the description associated with this instance of this OSID object. |
OsidObject.genus_type |
Gets the genus type of this object. |
The display_name
and description
attributes work exactly like they did for
ObjectiveBanks
and both return a Displaytext
object that can be interrogated
for its text or the format, language, script of the text to be displayed. We’ll get
to genus_type
in a little bit
Additionally Objectives
objects can hold some information particular to the kind
of data that they manage:
Objective.has_assessment () |
Tests if an assessment is associated with this objective. |
Objective.assessment |
Gets the assessment associated with this learning objective. |
Objective.assessment_id |
Gets the assessment Id associated with this learning objective. |
Objective.has_cognitive_process () |
Tests if this objective has a cognitive process type. |
Objective.cognitive_process |
Gets the grade associated with the cognitive process. |
Objective.cognitive_process_id |
Gets the grade Id associated with the cognitive process. |
Objective.has_knowledge_category () |
Tests if this objective has a knowledge dimension. |
Objective.knowledge_category |
Gets the grade associated with the knowledge dimension. |
Objective.knowledge_category_id |
Gets the grade Id associated with the knowledge dimension. |
OSID Types¶
The OSID specification defines Types
as a way to indicate additional agreements
between service consumers and service providers. A Type is similar to an Id but
includes other data for display and organization:
Type.display_name |
Gets the full display name of this Type . |
Type.display_label |
Gets the shorter display label for this Type . |
Type.description |
Gets a description of this Type . |
Type.domain |
Gets the domain. |
Types
also include identification elements so as to uniquely identify one Type
from another:
Type.authority |
Gets the authority of this Type . |
Type.namespace |
Gets the namespace of the identifier. |
Type.identifier |
Gets the identifier of this Type . |
Consuming applications will often need to persist Types
for future use.
Persisting a type reference requires persisting all three of these identification
elements.
For instance the MC3 service implementation supports two different types of
Objectives
, which help differentiate between topic type objectives and
learning outcome type objectives. One way to store, say, the topic type for
future programmatic reference might be to put it in a dict:
OBJECTIVE_TOPIC_TYPE = {
'authority': 'MIT-OEIT',
'namespace': 'mc3-objective',
'identifier': 'mc3.learning.topic'
}
The OSIDs also specify functions for looking up types that are defined
and/or supported, and as one might imagine, there is TypeLookupSession
specifically
designed for this purpose. This session, however, is not defined in the learning
service package, it is found in the type package, which therefore requires
a TypeManager
be instantiated:
tm = runtime.get_service_manager('LEARNING', <proxy>)
...
if tm.supports_type_lookup():
tls = tm.get_type_lookup_session()
The TypeLookupSession
provides a number of ways to get types, two of which are
sufficient to get started:
TypeLookupSession.types |
Gets all the known Types. |
TypeLookupSession.get_type (namespace, ...) |
Gets a Type by its string representation which is a combination of the authority and identifier. |
For kicks, let’s print a list of all the Types
supported by the implementation:
for typ in tls.types:
print typ.display_label.text
# For the MC3 implementation this should result in a very long list
Also, we can pass the dict
we created earlier to the session to get the actual
Type
object representing the three identification elements we persisted:
topic_type = tls.get_type(**OBJECTIVE_TOPIC_TYPE)
print topic_type.display_label.text + ': ' + topic_type.description.text
# This should print the string 'Topic: Objective that represents a learning topic'
(More to come)