Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Unreleased
- Added `kolibri-constants/Licenses`
- `licenses.generate_list` no longer casts ids to `int` or fills `description`
- `archive.contents_sha256` reads members in stored order
- Removed `resources/kindlookup.json`; use `content_kinds.KINDLIST`
- Added `kolibri-constants/ContentKinds`


0.2.17 Apr 13, 2026
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Content kinds (ContentNode subclasses)
Content items throughout the Kolibri ecosystem come in several kinds. The `kind`
attribute of each object can be one of ("topic", "video", "audio", "exercise"
"document", or "html5".
See [constants/content_kinds.py](https://github.com/learningequality/le-utils/blob/master/le_utils/constants/content_kinds.py#L11-L17) for latest list.
See [constants/content_kinds.py](https://github.com/learningequality/le-utils/blob/master/le_utils/constants/content_kinds.py#L9-L18) for latest list.

The currently supported content kinds are:
- Topic node (folder)
Expand Down
43 changes: 43 additions & 0 deletions js/ContentKinds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// -*- coding: utf-8 -*-
// Generated by scripts/generate_from_specs.py
// ContentKinds

export default {
TOPIC: "topic",
VIDEO: "video",
AUDIO: "audio",
EXERCISE: "exercise",
DOCUMENT: "document",
HTML5: "html5",
SLIDESHOW: "slideshow",
H5P: "h5p",
ZIM: "zim",
QUIZ: "quiz",
};

export const KINDLIST = [
{ id: "topic", name: "topic" },
{ id: "video", name: "video" },
{ id: "audio", name: "audio" },
{ id: "exercise", name: "exercise" },
{ id: "document", name: "document" },
{ id: "html5", name: "html5" },
{ id: "slideshow", name: "slideshow" },
{ id: "h5p", name: "h5p" },
{ id: "zim", name: "zim" },
{ id: "quiz", name: "quiz" },
];

export const MAPPING = {
"mp4": "video",
"webm": "video",
"mp3": "audio",
"perseus": "exercise",
"pdf": "document",
"epub": "document",
"bloompub": "document",
"bloomd": "document",
"zip": "html5",
"h5p": "h5p",
"zim": "zim",
};
64 changes: 33 additions & 31 deletions le_utils/constants/content_kinds.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import json
import pkgutil
from collections import namedtuple

from le_utils.constants import file_formats
# -*- coding: utf-8 -*-
# Generated by scripts/generate_from_specs.py
from __future__ import unicode_literals

""" Content Kind Constants """
from collections import namedtuple

# IMPORTANT: Keep these constants in sync with kindlookup.json and presetlookup.json
# ContentKinds

TOPIC = "topic"
VIDEO = "video"
Expand All @@ -19,6 +17,20 @@
ZIM = "zim"
QUIZ = "quiz"

MAPPING = {
"mp4": VIDEO,
"webm": VIDEO,
"mp3": AUDIO,
"perseus": EXERCISE,
"pdf": DOCUMENT,
"epub": DOCUMENT,
"bloompub": DOCUMENT,
"bloomd": DOCUMENT,
"zip": HTML5,
"h5p": H5P,
"zim": ZIM,
}

choices = (
(TOPIC, "Topic"),
(VIDEO, "Video"),
Expand All @@ -32,35 +44,25 @@
(QUIZ, "Quiz"),
)

""" File Format (extension) to Content Kind Mapping """
MAPPING = {
file_formats.MP4: VIDEO,
file_formats.WEBM: VIDEO,
file_formats.MP3: AUDIO,
file_formats.PDF: DOCUMENT,
file_formats.EPUB: DOCUMENT,
file_formats.PERSEUS: EXERCISE,
file_formats.HTML5: HTML5,
file_formats.H5P: H5P,
file_formats.ZIM: ZIM,
file_formats.BLOOMPUB: DOCUMENT,
file_formats.BLOOMD: DOCUMENT,
}


class Kind(namedtuple("Kind", ["id", "name"])):
pass


def generate_list(constantlist):
for id, kind in constantlist.items():
yield Kind(id=id, **kind)


def _initialize_kind_list():
constantlist = json.loads(pkgutil.get_data("le_utils", "resources/kindlookup.json").decode("utf-8"))

return generate_list(constantlist)
for id, record in constantlist.items():
yield Kind(id=id, **record)


KINDLIST = list(_initialize_kind_list())
KINDLIST = [
Kind(id="topic", name="topic"),
Kind(id="video", name="video"),
Kind(id="audio", name="audio"),
Kind(id="exercise", name="exercise"),
Kind(id="document", name="document"),
Kind(id="html5", name="html5"),
Kind(id="slideshow", name="slideshow"),
Kind(id="h5p", name="h5p"),
Kind(id="zim", name="zim"),
Kind(id="quiz", name="quiz"),
]
32 changes: 0 additions & 32 deletions le_utils/resources/kindlookup.json

This file was deleted.

48 changes: 47 additions & 1 deletion scripts/generate_from_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,29 @@ def namedtuple_tuples(spec, constants):
return tuples


def namedtuple_mappings(spec, constants):
mappings = OrderedDict()
name = spec["namedtuple"]["name"]
defined_names = set(constants) | set(spec.get("tuples", {}))
defined_names |= {name, "{}LIST".format(name.upper()), "choices", "default", "generate_list", "namedtuple", "unicode_literals"}
if "getter" in spec["namedtuple"]:
defined_names |= {spec["namedtuple"]["getter"], "_{}LOOKUP".format(name.upper())}
for mapping_name, entries in spec.get("mappings", {}).items():
if mapping_name in defined_names:
raise ValueError("Mapping {} is already defined".format(mapping_name))
mapping = mappings[mapping_name] = OrderedDict()
for constant, keys in entries.items():
if constant not in constants:
raise ValueError("Mapping {} references undefined constant {}".format(mapping_name, constant))
if not isinstance(keys, list):
raise ValueError("Mapping {} keys for {} are not a list".format(mapping_name, constant))
for key in keys:
if key in mapping:
raise ValueError("Mapping {} maps {} twice".format(mapping_name, key))
mapping[key] = constant
return mappings


def namedtuple_choices(spec):
if "choices_order" not in spec:
return None
Expand Down Expand Up @@ -315,6 +338,15 @@ def write_python_records(f, spec):
f.write("]\n")


def write_python_mappings(f, mappings):
for mapping_name, mapping in mappings.items():
f.write("\n")
f.write("{} = {{\n".format(mapping_name))
for key, constant in mapping.items():
f.write(" {!r}: {},\n".format(key, constant))
f.write("}\n")


def write_python_getter(f, spec):
upper = spec["namedtuple"]["name"].upper()
f.write("\n")
Expand All @@ -328,6 +360,7 @@ def write_python_getter(f, spec):
def write_namedtuple_python_file(output_file, name, spec):
constants = namedtuple_constants(spec)
tuples = namedtuple_tuples(spec, constants)
mappings = namedtuple_mappings(spec, constants)
choices = namedtuple_choices(spec)
with open(output_file, "w") as f:
write_python_header(f)
Expand All @@ -340,6 +373,7 @@ def write_namedtuple_python_file(output_file, name, spec):
for tuple_name, members in tuples.items():
f.write("\n")
f.write("{} = ({},)\n".format(tuple_name, ", ".join(members)))
write_python_mappings(f, mappings)
f.write("\n")
if choices is not None:
write_python_choices(f, choices)
Expand All @@ -354,21 +388,33 @@ def js_object(pairs):
return "{{ {} }}".format(", ".join("{}: {}".format(key, json.dumps(value)) for key, value in pairs))


def write_js_mappings(f, mappings, constants):
for mapping_name, mapping in mappings.items():
f.write("\n")
f.write("export const {} = {{\n".format(mapping_name))
for key, constant in mapping.items():
f.write(" {}: {},\n".format(json.dumps(key), json.dumps(constants[constant])))
f.write("};\n")


def write_namedtuple_js_file(output_file, name, spec):
fields = spec["namedtuple"]["fields"]
constants = namedtuple_constants(spec)
mappings = namedtuple_mappings(spec, constants)
with open(output_file, "w") as f:
write_js_header(f)
f.write("// {}\n".format(name))
f.write("\n")
f.write("export default {\n")
for constant, value in namedtuple_constants(spec).items():
for constant, value in constants.items():
f.write(" {}: {},\n".format(constant, json.dumps(value)))
f.write("};\n")
f.write("\n")
f.write("export const {}LIST = [\n".format(spec["namedtuple"]["name"].upper()))
for record in spec["records"]:
f.write(" {},\n".format(js_object((field, record[field]) for field in fields)))
f.write("];\n")
write_js_mappings(f, mappings, constants)


def write_labels_src_files(label_outputs):
Expand Down
91 changes: 91 additions & 0 deletions spec/constants-content_kinds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"namedtuple": {
"name": "Kind",
"fields": ["id", "name"]
},
"records": [
{
"constant": "TOPIC",
"label": "Topic",
"id": "topic",
"name": "topic"
},
{
"constant": "VIDEO",
"label": "Video",
"id": "video",
"name": "video"
},
{
"constant": "AUDIO",
"label": "Audio",
"id": "audio",
"name": "audio"
},
{
"constant": "EXERCISE",
"label": "Exercise",
"id": "exercise",
"name": "exercise"
},
{
"constant": "DOCUMENT",
"label": "Document",
"id": "document",
"name": "document"
},
{
"constant": "HTML5",
"label": "HTML5 App",
"id": "html5",
"name": "html5"
},
{
"constant": "SLIDESHOW",
"label": "Slideshow",
"id": "slideshow",
"name": "slideshow"
},
{
"constant": "H5P",
"label": "H5P",
"id": "h5p",
"name": "h5p"
},
{
"constant": "ZIM",
"label": "Zim",
"id": "zim",
"name": "zim"
},
{
"constant": "QUIZ",
"label": "Quiz",
"id": "quiz",
"name": "quiz"
}
],
"choices_order": [
"TOPIC",
"VIDEO",
"AUDIO",
"EXERCISE",
"DOCUMENT",
"HTML5",
"SLIDESHOW",
"H5P",
"ZIM",
"QUIZ"
],
"mappings": {
"MAPPING": {
"VIDEO": ["mp4", "webm"],
"AUDIO": ["mp3"],
"EXERCISE": ["perseus"],
"DOCUMENT": ["pdf", "epub", "bloompub", "bloomd"],
"HTML5": ["zip"],
"H5P": ["h5p"],
"ZIM": ["zim"]
}
}
}
Loading
Loading