{"id":45007058,"url":"https://github.com/learningequality/le-utils","last_synced_at":"2026-04-02T12:14:54.381Z","repository":{"id":14685591,"uuid":"17405039","full_name":"learningequality/le-utils","owner":"learningequality","description":"Utilities and constants shared across Kolibri, Ricecooker, and Kolibri Studio","archived":false,"fork":false,"pushed_at":"2026-03-24T22:12:24.000Z","size":595,"stargazers_count":7,"open_issues_count":17,"forks_count":36,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-03-26T01:53:13.809Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/learningequality.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2014-03-04T14:33:02.000Z","updated_at":"2026-03-24T22:06:09.000Z","dependencies_parsed_at":"2024-05-02T19:43:48.300Z","dependency_job_id":"034b6623-9db0-428a-9986-1a304d5892f6","html_url":"https://github.com/learningequality/le-utils","commit_stats":{"total_commits":266,"total_committers":23,"mean_commits":"11.565217391304348","dds":0.8007518796992481,"last_synced_commit":"e8f54a2e40e8dfc819c607fd13add88f548ea3d5"},"previous_names":[],"tags_count":38,"template":false,"template_full_name":null,"purl":"pkg:github/learningequality/le-utils","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/learningequality%2Fle-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/learningequality%2Fle-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/learningequality%2Fle-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/learningequality%2Fle-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/learningequality","download_url":"https://codeload.github.com/learningequality/le-utils/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/learningequality%2Fle-utils/sbom","scorecard":{"id":582117,"data":{"date":"2025-08-11","repo":{"name":"github.com/learningequality/le-utils","commit":"b934a8d7a29d242fcba2bae2ea58670d6ad2969b"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.9,"checks":[{"name":"Maintained","score":10,"reason":"13 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":9,"reason":"Found 11/12 approved changesets -- score normalized to 9","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/call-contributor-issue-comment.yml:1","Warn: no topLevel permission defined: .github/workflows/call-manage-issue-header.yml:1","Warn: no topLevel permission defined: .github/workflows/community-contribution-labeling.yml:1","Warn: no topLevel permission defined: .github/workflows/finalized_specs.yml:1","Warn: no topLevel permission defined: .github/workflows/npm-publish.yml:1","Warn: no topLevel permission defined: .github/workflows/pre-commit.yml:1","Warn: no topLevel permission defined: .github/workflows/pypi-publish.yml:1","Warn: no topLevel permission defined: .github/workflows/pythontest.yml:1","Warn: no topLevel permission defined: .github/workflows/unassign-inactive.yaml:1","Warn: no topLevel permission defined: .github/workflows/update-pr-spreadsheet.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: third-party GitHubAction not pinned by hash: .github/workflows/call-contributor-issue-comment.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/call-contributor-issue-comment.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/call-manage-issue-header.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/call-manage-issue-header.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/community-contribution-labeling.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/community-contribution-labeling.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/finalized_specs.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/finalized_specs.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/npm-publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/npm-publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/npm-publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/npm-publish.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pre-commit.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pre-commit.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pre-commit.yml:27: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pre-commit.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pre-commit.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pre-commit.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pre-commit.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pre-commit.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pypi-publish.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pypi-publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pypi-publish.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pypi-publish.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pypi-publish.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pypi-publish.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pythontest.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:70: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:74: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pythontest.yml:85: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/pythontest.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/unassign-inactive.yaml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/unassign-inactive.yaml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/update-pr-spreadsheet.yml:8: update your workflow using https://app.stepsecurity.io/secureworkflow/learningequality/le-utils/update-pr-spreadsheet.yml/main?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/npm-publish.yml:27","Warn: pipCommand not pinned by hash: .github/workflows/npm-publish.yml:28","Warn: pipCommand not pinned by hash: .github/workflows/pre-commit.yml:30","Warn: pipCommand not pinned by hash: .github/workflows/pypi-publish.yml:25","Warn: pipCommand not pinned by hash: .github/workflows/pypi-publish.yml:26","Warn: pipCommand not pinned by hash: .github/workflows/pythontest.yml:47","Warn: pipCommand not pinned by hash: .github/workflows/pythontest.yml:48","Warn: pipCommand not pinned by hash: .github/workflows/pythontest.yml:81","Warn: pipCommand not pinned by hash: .github/workflows/pythontest.yml:82","Info:   0 out of  16 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   9 third-party GitHubAction dependencies pinned","Info:   0 out of   9 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/pypi-publish.yml:10"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Branch-Protection","score":4,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'main'","Info: 'force pushes' disabled on branch 'main'","Warn: 'branch protection settings apply to administrators' is disabled on branch 'main'","Warn: 'stale review dismissal' is disabled on branch 'main'","Warn: required approving review count is 1 on branch 'main'","Warn: codeowners review is not required on branch 'main'","Warn: 'last push approval' is disabled on branch 'main'","Warn: 'up-to-date branches' is disabled on branch 'main'","Info: status check found to merge onto on branch 'main'","Info: PRs are required in order to make changes on branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-20T19:27:05.304Z","repository_id":14685591,"created_at":"2025-08-20T19:27:05.304Z","updated_at":"2025-08-20T19:27:05.304Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305981,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-18T23:04:47.604Z","updated_at":"2026-04-02T12:14:54.376Z","avatar_url":"https://github.com/learningequality.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"LE Utils\n========\nThe `le-utils` package contains shared constants used by Ricecooker, Kolibri, and\nKolibri Studio. This package is not meant to be installed or used directly, but\nplays an important role in all Learning Equality products.\n\nHow can I contribute?\n=====================\n\nWe welcome contributors!\n\nTo find out how to contribute, visit [Contributing to our open code base](https://learningequality.org/contributing-to-our-open-code-base).\n\nConstants\n=========\nThe Python files in the [le_utils/constants/](./le_utils/constants) are used to\ndefine constants (usually in `ALL_CAPS` form) to be used from Python code.\nThe same constants and naming conventions are also provided in JSON format in the\nfolder [le_utils/resources/](le_utils/resources) for use in frontend code.\nThis means, adding a new constant may require editing multiple files: the Python\nconstant-defining file, the JSON-file, and any associated tests.\n\n\nLanguages\n---------\nThe file [le_utils/constants/languages.py](./le_utils/constants/languages.py) and\nthe lookup table in [le_utils/resources/languagelookup.json](./le_utils/resources/languagelookup.json)\ndefine the internal representation for languages codes used by Ricecooker, Kolibri,\nand Kolibri Studio to identify educational content in different languages.\n\nThe internal representation uses a mixture of two-letter codes (e.g. `en`),\ntwo-letter-and-country code (e.g. `pt-BR` for Brazilian Portuguese),\nand three-letter codes (e.g., `zul` for Zulu).\n\nIn order to make sure you have the correct language code when interfacing with\nthe Kolibri ecosystem (e.g. when uploading new content to Kolibri Studio), you\nmust lookup the language object using the helper method `getlang`:\n\n```\n\u003e\u003e\u003e from le_utils.constants.languages import getlang\n\u003e\u003e\u003e language_obj = getlang('en')       # lookup language using language code\n\u003e\u003e\u003e language_obj\nLanguage(native_name='English', primary_code='en', subcode=None, name='English', ka_name=None)\n```\nThe function `getlang` will return `None` if the lookup fails. In such cases, you\ncan try lookup by name or lookup by alpha2 code (ISO_639-1) methods defined below.\n\nOnce you've successfully looked up the language object, you can obtain the internal\nrepresentation language code from the language object's `code` attribute:\n```\n\u003e\u003e\u003e language_obj.code\n'en'\n```\nThe Ricecooker API expects these internal representation language codes will be\nsupplied for all `language` attributes (channel language, node language, and files language).\n\n\n\n### More lookup helper methods\nThe helper method `getlang_by_name` allows you to lookup a language by name:\n```\n\u003e\u003e\u003e from le_utils.constants.languages import getlang_by_name\n\u003e\u003e\u003e language_obj = getlang_by_name('English')  # lookup language by name\n\u003e\u003e\u003e language_obj\nLanguage(native_name='English', primary_code='en', subcode=None, name='English', ka_name=None)\n```\n\nThe module `le_utils.constants.languages` defines two other language lookup methods:\n  - Use `getlang_by_native_name` for lookup up names by native language name,\n    e.g., you look for 'Français' to find French.\n -  Use `getlang_by_alpha2` to perform lookups using the standard two-letter codes\n    defined in [ISO_639-1](https://en.wikipedia.org/wiki/ISO_639-1) that are\n    supported by the `pycountries` library.\n\n\n#### Useful links\n\nThe following websites are useful for researching language codes:\n\n  - https://www.ethnologue.com/\n  - https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes\n\n\n\nLicenses\n--------\nAll content nodes within Kolibri and Kolibri Studio must have a license. The file\n[le_utils/constants/licenses.py](./le_utils/constants/licenses.py) contains the\nconstants used to identify the license types. These constants are meant to be\nused in conjunction with the helper method `ricecooker.classes.licenses.get_license`\nto create `Licence` objects.\n\nTo initialize a license object, you must specify the license type and the\n`copyright_holder` (str) which identifies a person or an organization. For example:\n```\nfrom ricecooker.classes.licenses import get_license\nfrom le_utils.constants import licenses\nlicense = get_license(licenses.CC_BY, copyright_holder=\"Khan Academy\")\n```\n\nNote: The `copyright_holder` field is required for all License types except for\nthe public domain license for which `copyright_holder` can be None.\n\n\n\n\nContent kinds (ContentNode subclasses)\n--------------------------------------\nContent items throughout the Kolibri ecosystem come in several kinds. The `kind`\nattribute of each object can be one of (\"topic\", \"video\", \"audio\", \"exercise\"\n\"document\", or \"html5\".\nSee [constants/content_kinds.py](https://github.com/learningequality/le-utils/blob/master/le_utils/constants/content_kinds.py#L11-L17) for latest list.\n\nThe currently supported content kinds are:\n  - Topic node (folder)\n  - Video content nodes backed by a video files and subtitles\n  - Audio content nodes backed by an audio files\n  - Document content nodes backed by a document files (PDF or ePub)\n  - HTML5 app content nodes backed by a HTML5 zip files\n  - Slideshow content nodes\n  - Exercise content nodes\n\nThe `kind` attribute identifies a subclass of the base content node class within\nthe data model, which differs on Ricecooker, Studio, and Kolibri:\n  - [ricecooker.classes.nodes.ContentNode](https://github.com/learningequality/ricecooker/blob/master/ricecooker/classes/nodes.py#L428-L506):\n    in-memory content node used to store metadata needed to upload new content to Kolibri  Studio\n  - [contentcuration.contentcuration.models.ContentNode](https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/models.py#L775):\n    node within one of the trees associated with a Kolibri Studio channel.\n  - [kolibri.core.content.models.ContentNode](https://github.com/learningequality/kolibri/blob/develop/kolibri/core/content/models.py#L175):\n    node within tree for a particular version of a channel on Kolibri.\n\nFor a detailed description of the common and different model attributes available\non content nodes in each part of the platform see [this doc](https://docs.google.com/spreadsheets/d/181hSEwJ7yVmMh7LEwaHENqQetYSsbSDwybHTO_0zZM0/edit#gid=1640972430).\n\n\n\nFile formats (extensions)\n-------------------------\nThese are low-level constant that represents what type of file and are essentially\nsynonymous with file extensions. The file format `MP4` is simply a convenient\nproxy for the file extension `mp4`.\nSee [file_formats.py](https://github.com/learningequality/le-utils/blob/master/le_utils/constants/file_formats.py)\nand [resourcces/formatlookup.json](https://github.com/nucleogenesis/le-utils/blob/master/le_utils/resources/formatlookup.json).\n\n\n\nFormat presets (ContentNode-File relation)\n------------------------------------------\nEvery `ContentNode` is associated with one or more `File` objects and nature of\nthis association is represented though the `format_preset` attribute of the file.\nThe `format_preset` is the role the file is playing in the content node,\ne.g., thumbnail, high resolution video, or low resolution video.\nNote that format presets are represented redundantly as python string in\n[constants/format_presets.py](https://github.com/learningequality/le-utils/blob/master/le_utils/constants/format_presets.py)\nand as json [resources/presetlookup.json](https://github.com/learningequality/le-utils/blob/master/le_utils/resources/presetlookup.json).\n\nYou can think of the different format presets on a content node as different \"slots\"\nto be filled in by files, with certain slots being required while other optional.\nFor examples, for a VideoNode (kind=`video`) to be a valid content node, it must\nhave at least one video file associated with it filling either the `high_res_video`\nslot or the `low_res_video` slot. Certain slots can have multiple files in them,\nlike the `video_subtitle` preset, since a VideoNode can have multiple subtitles\nassociated with it for different languages.\n\n\nThe figure below illustrates the structure between content nodes, files, and format presets.\n\n![Illustration of the relationships between content kinds (nodes), files, and format presets.](docs/img/le-utils_constants_and_mapping.png)\n\nIn the Sample shown, the Video Node is of content kind `video` and has three\nfiles associated with it:\n  - The first file has file format `mp4` and format preset `high_res_video`\n  - The second file is also in `mp4` format but the relation to the content node\n    is that `low_res_video`\n  - A third file with format `vtt` is associated with the content node with a\n    format preset of `video_subtitle`.\n\nFormat presets play a crucial role throughout the Kolibri content ecosystem and\ngovern such things as content validation rules applied by Ricecooker, Kolibri Studio\nedit rules, and the rendering logic on Kolibri.\n\n\nFile types (ricecooker.files.File subclasses)\n---------------------------------------------\nUsed on Ricecooker as identifiers to represents what type of file when serializing\nthings to JSON as part of the content import process. Note that file types constants\nare internal to ricecooker operations and are not used in Kolibri Studio or Kolibri.\n\n\n\nExercises\n---------\nThe file [le_utils/constants/exercises.py](./le_utils/constants/exercises.py)\ncontains identifiers for different question types and mastery models.\n\n\n\nProquint Channel Tokens\n-----------------------\nThe file [le_utils/proquint.py](./le_utils/proquint.py) contains helper methods\nfor generating proquint identifiers for content channels. These are short strings\nthat are easy to enter on devices without a full keyboard, e.g. `sutul-hakuh`.\n\n\nRoles\n-----\nThe `role` constants are used for Role-based access control (RBAC) within the\nKolibri platform. Currently, only two levels of visibility are supported:\n  - `learner` (default): content nodes are visible to all Kolibri users\n  - `coach`: content nodes are only visible to Kolibri coaches and administrators\n\n\nMetadata labels\n---------------\n\nThese are encoded in spec/labels-v*.json. Once a spec has been finalized it will be\nadded to finalized_specs.yml to ensure that CI will fail any future modifications\nto this specification. This ensures that the resulting built code has consistent\nordering so that generated bits for bitmasks are stable across releases.\nWe also require that all names in the specs be globally unique to minimize confusion\nand reduce the chance of collisions in translations of these terms for users.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flearningequality%2Fle-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flearningequality%2Fle-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flearningequality%2Fle-utils/lists"}