DLKit Overview

DLKit is a Python implementation of the OSIDs (www.osid.org). DLKit specific documentation is available at http://dlkit-doc.readthedocs.io

DLKit exposes Python API bindings of the OSID service contracts, which are organized into a number of service packages. Service packages describe a complete set of functionality for a particular service domain, like assessment, or logging. These service packages define APIs that describe all the entities, or objects related to a particular service domain, as well as the various actions that a programmer can take on these objects. Each service defines a catalog object, which houses / contains the additional objects. In addition to aiding organization, these catalogs also help control authorization to the contained objects, and can be arranged hierarchically (with authorizations flowing down).

         MIT
        /   \
       /     \
   Physics   Math  

Someone with access to all of MIT could see both Math and Physics materials, but folks in Physics may not be able to see Math materials.

Let's start with a simple example and dig more in depth.

Note that all data generated with this tutorial is saved in JSON files to your harddrive, in a folder called tutorial-data, sibling to this .ipynb file. If you ever want to start over, just delete that folder and all its contents.

Assessment Service

In the Assessment Service, the catalog is called a Bank. Banks contain other objects, like Items, Assessments, AssessmentsOffered, AssessmentsTaken, etc. We'll start with Item and Assessment.

To being with, you access the functionality of each service package through a Manager. For example, an AssessmentManager gives you access to the various methods available for accessing Banks and Items, and the other objects defined for assessment.

In order to get a Manager, we go through the runtime -- in this tutorial, the dlkit_runtime runtime. We will simulate a user and a test web request, to pass along to the service manager when its instantiated. This username is automatically available to many kinds of underlying actions and objects, such as when taking assessments.


In [1]:
from dlkit_runtime import PROXY_SESSION, RUNTIME
from dlkit_runtime.proxy_example import TestRequest

condition = PROXY_SESSION.get_proxy_condition()
dummy_request = TestRequest(username='tutorial_user@school.edu',
                            authenticated=True)
condition.set_http_request(dummy_request)
proxy = PROXY_SESSION.get_proxy(condition)
am = RUNTIME.get_service_manager('ASSESSMENT',
                                  proxy=proxy)

print am
item_lookup_session = am.get_item_lookup_session()
print item_lookup_session
print item_lookup_session.get_items()

rm = RUNTIME.get_service_manager('REPOSITORY', proxy=proxy)
print rm


<dlkit.services.assessment.AssessmentManager object at 0x10ef7b990>
<dlkit.services.assessment.Bank object at 0x10f658710>
<dlkit.mongo.assessment.objects.ItemList object at 0x10f631290>
<dlkit.services.repository.RepositoryManager object at 0x10f6a5390>

Banks

Now that we have an AssessmentManager, we can see what Banks exist in the system. Calling for "lists" of things returns a "thing list", like a BankList (a Python generator), and we can check the number of results with .available().


In [ ]:
print am.banks
print am.banks.available()

No banks!! Okay, let's create one. In DLKit, CrUD operations are done with forms. So let's get a form to create a new assessment bank, and assign it a displayName and description that we'll recognize later.

Notice that in the get_bank_form_for_create() method, we pass an empty list as an argument. This can be used to extend the base functionality of the bank, via record extensions. This is a more advanced feature we'll touch on in a bit.


In [ ]:
form = am.get_bank_form_for_create([])
form.display_name = "Class 9"
form.description = "For learning about DLKit"
bank = am.create_bank(form)
print bank
print str(bank.display_name.script_type)
print bank.description.text
print str(bank.ident)
bank.get_items()

You can see that our new Bank has some attributes -- some that we assigned (display_name and description, and others that were created by DLKit, like ident). display_name and description return DisplayText objects that include the text strings we created, but can also contain language, format, and script data, which is why we call display_name.text and description.text above.

Items

Now that we have a Bank, we can create assessment Items in it.

An Item is what you might think of as a basic assessment question with the associated answers (right or wrong). There are many types of Items, including multiple choice, fill in the blank, short answer, etc.

Again, we can inspect to see if any exist.


In [ ]:
print bank.items
print bank.items.available()

To create a new item, we'll grab a form. In the OSIDS, since Items are not defined beyond a question and answer, we'll need to pass along a list of record extensions, so give the item some functionality. This requires some internal knowledge about DLKit, so for now well just use a simple multiple choice record plus accomodation for wrong-answers.


