openedx-learning 0.27.1__tar.gz → 0.28.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {openedx_learning-0.27.1/openedx_learning.egg-info → openedx_learning-0.28.0}/PKG-INFO +5 -5
  2. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/__init__.py +1 -1
  3. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/toml.py +5 -4
  4. openedx_learning-0.28.0/openedx_learning/apps/authoring/backup_restore/zipper.py +242 -0
  5. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/api.py +29 -8
  6. {openedx_learning-0.27.1 → openedx_learning-0.28.0/openedx_learning.egg-info}/PKG-INFO +5 -5
  7. openedx_learning-0.28.0/openedx_tagging/core/tagging/models/utils.py +82 -0
  8. openedx_learning-0.27.1/openedx_learning/apps/authoring/backup_restore/zipper.py +0 -53
  9. openedx_learning-0.27.1/openedx_tagging/core/tagging/models/utils.py +0 -54
  10. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/CHANGELOG.rst +0 -0
  11. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/LICENSE.txt +0 -0
  12. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/MANIFEST.in +0 -0
  13. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/README.rst +0 -0
  14. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/api/__init__.py +0 -0
  15. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/api/authoring.py +0 -0
  16. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/api/authoring_models.py +0 -0
  17. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/__init__.py +0 -0
  18. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/__init__.py +0 -0
  19. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/__init__.py +0 -0
  20. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/admin.py +0 -0
  21. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/api.py +0 -0
  22. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/apps.py +0 -0
  23. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/__init__.py +0 -0
  24. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py +0 -0
  25. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +0 -0
  26. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/migrations/__init__.py +0 -0
  27. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/models.py +0 -0
  28. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/__init__.py +0 -0
  29. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/admin.py +0 -0
  30. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/api.py +0 -0
  31. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/apps.py +0 -0
  32. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0001_initial.py +0 -0
  33. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +0 -0
  34. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0003_collection_entities.py +0 -0
  35. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0004_collection_key.py +0 -0
  36. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +0 -0
  37. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/__init__.py +0 -0
  38. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/models.py +0 -0
  39. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/__init__.py +0 -0
  40. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/admin.py +0 -0
  41. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/api.py +0 -0
  42. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/apps.py +0 -0
  43. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/__init__.py +0 -0
  44. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/commands/__init__.py +0 -0
  45. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py +0 -0
  46. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0001_initial.py +0 -0
  47. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
  48. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +0 -0
  49. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/__init__.py +0 -0
  50. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/models.py +0 -0
  51. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/__init__.py +0 -0
  52. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/admin.py +0 -0
  53. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/api.py +0 -0
  54. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/apps.py +0 -0
  55. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/migrations/0001_initial.py +0 -0
  56. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/migrations/__init__.py +0 -0
  57. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/models.py +0 -0
  58. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/__init__.py +0 -0
  59. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/admin.py +0 -0
  60. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/apps.py +0 -0
  61. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/contextmanagers.py +0 -0
  62. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0001_initial.py +0 -0
  63. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
  64. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0003_containers.py +0 -0
  65. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py +0 -0
  66. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0005_alter_entitylistrow_options.py +0 -0
  67. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0006_draftchangelog.py +0 -0
  68. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0007_bootstrap_draftchangelog.py +0 -0
  69. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +0 -0
  70. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/__init__.py +0 -0
  71. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/__init__.py +0 -0
  72. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/container.py +0 -0
  73. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/draft_log.py +0 -0
  74. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/entity_list.py +0 -0
  75. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/learning_package.py +0 -0
  76. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/publish_log.py +0 -0
  77. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/publishable_entity.py +0 -0
  78. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/__init__.py +0 -0
  79. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/admin.py +0 -0
  80. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/api.py +0 -0
  81. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/apps.py +0 -0
  82. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/migrations/0001_initial.py +0 -0
  83. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/migrations/__init__.py +0 -0
  84. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/models.py +0 -0
  85. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/__init__.py +0 -0
  86. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/admin.py +0 -0
  87. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/api.py +0 -0
  88. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/apps.py +0 -0
  89. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/migrations/0001_initial.py +0 -0
  90. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/migrations/__init__.py +0 -0
  91. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/models.py +0 -0
  92. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/__init__.py +0 -0
  93. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/admin.py +0 -0
  94. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/api.py +0 -0
  95. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/apps.py +0 -0
  96. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/migrations/0001_initial.py +0 -0
  97. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/migrations/__init__.py +0 -0
  98. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/models.py +0 -0
  99. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/contrib/__init__.py +0 -0
  100. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/__init__.py +0 -0
  101. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/apps.py +0 -0
  102. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/urls.py +0 -0
  103. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/views.py +0 -0
  104. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/__init__.py +0 -0
  105. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/admin_utils.py +0 -0
  106. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/cache.py +0 -0
  107. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/collations.py +0 -0
  108. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/fields.py +0 -0
  109. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/managers.py +0 -0
  110. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/test_utils.py +0 -0
  111. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/lib/validators.py +0 -0
  112. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning/py.typed +0 -0
  113. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning.egg-info/SOURCES.txt +0 -0
  114. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning.egg-info/dependency_links.txt +0 -0
  115. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning.egg-info/not-zip-safe +0 -0
  116. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning.egg-info/requires.txt +4 -4
  117. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_learning.egg-info/top_level.txt +0 -0
  118. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/__init__.py +0 -0
  119. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/__init__.py +0 -0
  120. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/__init__.py +0 -0
  121. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/admin.py +0 -0
  122. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/api.py +0 -0
  123. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/apps.py +0 -0
  124. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/data.py +0 -0
  125. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
  126. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
  127. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/api.py +0 -0
  128. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
  129. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
  130. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
  131. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
  132. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
  133. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/template.json +0 -0
  134. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
  135. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
  136. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
  137. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
  138. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
  139. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
  140. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
  141. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
  142. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
  143. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
  144. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
  145. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
  146. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
  147. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
  148. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
  149. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
  150. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +0 -0
  151. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py +0 -0
  152. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py +0 -0
  153. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py +0 -0
  154. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
  155. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/__init__.py +0 -0
  156. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/base.py +0 -0
  157. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/import_export.py +0 -0
  158. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
  159. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
  160. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
  161. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
  162. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
  163. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
  164. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
  165. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -0
  166. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
  167. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/views.py +0 -0
  168. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
  169. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rules.py +0 -0
  170. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/urls.py +0 -0
  171. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/openedx_tagging/py.typed +0 -0
  172. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/requirements/base.in +0 -0
  173. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/setup.cfg +0 -0
  174. {openedx_learning-0.27.1 → openedx_learning-0.28.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-learning
3
- Version: 0.27.1
3
+ Version: 0.28.0
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -19,13 +19,13 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Requires-Python: >=3.11
21
21
  License-File: LICENSE.txt
22
- Requires-Dist: djangorestframework<4.0
23
- Requires-Dist: tomlkit
24
22
  Requires-Dist: celery
23
+ Requires-Dist: rules<4.0
25
24
  Requires-Dist: Django
26
- Requires-Dist: attrs
27
25
  Requires-Dist: edx-drf-extensions
28
- Requires-Dist: rules<4.0
26
+ Requires-Dist: tomlkit
27
+ Requires-Dist: attrs
28
+ Requires-Dist: djangorestframework<4.0
29
29
  Dynamic: author
30
30
  Dynamic: author-email
31
31
  Dynamic: classifier
@@ -2,4 +2,4 @@
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
4
 
5
- __version__ = "0.27.1"
5
+ __version__ = "0.28.0"
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
 
7
7
  import tomlkit
8
8
 
9
+ from openedx_learning.apps.authoring.publishing import api as publishing_api
9
10
  from openedx_learning.apps.authoring.publishing.models import PublishableEntity, PublishableEntityVersion
10
11
  from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
11
12
 
@@ -27,8 +28,8 @@ def toml_learning_package(learning_package: LearningPackage) -> str:
27
28
  def toml_publishable_entity(entity: PublishableEntity) -> str:
28
29
  """Create a TOML representation of a publishable entity."""
29
30
 
30
- current_draft_version = getattr(entity, "draft", None)
31
- current_published_version = getattr(entity, "published", None)
31
+ current_draft_version = publishing_api.get_draft_version(entity)
32
+ current_published_version = publishing_api.get_published_version(entity)
32
33
 
33
34
  doc = tomlkit.document()
34
35
  entity_table = tomlkit.table()
@@ -37,12 +38,12 @@ def toml_publishable_entity(entity: PublishableEntity) -> str:
37
38
 
38
39
  if current_draft_version:
39
40
  draft_table = tomlkit.table()
40
- draft_table.add("version_num", current_draft_version.version.version_num)
41
+ draft_table.add("version_num", current_draft_version.version_num)
41
42
  entity_table.add("draft", draft_table)
42
43
 
43
44
  published_table = tomlkit.table()
44
45
  if current_published_version:
45
- published_table.add("version_num", current_published_version.version.version_num)
46
+ published_table.add("version_num", current_published_version.version_num)
46
47
  else:
47
48
  published_table.add(tomlkit.comment("unpublished: no published_version_num"))
48
49
  entity_table.add("published", published_table)
@@ -0,0 +1,242 @@
1
+ """
2
+ This module provides functionality to create a zip file containing the learning package data,
3
+ including a TOML representation of the learning package and its entities.
4
+ """
5
+ import hashlib
6
+ import zipfile
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+ from django.db.models import Prefetch, QuerySet
11
+ from django.utils.text import slugify
12
+
13
+ from openedx_learning.api.authoring_models import (
14
+ ComponentVersion,
15
+ ComponentVersionContent,
16
+ Content,
17
+ LearningPackage,
18
+ PublishableEntity,
19
+ PublishableEntityVersion,
20
+ )
21
+ from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
22
+ from openedx_learning.apps.authoring.publishing import api as publishing_api
23
+
24
+ TOML_PACKAGE_NAME = "package.toml"
25
+
26
+
27
+ def slugify_hashed_filename(identifier: str) -> str:
28
+ """
29
+ Generate a filesystem-safe filename from an identifier.
30
+
31
+ Why:
32
+ Identifiers may contain characters that are invalid or ambiguous
33
+ in filesystems (e.g., slashes, colons, case differences).
34
+ Additionally, two different identifiers might normalize to the same
35
+ slug after cleaning. To avoid collisions and ensure uniqueness,
36
+ we append a short blake2b hash.
37
+
38
+ What:
39
+ - Slugify the identifier (preserves most characters, only strips
40
+ filesystem-invalid ones).
41
+ - Append a short hash for uniqueness.
42
+ - Result: human-readable but still unique and filesystem-safe filename.
43
+ """
44
+ slug = slugify(identifier, allow_unicode=True)
45
+ # Short digest ensures uniqueness without overly long filenames
46
+ short_hash = hashlib.blake2b(
47
+ identifier.encode("utf-8"),
48
+ digest_size=3,
49
+ ).hexdigest()
50
+ return f"{slug}_{short_hash}"
51
+
52
+
53
+ class LearningPackageZipper:
54
+ """
55
+ A class to handle the zipping of learning content for backup and restore.
56
+ """
57
+
58
+ def __init__(self, learning_package: LearningPackage):
59
+ self.learning_package = learning_package
60
+ self.folders_already_created: set[Path] = set()
61
+
62
+ def create_folder(self, folder_path: Path, zip_file: zipfile.ZipFile) -> None:
63
+ """
64
+ Create a folder for the zip file structure.
65
+ Skips creating the folder if it already exists based on the folder path.
66
+ Args:
67
+ folder_path (Path): The path of the folder to create.
68
+ """
69
+ if folder_path not in self.folders_already_created:
70
+ zip_info = zipfile.ZipInfo(str(folder_path) + "/")
71
+ zip_file.writestr(zip_info, "") # Add explicit empty directory entry
72
+ self.folders_already_created.add(folder_path)
73
+
74
+ def get_publishable_entities(self) -> QuerySet[PublishableEntity]:
75
+ """
76
+ Retrieve the publishable entities associated with the learning package.
77
+ Prefetches related data for efficiency.
78
+ """
79
+ lp_id = self.learning_package.pk
80
+ publishable_entities: QuerySet[PublishableEntity] = publishing_api.get_publishable_entities(lp_id)
81
+ return (
82
+ publishable_entities
83
+ .select_related(
84
+ "container",
85
+ "component__component_type",
86
+ "draft__version__componentversion",
87
+ "published__version__componentversion",
88
+ )
89
+ .prefetch_related(
90
+ # We should re-evaluate the prefetching strategy here,
91
+ # as the current approach may cause performance issues—
92
+ # especially with large libraries (up to 100K items),
93
+ # which is too large for this type of prefetch.
94
+ Prefetch(
95
+ "draft__version__componentversion__componentversioncontent_set",
96
+ queryset=ComponentVersionContent.objects.select_related("content"),
97
+ to_attr="prefetched_contents",
98
+ ),
99
+ Prefetch(
100
+ "published__version__componentversion__componentversioncontent_set",
101
+ queryset=ComponentVersionContent.objects.select_related("content"),
102
+ to_attr="prefetched_contents",
103
+ ),
104
+ )
105
+ )
106
+
107
+ def get_versions_to_write(self, entity: PublishableEntity):
108
+ """
109
+ Get the versions of a publishable entity that should be written to the zip file.
110
+ It retrieves both draft and published versions.
111
+ """
112
+ draft_version: Optional[PublishableEntityVersion] = publishing_api.get_draft_version(entity)
113
+ published_version: Optional[PublishableEntityVersion] = publishing_api.get_published_version(entity)
114
+
115
+ versions_to_write = [draft_version] if draft_version else []
116
+
117
+ if published_version and published_version != draft_version:
118
+ versions_to_write.append(published_version)
119
+ return versions_to_write
120
+
121
+ def create_zip(self, path: str) -> None:
122
+ """
123
+ Creates a zip file containing the learning package data.
124
+ Args:
125
+ path (str): The path where the zip file will be created.
126
+ Raises:
127
+ Exception: If the learning package cannot be found or if the zip creation fails.
128
+ """
129
+
130
+ with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
131
+ # Add the package.toml file
132
+ package_toml_content: str = toml_learning_package(self.learning_package)
133
+ zipf.writestr(TOML_PACKAGE_NAME, package_toml_content)
134
+
135
+ # Add the entities directory
136
+ entities_folder = Path("entities")
137
+ self.create_folder(entities_folder, zipf)
138
+
139
+ # Add the collections directory
140
+ collections_folder = Path("collections")
141
+ self.create_folder(collections_folder, zipf)
142
+
143
+ # ------ ENTITIES SERIALIZATION -------------
144
+
145
+ # get the publishable entities
146
+ publishable_entities: QuerySet[PublishableEntity] = self.get_publishable_entities()
147
+
148
+ for entity in publishable_entities:
149
+ # entity: PublishableEntity = entity # Type hint for clarity
150
+
151
+ # Create a TOML representation of the entity
152
+ entity_toml_content: str = toml_publishable_entity(entity)
153
+
154
+ if hasattr(entity, 'container'):
155
+ entity_slugify_hash = slugify_hashed_filename(entity.key)
156
+ entity_toml_filename = f"{entity_slugify_hash}.toml"
157
+ entity_toml_path = entities_folder / entity_toml_filename
158
+ zipf.writestr(str(entity_toml_path), entity_toml_content)
159
+
160
+ if hasattr(entity, 'component'):
161
+ # Create the component folder structure for the entity. The structure is as follows:
162
+ # entities/
163
+ # xblock.v1/ (component namespace)
164
+ # html/ (component type)
165
+ # my_component.toml (entity TOML file)
166
+ # my_component/ (component id)
167
+ # component_versions/
168
+ # v1/
169
+ # static/
170
+
171
+ # Generate the slugified hash for the component local key
172
+ # Example: if the local key is "my_component", the slugified hash might be "my_component_123456"
173
+ # It's a combination of the local key and a hash and should be unique
174
+ entity_slugify_hash = slugify_hashed_filename(entity.component.local_key)
175
+
176
+ # Create the component namespace folder
177
+ # Example of component namespace is: "entities/xblock.v1/"
178
+ component_namespace_folder = entities_folder / entity.component.component_type.namespace
179
+ self.create_folder(component_namespace_folder, zipf)
180
+
181
+ # Create the component type folder
182
+ # Example of component type is: "entities/xblock.v1/html/"
183
+ component_type_folder = component_namespace_folder / entity.component.component_type.name
184
+ self.create_folder(component_type_folder, zipf)
185
+
186
+ # Create the component id folder
187
+ # Example of component id is: "entities/xblock.v1/html/my_component_123456/"
188
+ component_id_folder = component_type_folder / entity_slugify_hash
189
+ self.create_folder(component_id_folder, zipf)
190
+
191
+ # Add the entity TOML file inside the component type folder as well
192
+ # Example: "entities/xblock.v1/html/my_component_123456.toml"
193
+ component_entity_toml_path = component_type_folder / f"{entity_slugify_hash}.toml"
194
+ zipf.writestr(str(component_entity_toml_path), entity_toml_content)
195
+
196
+ # Add component version folder into the component id folder
197
+ # Example: "entities/xblock.v1/html/my_component_123456/component_versions/"
198
+ component_version_folder = component_id_folder / "component_versions"
199
+ self.create_folder(component_version_folder, zipf)
200
+
201
+ # ------ COMPONENT VERSIONING -------------
202
+ # Focusing on draft and published versions
203
+
204
+ # Get the draft and published versions
205
+ versions_to_write: List[PublishableEntityVersion] = self.get_versions_to_write(entity)
206
+
207
+ for version in versions_to_write:
208
+ # Create a folder for the version
209
+ version_number = f"v{version.version_num}"
210
+ version_folder = component_version_folder / version_number
211
+ self.create_folder(version_folder, zipf)
212
+
213
+ # Add static folder for the version
214
+ static_folder = version_folder / "static"
215
+ self.create_folder(static_folder, zipf)
216
+
217
+ # ------ COMPONENT STATIC CONTENT -------------
218
+ component_version: ComponentVersion = version.componentversion
219
+
220
+ # Get content data associated with this version
221
+ contents: QuerySet[
222
+ ComponentVersionContent
223
+ ] = component_version.prefetched_contents # type: ignore[attr-defined]
224
+
225
+ for component_version_content in contents:
226
+ content: Content = component_version_content.content
227
+
228
+ # Important: The component_version_content.key contains implicitly
229
+ # the file name and the file extension
230
+ file_path = version_folder / component_version_content.key
231
+
232
+ if content.has_file and content.path:
233
+ # If has_file, we pull it from the file system
234
+ with content.read_file() as f:
235
+ file_data = f.read()
236
+ elif not content.has_file and content.text:
237
+ # Otherwise, we use the text content as the file data
238
+ file_data = content.text
239
+ else:
240
+ # If no file and no text, we skip this content
241
+ continue
242
+ zipf.writestr(str(file_path), file_data)
@@ -10,7 +10,7 @@ from contextlib import nullcontext
10
10
  from dataclasses import dataclass
11
11
  from datetime import datetime, timezone
12
12
  from enum import Enum
13
- from typing import ContextManager, TypeVar
13
+ from typing import ContextManager, Optional, TypeVar
14
14
 
15
15
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
16
16
  from django.db.models import F, Q, QuerySet
@@ -58,9 +58,9 @@ __all__ = [
58
58
  "create_publishable_entity_version",
59
59
  "get_publishable_entity",
60
60
  "get_publishable_entity_by_key",
61
+ "get_publishable_entities",
61
62
  "get_last_publish",
62
63
  "get_all_drafts",
63
- "get_entities",
64
64
  "get_entities_with_unpublished_changes",
65
65
  "get_entities_with_unpublished_deletes",
66
66
  "publish_all_drafts",
@@ -262,11 +262,18 @@ def get_all_drafts(learning_package_id: int, /) -> QuerySet[Draft]:
262
262
  )
263
263
 
264
264
 
265
- def get_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
265
+ def get_publishable_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
266
266
  """
267
267
  Get all entities in a learning package.
268
268
  """
269
- return PublishableEntity.objects.filter(learning_package_id=learning_package_id)
269
+ return (
270
+ PublishableEntity.objects
271
+ .filter(learning_package_id=learning_package_id)
272
+ .select_related(
273
+ "draft__version",
274
+ "published__version",
275
+ )
276
+ )
270
277
 
271
278
 
272
279
  def get_entities_with_unpublished_changes(
@@ -425,15 +432,22 @@ def publish_from_drafts(
425
432
  return publish_log
426
433
 
427
434
 
428
- def get_draft_version(publishable_entity_id: int, /) -> PublishableEntityVersion | None:
435
+ def get_draft_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
429
436
  """
430
437
  Return current draft PublishableEntityVersion for this PublishableEntity.
431
438
 
432
439
  This function will return None if there is no current draft.
433
440
  """
441
+ if isinstance(publishable_entity_or_id, PublishableEntity):
442
+ # Fetches the draft version for a given PublishableEntity.
443
+ # Gracefully handles cases where no draft is present.
444
+ draft: Optional[Draft] = getattr(publishable_entity_or_id, "draft", None)
445
+ if draft is None:
446
+ return None
447
+ return draft.version
434
448
  try:
435
449
  draft = Draft.objects.select_related("version").get(
436
- entity_id=publishable_entity_id
450
+ entity_id=publishable_entity_or_id
437
451
  )
438
452
  except Draft.DoesNotExist:
439
453
  # No draft was ever created.
@@ -445,15 +459,22 @@ def get_draft_version(publishable_entity_id: int, /) -> PublishableEntityVersion
445
459
  return draft.version
446
460
 
447
461
 
448
- def get_published_version(publishable_entity_id: int, /) -> PublishableEntityVersion | None:
462
+ def get_published_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
449
463
  """
450
464
  Return current published PublishableEntityVersion for this PublishableEntity.
451
465
 
452
466
  This function will return None if there is no current published version.
453
467
  """
468
+ if isinstance(publishable_entity_or_id, PublishableEntity):
469
+ # Fetches the published version for a given PublishableEntity.
470
+ # Gracefully handles cases where no published version is present.
471
+ published: Optional[Published] = getattr(publishable_entity_or_id, "published", None)
472
+ if published is None:
473
+ return None
474
+ return published.version
454
475
  try:
455
476
  published = Published.objects.select_related("version").get(
456
- entity_id=publishable_entity_id
477
+ entity_id=publishable_entity_or_id
457
478
  )
458
479
  except Published.DoesNotExist:
459
480
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-learning
3
- Version: 0.27.1
3
+ Version: 0.28.0
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -19,13 +19,13 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Requires-Python: >=3.11
21
21
  License-File: LICENSE.txt
22
- Requires-Dist: djangorestframework<4.0
23
- Requires-Dist: tomlkit
24
22
  Requires-Dist: celery
23
+ Requires-Dist: rules<4.0
25
24
  Requires-Dist: Django
26
- Requires-Dist: attrs
27
25
  Requires-Dist: edx-drf-extensions
28
- Requires-Dist: rules<4.0
26
+ Requires-Dist: tomlkit
27
+ Requires-Dist: attrs
28
+ Requires-Dist: djangorestframework<4.0
29
29
  Dynamic: author
30
30
  Dynamic: author-email
31
31
  Dynamic: classifier
@@ -0,0 +1,82 @@
1
+ """
2
+ Utilities for tagging and taxonomy models
3
+ """
4
+ from django.db import connection as db_connection
5
+ from django.db.models import Aggregate, CharField, TextField
6
+ from django.db.models.expressions import Combinable, Func
7
+
8
+ RESERVED_TAG_CHARS = [
9
+ '\t', # Used in the database to separate tag levels in the "lineage" field
10
+ # e.g. lineage="Earth\tNorth America\tMexico\tMexico City"
11
+ ' > ', # Used in the search index and Instantsearch frontend to separate tag levels
12
+ # e.g. tags_level3="Earth > North America > Mexico > Mexico City"
13
+ ';', # Used in CSV exports to separate multiple tags from the same taxonomy
14
+ # e.g. languages-v1: en;es;fr
15
+ ]
16
+ TAGS_CSV_SEPARATOR = RESERVED_TAG_CHARS[2]
17
+
18
+
19
+ class ConcatNull(Func): # pylint: disable=abstract-method
20
+ """
21
+ Concatenate two arguments together. Like normal SQL but unlike Django's
22
+ "Concat", if either argument is NULL, the result will be NULL.
23
+ """
24
+
25
+ function = "CONCAT"
26
+
27
+ def as_sqlite(self, compiler, connection, **extra_context):
28
+ """ SQLite doesn't have CONCAT() but has a concatenation operator """
29
+ return super().as_sql(
30
+ compiler,
31
+ connection,
32
+ template="%(expressions)s",
33
+ arg_joiner=" || ",
34
+ **extra_context,
35
+ )
36
+
37
+
38
+ class StringAgg(Aggregate, Combinable):
39
+ """
40
+ Aggregate function that collects the values of some column across all rows,
41
+ and creates a string by concatenating those values, with a specified separator.
42
+
43
+ This version supports PostgreSQL (STRING_AGG), MySQL (GROUP_CONCAT), and SQLite.
44
+ """
45
+ # Default function is for MySQL (GROUP_CONCAT)
46
+ function = 'GROUP_CONCAT'
47
+ template = '%(function)s(%(distinct)s%(expressions)s)'
48
+
49
+ def __init__(self, expression, distinct=False, delimiter=',', **extra):
50
+ self.delimiter = delimiter
51
+ # Handle the distinct option and output type
52
+ distinct_str = 'DISTINCT ' if distinct else ''
53
+
54
+ extra.update({
55
+ 'distinct': distinct_str,
56
+ 'output_field': CharField(),
57
+ })
58
+
59
+ # Check the database backend (PostgreSQL, MySQL, or SQLite)
60
+ if 'postgresql' in db_connection.vendor.lower():
61
+ self.function = 'STRING_AGG'
62
+ self.template = '%(function)s(%(distinct)s%(expressions)s, %(delimiter)s)'
63
+ extra.update({
64
+ "delimiter": self.delimiter,
65
+ "output_field": TextField(),
66
+ })
67
+
68
+ # Initialize the parent class with the necessary parameters
69
+ super().__init__(
70
+ expression,
71
+ **extra,
72
+ )
73
+
74
+ # Implementing abstract methods from Combinable
75
+ def __rand__(self, other):
76
+ return self._combine(other, 'AND', False)
77
+
78
+ def __ror__(self, other):
79
+ return self._combine(other, 'OR', False)
80
+
81
+ def __rxor__(self, other):
82
+ return self._combine(other, 'XOR', False)
@@ -1,53 +0,0 @@
1
- """
2
- This module provides functionality to create a zip file containing the learning package data,
3
- including a TOML representation of the learning package and its entities.
4
- """
5
- import zipfile
6
- from pathlib import Path
7
-
8
- from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
9
- from openedx_learning.apps.authoring.publishing import api as publishing_api
10
- from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
11
-
12
- TOML_PACKAGE_NAME = "package.toml"
13
-
14
-
15
- class LearningPackageZipper:
16
- """
17
- A class to handle the zipping of learning content for backup and restore.
18
- """
19
-
20
- def __init__(self, learning_package: LearningPackage):
21
- self.learning_package = learning_package
22
-
23
- def create_zip(self, path: str) -> None:
24
- """
25
- Creates a zip file containing the learning package data.
26
- Args:
27
- path (str): The path where the zip file will be created.
28
- Raises:
29
- Exception: If the learning package cannot be found or if the zip creation fails.
30
- """
31
- package_toml_content: str = toml_learning_package(self.learning_package)
32
-
33
- with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
34
- # Add the package.toml string
35
- zipf.writestr(TOML_PACKAGE_NAME, package_toml_content)
36
-
37
- # Add the entities directory
38
- entities_folder = Path("entities")
39
- zip_info = zipfile.ZipInfo(str(entities_folder) + "/") # Ensure trailing slash
40
- zipf.writestr(zip_info, "") # Add explicit empty directory entry
41
-
42
- # Add the collections directory
43
- collections_folder = Path("collections")
44
- collections_info = zipfile.ZipInfo(str(collections_folder) + "/") # Ensure trailing slash
45
- zipf.writestr(collections_info, "") # Add explicit empty directory
46
-
47
- # Add each entity's TOML file
48
- for entity in publishing_api.get_entities(self.learning_package.pk):
49
- # Create a TOML representation of the entity
50
- entity_toml_content: str = toml_publishable_entity(entity)
51
- entity_toml_filename = f"{entity.key}.toml"
52
- entity_toml_path = entities_folder / entity_toml_filename
53
- zipf.writestr(str(entity_toml_path), entity_toml_content)
@@ -1,54 +0,0 @@
1
- """
2
- Utilities for tagging and taxonomy models
3
- """
4
- from django.db.models import Aggregate, CharField
5
- from django.db.models.expressions import Func
6
-
7
- RESERVED_TAG_CHARS = [
8
- '\t', # Used in the database to separate tag levels in the "lineage" field
9
- # e.g. lineage="Earth\tNorth America\tMexico\tMexico City"
10
- ' > ', # Used in the search index and Instantsearch frontend to separate tag levels
11
- # e.g. tags_level3="Earth > North America > Mexico > Mexico City"
12
- ';', # Used in CSV exports to separate multiple tags from the same taxonomy
13
- # e.g. languages-v1: en;es;fr
14
- ]
15
- TAGS_CSV_SEPARATOR = RESERVED_TAG_CHARS[2]
16
-
17
-
18
- class ConcatNull(Func): # pylint: disable=abstract-method
19
- """
20
- Concatenate two arguments together. Like normal SQL but unlike Django's
21
- "Concat", if either argument is NULL, the result will be NULL.
22
- """
23
-
24
- function = "CONCAT"
25
-
26
- def as_sqlite(self, compiler, connection, **extra_context):
27
- """ SQLite doesn't have CONCAT() but has a concatenation operator """
28
- return super().as_sql(
29
- compiler,
30
- connection,
31
- template="%(expressions)s",
32
- arg_joiner=" || ",
33
- **extra_context,
34
- )
35
-
36
-
37
- class StringAgg(Aggregate): # pylint: disable=abstract-method
38
- """
39
- Aggregate function that collects the values of some column across all rows,
40
- and creates a string by concatenating those values, with "," as a separator.
41
-
42
- This is the same as Django's django.contrib.postgres.aggregates.StringAgg,
43
- but this version works with MySQL and SQLite.
44
- """
45
- function = 'GROUP_CONCAT'
46
- template = '%(function)s(%(distinct)s%(expressions)s)'
47
-
48
- def __init__(self, expression, distinct=False, **extra):
49
- super().__init__(
50
- expression,
51
- distinct='DISTINCT ' if distinct else '',
52
- output_field=CharField(),
53
- **extra,
54
- )