diff --git a/ChangeLog.md b/ChangeLog.md index 8edd64e..988ffec 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [v0.3.3] - 2023-06-28 +### Security +- Upgraded signature to match the security standard. + ### Security - Update dependencies diff --git a/README.md b/README.md index ca012f1..42691b0 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,6 @@ Note: after installation, run this command in the SDK root folder: pip install . -Then, if you're following the tutorial on this page, also run: - - pip install .[quickstart] - ### **Alternative 2: development install from a git clone** To install from the terminal, run the following command: @@ -92,10 +88,6 @@ To set up up your local development environment, use the following: source .venv/learnosity-sdk-python/bin/activate pip install -e . -Then, if you're following the tutorial on this page, also run: - - pip install -e .[quickstart] - Note that these manual installation methods are for development and testing only. For production use, you should install the SDK using the Pip package manager for Python, as described above. @@ -115,7 +107,19 @@ From this point on, we'll assume that your web server is available at this local http://localhost:8000/ -Open this page with your web browser. This is a basic example of an assessment loaded into a web page with Learnosity's assessment player. You can interact with this demo assessment to try out the various Question types. +You can now access the APIs using the following URL [click here](http://localhost:8000). + + + +Following are the routes to access our APIs. + +* Author API : http://localhost:8000/authorapi +* Questions API : http://localhost:8000/questionsapi +* Items API : http://localhost:8000/itemsapi +* Reports API : http://localhost:8000/reportsapi +* Question Editor API : http://localhost:8000/questioneditorapi + +Open these pages with your web browser. These are all basic examples of Learnosity's integration. You can interact with these demo pages to try out the various APIs. The Items API example is a basic example of an assessment loaded into a web page with Learnosity's assessment player. You can interact with this demo assessment to try out the various Question types. @@ -225,7 +229,7 @@ The following example HTML/Jinja template can be found near the bottom of the [s
- + ` tag, which includes Learnosity's Items API on the page and makes the global `LearnosityItems` object available. The version specified as `v2021.2.LTS` will retrieve that specific [Long Term Support (LTS) version](https://help.learnosity.com/hc/en-us/articles/360001268538-Release-Cadence-and-Version-Lifecycle). In production, you should always pin to a specific LTS version to ensure version compatibility. +* The `` tag, which includes Learnosity's Items API on the page and makes the global `LearnosityItems` object available. The version specified as `latest-lts` will retrieve the latest version supported. To know more about switching to specific LTS version visit [Long Term Support (LTS) version](https://help.learnosity.com/hc/en-us/articles/360001268538-Release-Cadence-and-Version-Lifecycle). In production, you should always pin to a specific LTS version to ensure version compatibility. * The call to `LearnosityItems.init()`, which initiates Items API to inject the assessment player into the page. * The variable `{{generated_request}}` dynamically sends the contents of our init options to JavaScript, so it can be passed to `init()`. diff --git a/docs/images/image-quickstart-index.png b/docs/images/image-quickstart-index.png new file mode 100644 index 0000000..5daf00e Binary files /dev/null and b/docs/images/image-quickstart-index.png differ diff --git a/docs/quickstart/assessment/standalone_assessment.py b/docs/quickstart/assessment/standalone_assessment.py index f8c5062..81f4e80 100644 --- a/docs/quickstart/assessment/standalone_assessment.py +++ b/docs/quickstart/assessment/standalone_assessment.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021 Learnosity, Apache 2.0 License +# Copyright (c) 2023 Learnosity, Apache 2.0 License # SPDX-License-Identifier: Apache-2.0 # # Basic example of embedding a standalone assessment using Items API @@ -22,14 +22,24 @@ host = "localhost" port = 8000 +# Public & private security keys required to access Learnosity APIs and +# data. These keys grant access to Learnosity's public demos account. +# Learnosity will provide keys for your own account. +security = { + "user_id" : "abc", + "consumer_key": config.consumer_key, + # Change to the domain used in the browser, e.g. 127.0.0.1, learnosity.com + "domain": host, +} + # Items API configuration parameters. items_request = { - # Unique student identifier, a UUID generated above. + # Unique student identifier, a UUID generated above. "user_id": user_id, # A reference of the Activity to retrieve from the Item bank, defining # which Items will be served in this assessment. "activity_template_id": "quickstart_examples_activity_template_001", - # Uniquely identifies this specific assessment attempt session for + # Uniquely identifies this specific assessment attempt session for # save/resume, data retrieval and reporting purposes. A UUID generated above. "session_id": session_id, # Used in data retrieval and reporting to compare results @@ -38,7 +48,7 @@ # Selects a rendering mode, `assess` type is a "standalone" mode (loading a # complete assessment player for navigation, VS `inline`, for embedded). "rendering_type": "assess", - # Selects the context for the student response storage. `submit_practice` + # Selects the context for the student response storage. `submit_practice` # mode means student response storage in the Learnosity cloud, for grading. "type": "submit_practice", # Human-friendly display name to be shown in reporting. @@ -47,56 +57,440 @@ "state": "initial" } -# Public & private security keys required to access Learnosity APIs and -# data. These keys grant access to Learnosity's public demos account. -# Learnosity will provide keys for your own account. -security = { - 'consumer_key': config.consumer_key, - # Change to the domain used in the browser, e.g. 127.0.0.1, learnosity.com - 'domain': host, -} +# Questions API configuration parameters. +questions_request = { + "id": "f0001", + "name": "Intro Activity - French 101", + "questions": [ + { + "response_id": "60005", + "type": "association", + "stimulus": "Match the cities to the parent nation.", + "stimulus_list": ["London", "Dublin", "Paris", "Sydney"], + "possible_responses": ["Australia", "France", "Ireland", "England" + ], + "validation": { + "valid_responses": [ + ["England"],["Ireland"],["France"],["Australia"] + ] + }, + "instant_feedback": True + } + ], +} + +# Author API configuration parameters. +# mode can be changed by item_list and item_edit +author_request = { + "mode": "item_edit", + "reference": "a15ac409-f6d5-42de-a491-a1e4ab03c826", + "user": { + "id" : "brianmoser", + "firstname" : "Test", + "lastname" : "Test", + "email" : "test@test.com" + }, + "config": { + "global": { + "disable_onbeforeunload": True, + "hide_tags": + [ + { + "type": "internal_category_uuid" + } + ] + }, + "item_edit": { + "item": { + "back": True, + "columns": True, + "answers": True, + "scoring": True, + "reference": { + "edit": False, + "show": False, + "prefix": "LEAR_" + }, + "save": True, + "status": False, + "dynamic_content": True, + "shared_passage": True + }, + "widget": { + "delete": False, + "edit": True + } + }, + "item_list": { + "item": { + "status": True, + "url": "http://myApp.com/items/:reference/edit" + }, + "toolbar": { + "add": True, + "browse": { + "controls": [ + { + "type": "hierarchy", + "hierarchies": [ + { + "reference": "CCSS_Math_Hierarchy", + "label": "CCSS Math" + }, + { + "reference": "CCSS_ELA_Hierarchy", + "label": "CCSS ELA" + }, + { + "reference": "Demo_Items_Hierarchy", + "label": "Demo Items" + } + ] + }, + { + "type": "tag", + "tag": { + "type": "Alignment", + "label": "def456" + } + }, + { + "type": "tag", + "tag": { + "type": "Course", + "label": "commoncore" + } + } + ] + } + }, + "filter": { + "restricted": { + "current_user": True, + "tags": { + "all": [ + { + "type": "Alignment", + "name": ["def456", "abc123"] + }, + { + "type": "Course" + } + ], + "either": [ + { + "type": "Grade", + "name": "4" + }, + { + "type": "Grade", + "name": "5" + }, + { + "type": "Subject", + "name": ["Math", "Science"] + } + ], + "none": [ + { + "type": "Grade", + "name": "6" + } + ] + } + } + } + }, + "dependencies": { + "question_editor_api": { + "init_options": {} + }, + "questions_api": { + "init_options": {} + } + }, + "widget_templates": { + "back": True, + "save": True, + "widget_types": { + "default": "questions", + "show": True + } + }, + "container": { + "height": "auto", + "fixed_footer_height": 0, + "scroll_into_view_selector": "body" + }, + "label_bundle": { + "backButton": "Zurück", + "loadingText": "Wird geladen", + "modalClose": "Schließen", + "saveButton": "Speichern", + "duplicateButton": "Duplikat", + "dateTimeLocale": "en-us", + "toolTipDateTimeSeparator": "um", + "toolTipDateFormat": "DD-MM-YYYY", + "toolTipTimeFormat": "HH:MM:SS", + } + }, + } + +# Reports API configuration parameters. +report_request = { + "reports" : [{ + "id": "session-detail", + "type": "session-detail-by-item", + "user_id": "906d564c-39d4-44ba-8ddc-2d44066e2ba9", + "session_id": "906d564c-39d4-44ba-8ddc-2d44066e2ba9" + }] +} + +# Question Editor API configuration parameters. +question_editor_request = { + "configuration" : { + "consumer_key": config.consumer_key, + }, + "widget_conversion": True, + "ui" : { + "search_field" : True, + }, + "layout":{ + "global_template": "edit_preview", + "mode": "advanced" + } +} # Set up Learnosity initialization data. -init = Init( - 'items', security, config.consumer_secret, - request=items_request +initItems = Init( + "items", security, config.consumer_secret, + request = items_request +) + +initQuestions = Init( + "questions", security, config.consumer_secret, + request = questions_request +) + +initAuthor = Init( + "author", security, config.consumer_secret, + request = author_request +) + +initReports = Init( + "reports", security, config.consumer_secret, + request = report_request +) + +initQuestionEditor = Init( + "questions", security, config.consumer_secret, + request = question_editor_request ) -generated_request = init.generate() + +# Generated request(initOptions) w.r.t all apis +generated_request_Items = initItems.generate() +generated_request_Questions = initQuestions.generate() +generated_request_Author = initAuthor.generate() +generated_request_Reports = initReports.generate() +generated_request_QuestionEditor = initQuestionEditor.generate() # - - - - - - Section 2: your web page configuration - - - - - -# # Set up the HTML page template, for serving to the built-in Python web server class LearnosityServer(BaseHTTPRequestHandler): + + def createResponse(self,response): + # Send headers and data back to the client. + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + # Send the response to the client. + self.wfile.write(response.encode("utf-8")) + def do_GET(self): + + if self.path.endswith("/"): + + # Define the page HTML, as a Jinja template, with {{variables}} passed in. + template = Template(""" + + + + + +

{{ name }}

+ + + + + + + + + + + + + + + + + + + + + + + + + +
APIsLinks
Author APIHere
Questions APIHere
Items APIHere
Reports APIHere
Question Editor APIHere
+ + + """) + + # Render the page template and grab the variables needed. + response = template.render(name='Standalone API Examples') + self.createResponse(response) + + if self.path.endswith("/itemsapi"): # Define the page HTML, as a Jinja template, with {{variables}} passed in. - template = Template(""" - - - - - -

{{ name }}

- -
- - - - - - - """) - - # Render the page template and grab the variables needed. - response = template.render(name='Standalone Assessment Example', generated_request=generated_request) - - # Send headers and data back to the client. - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - # Send the response to the client. - self.wfile.write(response.encode("utf-8")) + template = Template(""" + + +

{{ name }}

+ +
+ + + + + + + """) + + # Render the page template and grab the variables needed. + response = template.render(name='Standalone Items API Example', generated_request=generated_request_Items) + + self.createResponse(response) + + if self.path.endswith("/questionsapi"): + # Define the page HTML, as a Jinja template, with {{variables}} passed in. + template = Template(""" + + +

{{ name }}

+ + + + + + + + + """) + + response = template.render(name='Standalone Questions API Example', generated_request=generated_request_Questions) + self.createResponse(response) + + if self.path.endswith("/authorapi"): + # Define the page HTML, as a Jinja template, with {{variables}} passed in. + template = Template(""" + + +

{{ name }}

+ +
+ + + + + + + """) + + response = template.render(name='Standalone Author API Example', generated_request=generated_request_Author) + self.createResponse(response) + + if self.path.endswith("/reportsapi"): + # Define the page HTML, as a Jinja template, with {{variables}} passed in. + template = Template(""" + + +

{{ name }}

+ + + + + + + + + """) + + response = template.render(name='Standalone Reports API Example', generated_request=generated_request_Reports) + self.createResponse(response) + + if self.path.endswith("/questioneditorapi"): + # Define the page HTML, as a Jinja template, with {{variables}} passed in. + template = Template(""" + + +

{{ name }}

+ +
+ + + + + + + """) + + response = template.render(name='Standalone Question Editor API Example', generated_request=generated_request_QuestionEditor) + self.createResponse(response) def main(): web_server = HTTPServer((host, port), LearnosityServer) @@ -108,4 +502,4 @@ def main(): # Run the web server. if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/learnosity_sdk/_version.py b/learnosity_sdk/_version.py index 7b80bfd..a59e613 100644 --- a/learnosity_sdk/_version.py +++ b/learnosity_sdk/_version.py @@ -1 +1 @@ -__version__ = 'v0.3.2' +__version__ = 'v0.3.3' diff --git a/learnosity_sdk/request/init.py b/learnosity_sdk/request/init.py index 7f0bc4f..9a11067 100644 --- a/learnosity_sdk/request/init.py +++ b/learnosity_sdk/request/init.py @@ -1,6 +1,7 @@ import datetime import hashlib +import hmac import json import platform from learnosity_sdk._version import __version__ @@ -132,9 +133,6 @@ def generate_signature(self): if key in self.security: vals.append(self.security[key]) - # Add the secret. - vals.append(self.secret) - # Add the request if necessary if self.sign_request_data and self.request_string is not None: vals.append(self.request_string) @@ -239,7 +237,9 @@ def set_service_options(self): def hash_list(self, l): "Hash a list by concatenating values with an underscore" - return hashlib.sha256("_".join(l).encode('utf-8')).hexdigest() + concatValues = "_".join(l) + signature = hmac.new(bytes(str(self.secret),'utf_8'), msg = bytes(str(concatValues) , 'utf-8'), digestmod = hashlib.sha256).hexdigest() + return '$02$' + signature def add_telemetry_data(self): if self.__telemetry_enabled: diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index bcffed6..09603bd 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -42,36 +42,36 @@ ] }, None, - '03f4869659eeaca81077785135d5157874f4800e57752bf507891bf39c4d4a90', + '$02$8de51b7601f606a7f32665541026580d09616028dde9a929ce81cf2e88f56eb8', ), ServiceTestSpec( "data", True, None, {"limit": 100}, "get", - 'e1eae0b86148df69173cb3b824275ea73c9c93967f7d17d6957fcdd299c8a4fe', + '$02$e19c8a62fba81ef6baf2731e2ab0512feaf573ca5ca5929c2ee9a77303d2e197', ), ServiceTestSpec( "assess", True, {"user_id": "$ANONYMIZED_USER_ID"}, {"foo": "bar"}, None, - '03f4869659eeaca81077785135d5157874f4800e57752bf507891bf39c4d4a90', + '$02$8de51b7601f606a7f32665541026580d09616028dde9a929ce81cf2e88f56eb8', ), ServiceTestSpec( # string "items", True, {"user_id": "$ANONYMIZED_USER_ID"}, '{ "user_id" : "$ANONYMIZED_USER_ID", "activity_id": "8E9859C2-CBCF-427B-A478-B8FFC5222DEB", "session_id": "E637AC08-7BF1-48AF-B264-0F40D5BF8898", "rendering_type": "assess", "items": [ "item_1" ] }', None, - '584e9c7cae8530e92b258b3ac4361e58484a5e604f0b17d0acd8d7298cb8230a', + '$02$57bfc14e7d1c66d1f370546120dda2195b3ad8ad866c5fcd818c4051389f6df2', ), ServiceTestSpec( # Dict "items", True, {"user_id": "$ANONYMIZED_USER_ID"}, { "user_id" : "$ANONYMIZED_USER_ID", "activity_id": "8E9859C2-CBCF-427B-A478-B8FFC5222DEB", "session_id": "E637AC08-7BF1-48AF-B264-0F40D5BF8898", "rendering_type": "assess", "items": [ "item_1" ] }, None, - '584e9c7cae8530e92b258b3ac4361e58484a5e604f0b17d0acd8d7298cb8230a', + '$02$57bfc14e7d1c66d1f370546120dda2195b3ad8ad866c5fcd818c4051389f6df2', ), ServiceTestSpec( "events", True, None, {"users": [ "$ANONYMIZED_USER_ID_1", "$ANONYMIZED_USER_ID_2", "$ANONYMIZED_USER_ID_3", "$ANONYMIZED_USER_ID_4" ] }, None, - '20739eed410d54a135e8cb3745628834886ab315bfc01693ce9acc0d14dc98bf' + '$02$5c3160dbb9ab4d01774b5c2fc3b01a35ce4f9709c84571c27dfe333d1ca9d349' ), ]