In [ ]:
from dlkit_runtime.primitives import Type
from records.registry import ITEM_RECORD_TYPES
MULTIPLE_CHOICE_ITEM = Type(**ITEM_RECORD_TYPES['multi-choice'])
WRONG_ANSWER_ITEM = Type(**ITEM_RECORD_TYPES['wrong-answer'])

form = bank.get_item_form_for_create([MULTIPLE_CHOICE_ITEM, WRONG_ANSWER_ITEM])
form.display_name = "Basic addition question"
form.description = "addition question with fruit"
item = bank.create_item(form)
print item
print item.get_question()

So we have now created an item, but where's the actual question that we want a student to respond to?? We'll create that separately, with its own records. Recall:

    Item
      |--Question
      |--Answers

Since Questions are distinct from Items, students can be sent only the Question object, without any danger of them seeing any answers . Note that the Question form requires an extra itemId argument. This attaches the question to the Item created previously.


In [ ]:
from records.registry import QUESTION_RECORD_TYPES
MULTIPLE_CHOICE_QUESTION = Type(**QUESTION_RECORD_TYPES['multi-choice-text'])

form = bank.get_question_form_for_create(item.ident, [MULTIPLE_CHOICE_QUESTION])
form.set_text("Which color do you prefer?")
form.add_choice("blue")
form.add_choice("red")
form.add_choice("yellow")
question = bank.create_question(form)

print question.get_choices()
print question.get_text().text

form = bank.get_question_form_for_update(question.ident)
form.set_text('why is the sky blue?')
question = bank.update_question(form)

print question.get_text()
print question.get_text().text
print str(question.get_text().language_type)
print str(question.get_text().script_type)
print str(question.get_text().format_type)

The Item has a method that allows us to get access to the Question.


In [ ]:
print item.get_question()

What?!?! Oh, wait ... because the Item object we have was initialized before the Question was actually created, we need to re-grab that Item to get it in the "newest" state. Note that this is a due to the particular service implementation we are using. Some implementations might actually keep objects up-to-date with the underlying persistence (like database or filespace) and never let an object get stale. you can check if an object is known to be up-to-date with underlying data by calling it's is_current() method. If it is not, you can refresh the object, in this case our Item by calling the get_item(item_id) method of the Bank.


In [ ]:
print item.is_current()
if not item.is_current():
    item = bank.get_item(item.ident)
print item.get_question()
print item.get_question().get_choices()

We can also set up wrong / right Answers, to be used in evaluating the "correctness" of the response. We indicate the type of Answer with the genusTypeId property.


In [ ]:
from records.registry import ANSWER_GENUS_TYPES, ANSWER_RECORD_TYPES
from dlkit.primordium.transport.objects import DataInputStream
MULTIPLE_CHOICE_ANSWER = Type(**ANSWER_RECORD_TYPES['multi-choice'])
FEEDBACK_ANSWER = Type(**ANSWER_RECORD_TYPES['answer-with-feedback'])
FILES_ANSWER = Type(**ANSWER_RECORD_TYPES['files'])
RIGHT_ANSWER = Type(**ANSWER_GENUS_TYPES['right-answer'])
WRONG_ANSWER = Type(**ANSWER_GENUS_TYPES['wrong-answer'])

TEST_FILE = '/Users/cjshaw/Desktop/Captura de pantalla 2016-09-20 a las 4.45.05 PM.png'
print repository
form = bank.get_answer_form_for_create(item.ident, [MULTIPLE_CHOICE_ANSWER, FILES_ANSWER, FEEDBACK_ANSWER])
form.set_genus_type(RIGHT_ANSWER)
# We'll just set "blue" as the right answer, arbitrarily
form.add_choice_id('57fe61edcdfc5c3a0bc23b49')
form.set_feedback('Correct!')
with open(TEST_FILE, 'r') as test_file:
    form.add_file(DataInputStream(test_file),
                  label="testFile",
                  asset_name="random screenshot",
                  asset_description="for testing")
answer1 = bank.create_answer(form)
print answer1.feedback
print answer1.object_map['fileIds']

form = bank.get_answer_form_for_create(item.ident, [MULTIPLE_CHOICE_ANSWER])
form.set_genus_type(WRONG_ANSWER)
# and "yellow" as the wrong answer
form.add_choice_id('57fe61edcdfc5c3a0bc23b4b')
answer2 = bank.create_answer(form)
print "done creating answers"

