PageRenderTime 77ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/kalite/topic_tools/content_models.py

https://gitlab.com/gregtyka/ka-lite
Python | 753 lines | 740 code | 0 blank | 13 comment | 2 complexity | aa798f86dd37f070ed33636b08f51050 MD5 | raw file
  1. """
  2. This module acts as the only interface point between the main app and the database backend for the content.
  3. It exposes several convenience functions for accessing content, which fall into two broad categories:
  4. Topic functions - which return a limited set of fields, for use in rendering topic tree type structures.
  5. Content functions - which return a full set of fields, for use in rendering content or reasoning about it.
  6. In addition, content can either be returned in an exanded format, where all fields are directly represented on
  7. the dictionary, or with many of the fields collapsed into an 'extra_fields' key.
  8. All functions return the model data as a dictionary, in order to prevent external functions from having to know
  9. implementation details about the model class used in this module.
  10. """
  11. import json
  12. import itertools
  13. from peewee import Model, SqliteDatabase, CharField, TextField, BooleanField, ForeignKeyField, PrimaryKeyField, Using,\
  14. DoesNotExist, fn, IntegerField, OperationalError, FloatField
  15. from playhouse.shortcuts import model_to_dict
  16. from .base import available_content_databases
  17. from .settings import CONTENT_DATABASE_PATH, CHANNEL
  18. from .annotate import update_content_availability
  19. from django.conf import settings
  20. logging = settings.LOG
  21. # This Item is defined without a database.
  22. # This allows us to use a separate database for each language, so that we
  23. # can reduce performance cost, and keep queries simple for multiple languages.
  24. # In addition, we can distribute databases separately for each language pack.
  25. class Item(Model):
  26. title = CharField()
  27. description = TextField()
  28. available = BooleanField()
  29. files_complete = IntegerField(default=0)
  30. total_files = IntegerField(default=0)
  31. kind = CharField()
  32. parent = ForeignKeyField("self", default=None, null=True, index=True, related_name="children")
  33. id = CharField(index=True)
  34. pk = PrimaryKeyField(primary_key=True)
  35. slug = CharField()
  36. path = CharField(index=True, unique=True)
  37. extra_fields = CharField(null=True, default="")
  38. youtube_id = CharField(null=True, default="")
  39. size_on_disk = IntegerField(default=0)
  40. remote_size = IntegerField(default=0)
  41. sort_order = FloatField(default=0)
  42. class Meta:
  43. # Order by sort_order by default for all queries.
  44. order_by = ('sort_order',)
  45. def __init__(self, *args, **kwargs):
  46. kwargs = parse_model_data(kwargs)
  47. super(Item, self).__init__(*args, **kwargs)
  48. class AssessmentItem(Model):
  49. id = CharField(max_length=50)
  50. # looks like peewee doesn't like a primary key field that's not an integer.
  51. # Hence, we have a separate field for the primary key.
  52. pk = PrimaryKeyField(primary_key=True)
  53. item_data = TextField() # A serialized JSON blob
  54. author_names = CharField(max_length=200) # A serialized JSON list
  55. def parse_model_data(item):
  56. extra_fields = item.get("extra_fields", {})
  57. if type(extra_fields) is not dict:
  58. extra_fields = json.loads(extra_fields)
  59. remove_keys = []
  60. for key, value in item.iteritems():
  61. if key not in Item._meta.fields:
  62. extra_fields[key] = value
  63. remove_keys.append(key)
  64. for key in remove_keys:
  65. del item[key]
  66. item["extra_fields"] = json.dumps(extra_fields)
  67. return item
  68. def unparse_model_data(item):
  69. extra_fields = json.loads(item.get("extra_fields", "{}"))
  70. # Do this to ensure any model fields that have accidentally
  71. # been folded into extra fields are not overwritten on output
  72. extra_fields.update(item)
  73. return extra_fields
  74. def set_database(function):
  75. """
  76. Sets the appropriate database for the ensuing model interactions.
  77. """
  78. def wrapper(*args, **kwargs):
  79. language = kwargs.get("language", "en")
  80. path = kwargs.pop("database_path", None)
  81. if not path:
  82. path = CONTENT_DATABASE_PATH.format(
  83. channel=kwargs.get("channel", CHANNEL),
  84. language=language
  85. )
  86. db = SqliteDatabase(path)
  87. kwargs["db"] = db
  88. db.connect()
  89. # This should contain all models in the database to make them available to the wrapped function
  90. with Using(db, [Item, AssessmentItem]):
  91. try:
  92. output = function(*args, **kwargs)
  93. except DoesNotExist:
  94. output = None
  95. except OperationalError:
  96. logging.error("No content database file found")
  97. raise
  98. db.close()
  99. return output
  100. return wrapper
  101. def parse_data(function):
  102. """
  103. Parses the output of functions to be dicts (and expanded extra_fields if needed)
  104. """
  105. def wrapper(*args, **kwargs):
  106. dicts = kwargs.get("dicts", True)
  107. expanded = kwargs.get("expanded", True)
  108. output = function(*args, **kwargs)
  109. if dicts and output:
  110. try:
  111. if expanded:
  112. output = map(unparse_model_data, output.dicts())
  113. else:
  114. output = [item for item in output.dicts()]
  115. except (TypeError, OperationalError):
  116. logging.warn("No content database file found")
  117. output = []
  118. return output
  119. return wrapper
  120. @parse_data
  121. @set_database
  122. def get_random_content(kinds=None, limit=1, **kwargs):
  123. """
  124. Convenience function for returning random content nodes for use in testing
  125. :param kinds: A list of node kinds to select from.
  126. :param limit: The maximum number of items to return.
  127. :return: A list of randomly selected content dictionaries.
  128. """
  129. if not kinds:
  130. kinds = ["Video", "Audio", "Exercise", "Document"]
  131. return Item.select().where(Item.kind.in_(kinds)).order_by(fn.Random()).limit(limit)
  132. @set_database
  133. def get_content_item(content_id=None, topic=False, **kwargs):
  134. """
  135. Convenience function for returning a fully fleshed out content node for use in rendering content
  136. To save server processing, the extra_fields are fleshed out on the client side.
  137. By default, don't return topic nodes to avoid id collisions.
  138. :param content_id: The content_id to select by - caution, this is a non-unique field.
  139. :param topic: Return non-topic or topic nodes - default to non-topics.
  140. :return: A single content dictionary.
  141. """
  142. if content_id:
  143. # Ignore topics in case of id collision.
  144. if topic:
  145. value = Item.get(Item.id == content_id, Item.kind == "Topic")
  146. else:
  147. value = Item.get(Item.id == content_id, Item.kind != "Topic")
  148. return model_to_dict(value)
  149. @parse_data
  150. @set_database
  151. def get_content_items(ids=None, **kwargs):
  152. """
  153. Convenience function for returning multiple topic tree nodes for use in rendering content
  154. :param ids: A list of node ids to select - as ids are non-unique a single id may return multiple content items.
  155. :return: A list of content dictionaries.
  156. """
  157. if ids:
  158. values = Item.select().where(Item.id.in_(ids))
  159. else:
  160. values = Item.select()
  161. return values
  162. @parse_data
  163. @set_database
  164. def get_topic_nodes(parent=None, ids=None, **kwargs):
  165. """
  166. Convenience function for returning a set of topic nodes with limited fields for rendering the topic tree
  167. Can either pass in the parent id to return all the immediate children of a node,
  168. or a list of ids to return an arbitrary set of nodes with limited fields.
  169. :param parent: id of a parent node (always a topic).
  170. :param ids: A list of ids to return.
  171. :return: A list of content dictionaries with limited fields.
  172. """
  173. if parent:
  174. Parent = Item.alias()
  175. if parent == "root":
  176. selector = Parent.parent.is_null()
  177. else:
  178. selector = Parent.id == parent
  179. values = Item.select(
  180. Item.title,
  181. Item.description,
  182. Item.available,
  183. Item.kind,
  184. Item.children,
  185. Item.id,
  186. Item.path,
  187. Item.slug,
  188. ).join(Parent, on=(Item.parent == Parent.pk)).where(selector & Item.available)
  189. return values
  190. elif ids:
  191. values = Item.select(
  192. Item.title,
  193. Item.description,
  194. Item.available,
  195. Item.kind,
  196. Item.children,
  197. Item.id,
  198. Item.path,
  199. Item.slug,
  200. ).where(Item.id.in_(ids))
  201. return values
  202. @parse_data
  203. @set_database
  204. def get_topic_update_nodes(parent=None, **kwargs):
  205. """
  206. Convenience function for returning a set of topic nodes with limited fields for rendering the update topic tree
  207. :param parent: id of a parent node (always a topic).
  208. :return: A list of content dictionaries with limited fields.
  209. """
  210. if parent:
  211. Parent = Item.alias()
  212. if parent == "root":
  213. selector = Parent.parent.is_null()
  214. else:
  215. selector = Parent.id == parent
  216. values = Item.select(
  217. Item.title,
  218. Item.description,
  219. Item.available,
  220. Item.kind,
  221. Item.pk,
  222. Item.size_on_disk,
  223. Item.remote_size,
  224. Item.files_complete,
  225. Item.total_files,
  226. Item.id,
  227. Item.path,
  228. Item.youtube_id,
  229. ).join(Parent, on=(Item.parent == Parent.pk)).where((selector) & (Item.total_files != 0))
  230. return values
  231. @set_database
  232. def get_topic_node(content_id=None, topic=True, **kwargs):
  233. """
  234. Convenience function for returning a topic/content node with limited fields
  235. :param content_id: A list of ids to return.
  236. :return: A list of content dictionaries with limited fields.
  237. """
  238. if content_id:
  239. if topic:
  240. kind_selector = Item.kind == "Topic"
  241. else:
  242. kind_selector = Item.kind != "Topic"
  243. value = Item.select(
  244. Item.title,
  245. Item.description,
  246. Item.available,
  247. Item.kind,
  248. Item.children,
  249. Item.id,
  250. Item.path,
  251. Item.slug,
  252. ).where((Item.id == content_id) & (kind_selector)).get()
  253. return model_to_dict(value)
  254. @set_database
  255. def get_topic_nodes_with_children(parent=None, **kwargs):
  256. """
  257. Convenience function for returning a set of topic nodes with children listed as ids.
  258. Used for parsing and traversing the topic tree in content recommendation.
  259. :param parent: id of a parent node (always a topic).
  260. :return: A list of content dictionaries with the specified parent, with a children field as a list of ids.
  261. """
  262. if parent:
  263. Parent = Item.alias()
  264. Child = Item.alias()
  265. if parent == "root":
  266. selector = Parent.parent.is_null()
  267. else:
  268. selector = Parent.id == parent
  269. child_values = [item for item in Item.select(
  270. Child
  271. ).join(Child, on=(Child.parent == Item.pk)).join(Parent, on=(Item.parent == Parent.pk)).where(selector).dicts()]
  272. parent_values = [item for item in Item.select(
  273. Item
  274. ).join(Parent, on=(Item.parent == Parent.pk)).where(selector).dicts()]
  275. topics = []
  276. for topic in parent_values:
  277. output = {}
  278. output.update(topic)
  279. output["children"] = [child["id"] for child in child_values if child["parent"] == topic["pk"]]
  280. topics.append(output)
  281. return topics
  282. @parse_data
  283. @set_database
  284. def get_content_parents(ids=None, **kwargs):
  285. """
  286. Convenience function for returning all parent nodes of a set of content as specified by ids.
  287. :param ids: A list of topic ids.
  288. :return: A list of content dictionaries.
  289. """
  290. if ids:
  291. Parent = Item.alias()
  292. parent_values = Item.select(
  293. Parent
  294. ).join(Parent, on=(Item.parent == Parent.pk)).where(Item.id.in_(ids)).distinct()
  295. if parent_values is None:
  296. parent_values = list()
  297. return parent_values
  298. else:
  299. return list()
  300. @parse_data
  301. @set_database
  302. def get_leafed_topics(kinds=None, db=None, **kwargs):
  303. """
  304. Convenience function for returning a set of topic nodes that contain content
  305. """
  306. if not kinds:
  307. kinds = ["Video", "Audio", "Exercise", "Document"]
  308. Parent = Item.alias()
  309. parent_values = Item.select(
  310. Parent
  311. ).join(Parent, on=(Item.parent == Parent.pk)).where(Item.kind.in_(kinds)).distinct()
  312. return parent_values
  313. @parse_data
  314. @set_database
  315. def get_topic_contents(kinds=None, topic_id=None, **kwargs):
  316. """
  317. Convenience function for returning a set of content/leaf nodes contained within a topic
  318. :param kinds: A list of content kinds to select from.
  319. :param topic_id: The id of the topic to select within.
  320. :return: A list of content dictionaries.
  321. """
  322. if topic_id:
  323. topic_node = Item.get(Item.id == topic_id, Item.kind == "Topic")
  324. if not kinds:
  325. kinds = ["Video", "Audio", "Exercise", "Document"]
  326. return Item.select(Item).where(Item.kind.in_(kinds), Item.path.contains(topic_node.path))
  327. @set_database
  328. def get_download_youtube_ids(paths=None, downloaded=False, **kwargs):
  329. """
  330. Convenience function for taking a list of content ids and returning
  331. all associated youtube_ids for downloads, regardless of whether the input
  332. paths are paths for content nodes or topic nodes
  333. :param paths: A list of paths to nodes - used to ensure uniqueness.
  334. :param downloaded: Boolean to select whether to return files that have been downloaded already or not.
  335. :return: A unique list of youtube_ids as strings.
  336. """
  337. if paths:
  338. youtube_ids = dict()
  339. for path in paths:
  340. selector = (Item.kind != "Topic") & (Item.path.contains(path)) & (Item.youtube_id.is_null(False))
  341. if downloaded:
  342. selector &= Item.files_complete > 0
  343. else:
  344. selector &= Item.files_complete == 0
  345. youtube_ids.update(dict([item for item in Item.select(Item.youtube_id, Item.title).where(selector).tuples() if item[0]]))
  346. return youtube_ids
  347. def get_video_from_youtube_id(youtube_id):
  348. """
  349. This function is provided to ensure that the data migration 0029_set_video_id_for_realz
  350. in the main app is still able to be run if needed.
  351. It searches through every available content database in order to find the associated content id
  352. for a particular youtube id.
  353. :param youtube_id: String containing a youtube id.
  354. :return: A dictionary containing video metadata.
  355. """
  356. for channel, language in available_content_databases():
  357. video = _get_video_from_youtube_id(channel=channel, language=language, youtube_id=youtube_id)
  358. if video:
  359. return video
  360. @parse_data
  361. @set_database
  362. def _get_video_from_youtube_id(youtube_id=None, **kwargs):
  363. """
  364. Convenience function for returning a fully fleshed out video content node from youtube_id
  365. :param youtube_id: String containing a youtube id.
  366. :return: A dictionary containing video metadata.
  367. """
  368. if youtube_id:
  369. value = Item.get(Item.youtube_id == youtube_id, Item.kind == "Video")
  370. return model_to_dict(value)
  371. @set_database
  372. def search_topic_nodes(kinds=None, query=None, page=1, items_per_page=10, exact=True, **kwargs):
  373. """
  374. Search all nodes and return limited fields.
  375. :param kinds: A list of content kinds.
  376. :param query: Text string to search for in titles or extra fields.
  377. :param page: Which page of the paginated search to return.
  378. :param items_per_page: How many items on each page of the paginated search.
  379. :param exact: Flag to allow for an exact match, if false, always return more than one item.
  380. :return: A list of dictionaries containing content metadata.
  381. """
  382. if query:
  383. if not kinds:
  384. kinds = ["Video", "Audio", "Exercise", "Document", "Topic"]
  385. try:
  386. topic_node = Item.select(
  387. Item.title,
  388. Item.description,
  389. Item.available,
  390. Item.kind,
  391. Item.id,
  392. Item.path,
  393. Item.slug,
  394. ).where((fn.Lower(Item.title) == query) & (Item.kind.in_(kinds))).get()
  395. if exact:
  396. # If allowing an exact match, just return that one match and we're done!
  397. return [model_to_dict(topic_node)], True, None
  398. except DoesNotExist:
  399. topic_node = {}
  400. pass
  401. # For efficiency, don't do substring matches when we've got lots of results
  402. topic_nodes = Item.select(
  403. Item.title,
  404. Item.description,
  405. Item.available,
  406. Item.kind,
  407. Item.id,
  408. Item.path,
  409. Item.slug,
  410. ).where((Item.kind.in_(kinds)) & ((fn.Lower(Item.title).contains(query)) | (fn.Lower(Item.extra_fields).contains(query))))
  411. pages = topic_nodes.count() / items_per_page
  412. topic_nodes = [item for item in topic_nodes.paginate(page, items_per_page).dicts()]
  413. if topic_node:
  414. # If we got an exact match, show it first.
  415. topic_nodes.insert(0, model_to_dict(topic_node))
  416. return topic_nodes, False, pages
  417. @set_database
  418. def bulk_insert(items, **kwargs):
  419. """
  420. Insert many rows into the database at once.
  421. Limit to 500 items at a time for performance reasons.
  422. :param items: List of dictionaries containing content metadata.
  423. """
  424. if items:
  425. db = kwargs.get("db")
  426. items = map(parse_model_data, items)
  427. if db:
  428. with db.atomic():
  429. for idx in range(0, len(items), 500):
  430. Item.insert_many(items[idx:idx + 500]).execute()
  431. @set_database
  432. def create(item, **kwargs):
  433. """
  434. Wrapper around create that allows us to specify a database
  435. and also parse the model data to compress extra fields.
  436. :param item: A dictionary containing content metadata for one node.
  437. :return Item
  438. """
  439. if item:
  440. return Item.create(**parse_model_data(item))
  441. @set_database
  442. def get(item, **kwargs):
  443. """
  444. Fetch a content item, automatically choosing the correct content database (because of the set_database
  445. decorator).
  446. :param item: A dictionary containing content metadata for one node. "extra_fields" should not be inflated!
  447. :return: Item, or None if no such item is found
  448. """
  449. if item:
  450. selector = None
  451. for attr, value in item.iteritems():
  452. if not selector:
  453. selector = (getattr(Item, attr) == value)
  454. else:
  455. selector &= (getattr(Item, attr) == value)
  456. return Item.get(selector)
  457. @set_database
  458. def delete_instances(ids, **kwargs):
  459. """
  460. Given a list of Item ids, deletes all instances with that id.
  461. :param item: A list of `Item.id`s
  462. :return: None
  463. """
  464. if ids:
  465. for item in Item.select().where(Item.id.in_(ids)):
  466. item.delete_instance()
  467. @set_database
  468. def get_or_create(item, **kwargs):
  469. """
  470. Wrapper around get or create that allows us to specify a database
  471. and also parse the model data to compress extra fields.
  472. :param item: A dictionary containing content metadata for one node.
  473. :return tuple of Item and Boolean for whether created or not.
  474. """
  475. if item:
  476. return Item.create_or_get(**parse_model_data(item))
  477. @set_database
  478. def update_item(update=None, path=None, **kwargs):
  479. """
  480. Select an item by path, update fields and save.
  481. Updates all items that have the same id as well.
  482. Ids are not unique due to denormalization, yet items with the same id should have the same info.
  483. :param update: Dictionary of attributes to update on the model.
  484. :param path: Unique path for the content node to be updated. Also updates nodes with the same id.
  485. """
  486. if update and path:
  487. base_item = Item.get(Item.path == path)
  488. items = Item.select().where((Item.id == base_item.id) & (Item.kind == base_item.kind))
  489. for item in items:
  490. if any(key not in Item._meta.fields for key in update):
  491. item_data = unparse_model_data(item)
  492. item_data.update(update)
  493. for key, value in parse_model_data(item_data).iteritems():
  494. setattr(item, key, value)
  495. else:
  496. for key, value in update.iteritems():
  497. setattr(item, key, value)
  498. item.save()
  499. def iterator_content_items(ids=None, channel="khan", language="en", **kwargs):
  500. """
  501. Generator to iterate over content items specified by ids,
  502. run update content availability on that item and then yield the
  503. required update.
  504. :param update: Dictionary of attributes to update on the model.
  505. :yield: Tuple of unique path to item, and the update to be carried out on that item
  506. """
  507. if ids:
  508. items = Item.select().where(Item.id.in_(ids)).dicts().iterator()
  509. else:
  510. items = Item.select().dicts().iterator()
  511. mapped_items = itertools.imap(unparse_model_data, items)
  512. updated_mapped_items = update_content_availability(mapped_items, channel=channel, language=language)
  513. for path, update in updated_mapped_items:
  514. yield path, update
  515. def iterator_content_items_by_youtube_id(ids=None, channel="khan", language="en", **kwargs):
  516. """
  517. Generator to iterate over content items specified by youtube ids,
  518. run update content availability on that item and then yield the
  519. required update.
  520. :param update: Dictionary of attributes to update on the model.
  521. :yield: Tuple of unique path to item, and the update to be carried out on that item
  522. """
  523. if ids:
  524. items = Item.select().where(Item.youtube_id.in_(ids)).dicts().iterator()
  525. else:
  526. items = Item.select().dicts().iterator()
  527. mapped_items = itertools.imap(unparse_model_data, items)
  528. updated_mapped_items = update_content_availability(mapped_items, channel=channel, language=language)
  529. for path, update in updated_mapped_items:
  530. yield path, update
  531. @set_database
  532. def create_table(**kwargs):
  533. """
  534. Create a table in the database.
  535. """
  536. db = kwargs.get("db")
  537. if db:
  538. db.create_tables([Item, AssessmentItem])
  539. def annotate_content_models_by_youtube_id(channel="khan", language="en", youtube_ids=None):
  540. """
  541. Annotate content models that have the youtube ids specified in a list.
  542. :param channel: Channel to update.
  543. :param language: Language of channel to update.
  544. :param youtube_ids: List of youtube_ids to find content models for annotation.
  545. """
  546. annotate_content_models(channel=channel, language=language, ids=youtube_ids, iterator_content_items=iterator_content_items_by_youtube_id)
  547. @set_database
  548. def annotate_content_models(channel="khan", language="en", ids=None, iterator_content_items=iterator_content_items, **kwargs):
  549. """
  550. Annotate content models that have the ids specified in a list.
  551. Our ids can be duplicated at the moment, so this may be several content items per id.
  552. When a content item has been updated, propagate availability up the topic tree.
  553. :param channel: Channel to update.
  554. :param language: Language of channel to update.
  555. :param ids: List of content ids to find content models for annotation.
  556. :param iterator_content_items: Generator function to use to yield paths and updates.
  557. """
  558. db = kwargs.get("db")
  559. if db:
  560. content_models = iterator_content_items(ids=ids, channel=channel, language=language)
  561. with db.atomic() as transaction:
  562. def recurse_availability_up_tree(node, available):
  563. if not node.parent:
  564. return
  565. else:
  566. parent = node.parent
  567. Parent = Item.alias()
  568. children = Item.select().join(Parent, on=(Item.parent == Parent.pk)).where(Item.parent == parent.pk)
  569. if not available:
  570. children_available = children.where(Item.available == True).count() > 0
  571. available = children_available
  572. files_complete = children.aggregate(fn.SUM(Item.files_complete))
  573. child_remote = children.where(((Item.available == False) & (Item.kind != "Topic")) | (Item.kind == "Topic")).aggregate(fn.SUM(Item.remote_size))
  574. child_on_disk = children.aggregate(fn.SUM(Item.size_on_disk))
  575. if parent.available != available:
  576. parent.available = available
  577. if parent.files_complete != files_complete:
  578. parent.files_complete = files_complete
  579. # Ensure that the aggregate sizes are not None
  580. if parent.remote_size != child_remote and child_remote:
  581. parent.remote_size = child_remote
  582. # Ensure that the aggregate sizes are not None
  583. if parent.size_on_disk != child_on_disk and child_on_disk:
  584. parent.size_on_disk = child_on_disk
  585. if parent.is_dirty():
  586. parent.save()
  587. recurse_availability_up_tree(parent, available)
  588. for path, update in content_models:
  589. if update:
  590. # We have duplicates in the topic tree, make sure the stamping happens to all of them.
  591. item = Item.get(Item.path == path)
  592. if item.kind != "Topic":
  593. item_data = unparse_model_data(model_to_dict(item, recurse=False))
  594. item_data.update(update)
  595. item_data = parse_model_data(item_data)
  596. for attr, val in item_data.iteritems():
  597. setattr(item, attr, val)
  598. item.save()
  599. recurse_availability_up_tree(item, update.get("available", False))
  600. @set_database
  601. def update_parents(parent_mapping=None, **kwargs):
  602. """
  603. Convenience function to add parent nodes to other nodes in the database.
  604. Needs a mapping from item path to parent id.
  605. As only Topics can be parents, and we can have duplicate ids, we filter on both.
  606. :param update_mapping: A dictionary containing item paths as keys, with parent ids as values.
  607. """
  608. if parent_mapping:
  609. db = kwargs.get("db")
  610. if db:
  611. with db.atomic() as transaction:
  612. for key, value in parent_mapping.iteritems():
  613. if value:
  614. try:
  615. # Only Topics can be parent nodes
  616. parent = Item.get(Item.id == value, Item.kind == "Topic")
  617. item = Item.get(Item.path == key)
  618. except DoesNotExist:
  619. print(key, value, "Parent or Item not found")
  620. if item and parent:
  621. item.parent = parent
  622. item.save()
  623. @set_database
  624. def get_assessment_item_data(assessment_item_id=None, **kwargs):
  625. """
  626. Wrapper function to return assessment_item from database as a dictionary.
  627. :param assessment_item_id: id of the assessment item to return.
  628. :return: Dictionary containing assessment item data.
  629. """
  630. try:
  631. assessment_item = AssessmentItem.get(AssessmentItem.id == assessment_item_id)
  632. return model_to_dict(assessment_item)
  633. except OperationalError:
  634. return {}