Now we can find the Item Answers (if you run the above block multiple times, you'll get a varying number of Answer elements.


In [ ]:
item = bank.get_item(item.ident)
print item.get_answers().available()
print [a.object_map for a in item.get_answers()]

Wait ... where are all the wrong Answers? There should be 2x the number of times you ran the block, Answers ... and by looking at the genusTypeId attributes, it seems like all the Answers from get_answers() are only "right-answer" types.

This is because DLKit defines the get_answers() method to only return the "right" answers, so the "wrong" answers need to be retrieved with a different method, defined by the Item record extension for wrong answers.


In [ ]:
print item.get_wrong_answers().available()
print [a.object_map for a in item.get_wrong_answers()]

There they are!

That's a basic overview of Items. They are very powerful, and to learn more, you can inspect the records directory that came along with this tutorial.

Assessments

Conceptually, Items are organized into Assessments -- which are then offered to students. An Assessment in a classroom might be a homework, a quiz, or an exam ... a tool that evaluates students' knowledge of a topic or outcome.

Let's see what Assessments exist in our Bank.


In [ ]:
print bank.get_assessments()
print bank.get_assessments().available()

If we're starting with a clean slate, we have no Assessments. Let's get a form, and create one. Note that with the current DLKit configuration, we need to include at least the simple-child-sequencing record for each Assessment.


In [ ]:
from records.registry import ASSESSMENT_RECORD_TYPES
SIMPLE_SEQUENCE_ASSESSMENT = Type(**ASSESSMENT_RECORD_TYPES['simple-child-sequencing'])

form = bank.get_assessment_form_for_create([SIMPLE_SEQUENCE_ASSESSMENT])
form.display_name = 'Homework #1'
form.description = 'Favorites'
assessment = bank.create_assessment(form)
print assessment
print bank.get_assessment_items(assessment.ident).available()

Now that we have our Assessment, we can add Items to it.


In [ ]:
bank.add_item(assessment.ident, item.ident)
print bank.get_assessment_items(assessment.ident).available()

This is great for authoring Assessments, but DLKit defines another set of methods to offer the Assessment in "student mode". This is through the AssessmentSession methods which define actions for taking assessments -- but first, we need to create an AssessmentOffered. AssessmentsOffered wrap some other information around the canonical Assessment, like the start_time, deadline, duration, etc. -- all of these typically are optional. Leaving them out "opens" the AssessmentOffered to students immediately, and it never "closes".

Again, we need a form for this, and that method requires the Assessment ID.


In [ ]:
form = bank.get_assessment_offered_form_for_create(assessment.ident, [])
assessment_offered = bank.create_assessment_offered(form)
print assessment_offered

Once we have an AssessmentOffered, we can provide a set of methods for students to take the assessment. Each "taker" generates an AssessmentTaken, which links a taker's user ID (which was provided through the Proxy when the AssessmentManager was initially set up) to the AssessmentOffered ID. The AssessmentTaken also links to one or more AssessmentSections, which maintains a list of questions takers have seen and thier responses.


In [ ]:
form = bank.get_assessment_taken_form_for_create(assessment_offered.ident, [])
assessment_taken = bank.create_assessment_taken(form)
print assessment_taken
print str(assessment_taken.get_taking_agent_id().identifier)

There are several ways a taker can get the Assessment questions ... they are the same for our simple Assessment because there is only one question, but you can easily imagine how the behavior might change when multiple questions are present. For sequential Assessments, each question must be answered in order. Hence, you iterate through the questions using methods like get_first_question() & get_next_question() or get_first_unanswered_question() & get_next_unanswered_question(). Alternatively, if the Assessment does not specify that the questions must be answered sequentially, you can get all the questions in bulk, via get_questions(). In more complex and adaptive scenarios, even this method might be dynamically updated, as students navigate through the Assessment.

Assessments can be divided into multiple AssessmentSections, for example to provide UI separation. In our simple Assessment, there is only one AssessmentSection, and we'll use that ID to grab the questions.

Let's demonstrate the methods.


In [ ]:
assessment_section = bank.get_first_assessment_section(assessment_taken.ident)
print str(bank.get_first_question(assessment_section.ident).ident)
print str(bank.get_first_unanswered_question(assessment_section.ident).ident)
print [str(q.ident) for q in bank.get_questions(assessment_section.ident)]

Now, it's interesting to note that this question ID is different than the original Item ID.


In [ ]:
print str(item.ident)

This is because questions, when put into an AssessmentSection, generate new, unique IDs. This helps us manage more advanced Item and Question records that manipulate the ID attribute to provide adaptability or consistent randomization -- which is beyond the scope of this basic tutorial.

However, we will use this new question ID to submit a Response. Again, we'll use a form, and we'll supply it with a choice ID from earlier.


In [ ]:
question = bank.get_first_question(assessment_section.ident)
choices = question.get_choices()
print choices

form = bank.get_response_form(assessment_section.ident, question.ident)
form.add_choice_id(choices[1]['id'])
bank.submit_response(assessment_section.ident, question.ident, form)
response = bank.get_response(assessment_section.ident, question.ident)
print response.is_correct()
print response.get_submission_time()

Note that this last method, is_correct(), is a non-OSID convenience method, that currently only works with multiple choice questions, but can easily be integrated into any record extension.

We can try submitting again, if the AssessmentOffered allows us to.


In [ ]:
form = bank.get_response_form(assessment_section.ident, question.ident)
form.add_choice_id(choices[0]['id'])
bank.submit_response(assessment_section.ident, question.ident, form)
response = bank.get_response(assessment_section.ident, question.ident)
print response.is_correct()
print response.get_submission_time()

Note that both submissions are stored in the database, but currently only the most recent one can be retrieved. Time data in DLKit is stored as UTC, so the information printed above may differ from your local time.

At any time, instructors can get a record of all student Responses and questions.


In [ ]:
responses = bank.get_assessment_taken_responses(assessment_taken.ident)
print [r.object_map for r in responses]

At the moment, the results only include the Responses without regard to AssessmentSections, but you can easily link in the actual questions as well. Sample code is provided below.


In [ ]:
question_maps = []
for index, question in enumerate(assessment_section.get_questions()):
    question_map = question.object_map
    question_map.update({
            'itemId': assessment_section._my_map['questions'][index]['itemId'],
            'responses': []
        })
    question_maps.append(question_map)
for index, response in enumerate(responses):
    question_maps[index]['responses'].append(response.object_map)
print question_maps

Note that we add back in the itemId attribute so that we can map the questions that students see (now with all unique IDs) back to the original Items.

But, um...why is the Responses list empty? Because OsidLists are exhaustive -- they are Python generators. So they can only be iterated through once. We can solve that either by converting to a list, or calling the get_assessment_taken_responses() method again.


In [ ]:
responses_list = list(bank.get_assessment_taken_responses(assessment_taken.ident))
responses = bank.get_assessment_taken_responses(assessment_taken.ident)
question_maps = []
for index, question in enumerate(assessment_section.get_questions()):
    question_map = question.object_map
    question_map.update({
            'itemId': assessment_section._my_map['questions'][index]['itemId'],
            'responses': []
        })
    question_maps.append(question_map)
for index, response in enumerate(responses):
    question_maps[index]['responses'].append(response.object_map)
print question_maps

There you have the basics of the Assessment service! Much of the additional complexity appears in the record extensions for various Item types and Assessment / AssessmentOffered / AssessmentTaken settings.

For example, we have randomized multiple choice questions where the choices appear in different orders to each student, but when students return to the AssessmentTaken, they see the exact same order they've seen before -- randomized per student, not per view of the question.

Another example of a complex assessment is adaptive behavior, where the "next question" a student sees depends on their response to the previous question. So each students gets a different set of questions, depending on their knowledge and performance.

Object Patterns in DLKit

Now that we've gone through the Assessment service, you've hopefully learned the basics about catalogs, objects, forms, sessions, and managers. Luckily, these patterns appear across all the DLKit services. So you can easily pick up how to use the other services. http://osid.org/ is a good reference, and we've included a simple table below that reflects the services available in this DLKit build:

-------------------------------------------------------------------------------------------------------------
|   Service     |      Catalog     |                            Objects                                     |
-------------------------------------------------------------------------------------------------------------
| Assessment    | Bank             | Item, Assessment, AssessmentOffered, AssessmentTaken, AssessmentPart   |
| Authorization | Vault            | Authorization                                                          |
| Commenting    | Book             | Comment                                                                |
| Grading       | Gradebook        | GradeSystem, GradeEntry, GradebookColumn                               |
| Logging       | Log              | LogEntry                                                               |
| Repository    | Repository       | Asset, AssetContent, Composition                                       |
| Resource      | Bin              | Resource                                                               |
-------------------------------------------------------------------------------------------------------------

Repository Service Example


In [4]:
print rm

form = rm.get_repository_form_for_create([])
form.display_name = 'test repository'
repo = rm.create_repository(form)
print repo


<dlkit.services.repository.RepositoryManager object at 0x10f6a5390>
<dlkit.services.repository.Repository object at 0x10eca4390>

Assets

Assets are digital "things", like a video, an HTML page, an image, etc. It includes things like copyright, license, etc., but the actual digital bits are stored in Asset Contents.

For example, an asset might represent a video, but it might include assetContents for a transcript, an .mp4 version, a thumbnail, etc.

    Asset
      |--AssetContents

To make this work with and save files to your filesystem (instead of GridFS), you need to include an assetContent record type.


In [5]:
from dlkit.primordium.transport.objects import DataInputStream
from dlkit.primordium.id.primitives import Id

form = repo.get_asset_form_for_create([])
form.display_name = 'My Image'
asset = repo.create_asset(form)


asset_content_type_list = []
try:
    config = repo._catalog._runtime.get_configuration()
    parameter_id = Id('parameter:assetContentRecordTypeForFiles@filesystem')
    asset_content_type_list.append(
        config.get_value_by_parameter(parameter_id).get_type_value())
except (AttributeError, KeyError):
    pass



form = repo.get_asset_content_form_for_create(asset.ident, asset_content_type_list)
with open('/Users/cjshaw/Documents/Projects/CLIx/dlkit-tutorial/files/draggable.green.dot.png', 'r') as dot:
    form.set_data(DataInputStream(dot))
repo.create_asset_content(form)


Out[5]:
<dlkit.filesystem.repository.objects.AssetContent at 0x10fba9150>

Compositions

Compositions are organizational "buckets" that allow you to organize assets into things like "chapters" or "sections". They can have children compositions or include assets.

    Composition
          |--Composition
          |     |--Asset
          |--Asset

For example, if you represent this like a book, it might be:

    Chapter
          |--Section
          |      |--Example problem
          |--Video

In [ ]:
form = repo.get_composition_form_for_create([])
form.display_name = 'Activity 1'
activity_1 = repo.create_composition(form)

form = repo.get_composition_form_for_create([])
form.display_name = 'Lesson 1'
form.set_children([activity_1.ident])
lesson_1 = repo.create_composition(form)

repo.add_asset(asset.ident, lesson_1.ident)

form = repo.get_composition_form_for_create([])
form.display_name = 'Unit 1'
form.set_children([lesson_1.ident])
unit_1 = repo.create_composition(form)

print unit_1.get_children().available()
print repo.get_composition_assets(lesson_1.ident).available()

Resource Service

In the Resource Service, the catalogs are called bins and the objects are called resources. While this sounds very generic, one specific use case is for representing users. Resources have a displayName and description, and also an avatar -- for a profile picture or something. We'll re-use the green-dot asset from above, so make sure you run those cells in the Repository Service first.


In [2]:
resm = RUNTIME.get_service_manager('RESOURCE', proxy=proxy)
print resm


<dlkit.services.resource.ResourceManager object at 0x10f96d090>

In [9]:
form = resm.get_bin_form_for_create([])
form.display_name = 'My bin'
bin = resm.create_bin(form)

print bin

form = bin.get_resource_form_for_create([])
form.display_name = "Cole Shaw"
form.description = "Software Developer"
form.set_avatar(asset.ident)
resource = bin.create_resource(form)

print resource.get_avatar()
print resource.get_avatar().get_asset_contents().next().get_data().read()


<dlkit.services.resource.Bin object at 0x10f9f1a50>
<dlkit.filesystem.repository.objects.Asset object at 0x10fbb9050>
�PNG

�d���C�'f����Y�e9�&X��0Wd�T|
T��	7S����+�X��{�	��9��~eXL�3��`Z{��b?%��q^��-K�$��/wmnI�2'��r��J:a���k��׌z�V��ė�+�5��f�(^e�+�����煀�i�"���o�IEND�B`�

In [ ]: