openedx-learning 0.6.3__tar.gz → 0.9.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 (115) hide show
  1. openedx-learning-0.9.0/PKG-INFO +226 -0
  2. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/__init__.py +1 -1
  3. openedx-learning-0.9.0/openedx_learning.egg-info/PKG-INFO +226 -0
  4. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning.egg-info/SOURCES.txt +3 -0
  5. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/admin.py +1 -1
  6. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/api.py +106 -35
  7. openedx-learning-0.9.0/openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py +62 -0
  8. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/models/base.py +40 -23
  9. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/models/utils.py +10 -0
  10. openedx-learning-0.9.0/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
  11. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +3 -2
  12. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/v1/views.py +5 -0
  13. openedx-learning-0.9.0/openedx_tagging/py.typed +0 -0
  14. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/setup.py +2 -0
  15. openedx-learning-0.6.3/PKG-INFO +0 -218
  16. openedx-learning-0.6.3/openedx_learning.egg-info/PKG-INFO +0 -218
  17. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/CHANGELOG.rst +0 -0
  18. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/LICENSE.txt +0 -0
  19. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/MANIFEST.in +0 -0
  20. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/README.rst +0 -0
  21. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/contrib/__init__.py +0 -0
  22. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/contrib/media_server/__init__.py +0 -0
  23. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/contrib/media_server/apps.py +0 -0
  24. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/contrib/media_server/urls.py +0 -0
  25. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/contrib/media_server/views.py +0 -0
  26. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/__init__.py +0 -0
  27. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/__init__.py +0 -0
  28. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/admin.py +0 -0
  29. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/api.py +0 -0
  30. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/apps.py +0 -0
  31. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/migrations/0001_initial.py +0 -0
  32. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
  33. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/migrations/__init__.py +0 -0
  34. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/components/models.py +0 -0
  35. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/__init__.py +0 -0
  36. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/admin.py +0 -0
  37. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/api.py +0 -0
  38. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/apps.py +0 -0
  39. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/migrations/0001_initial.py +0 -0
  40. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/migrations/__init__.py +0 -0
  41. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/contents/models.py +0 -0
  42. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/__init__.py +0 -0
  43. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/admin.py +0 -0
  44. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/api.py +0 -0
  45. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/apps.py +0 -0
  46. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/migrations/0001_initial.py +0 -0
  47. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
  48. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/migrations/__init__.py +0 -0
  49. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/model_mixins.py +0 -0
  50. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/core/publishing/models.py +0 -0
  51. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/__init__.py +0 -0
  52. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/admin_utils.py +0 -0
  53. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/cache.py +0 -0
  54. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/collations.py +0 -0
  55. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/fields.py +0 -0
  56. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/managers.py +0 -0
  57. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/test_utils.py +0 -0
  58. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/lib/validators.py +0 -0
  59. openedx-learning-0.6.3/openedx_learning/rest_api/__init__.py → openedx-learning-0.9.0/openedx_learning/py.typed +0 -0
  60. {openedx-learning-0.6.3/openedx_learning/rest_api/v1 → openedx-learning-0.9.0/openedx_learning/rest_api}/__init__.py +0 -0
  61. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/rest_api/apps.py +0 -0
  62. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/rest_api/urls.py +0 -0
  63. {openedx-learning-0.6.3/openedx_tagging/core → openedx-learning-0.9.0/openedx_learning/rest_api/v1}/__init__.py +0 -0
  64. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/rest_api/v1/components.py +0 -0
  65. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning/rest_api/v1/urls.py +0 -0
  66. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning.egg-info/dependency_links.txt +0 -0
  67. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning.egg-info/not-zip-safe +0 -0
  68. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning.egg-info/requires.txt +3 -3
  69. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_learning.egg-info/top_level.txt +0 -0
  70. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/__init__.py +0 -0
  71. {openedx-learning-0.6.3/openedx_tagging/core/tagging → openedx-learning-0.9.0/openedx_tagging/core}/__init__.py +0 -0
  72. {openedx-learning-0.6.3/openedx_tagging/core/tagging/migrations → openedx-learning-0.9.0/openedx_tagging/core/tagging}/__init__.py +0 -0
  73. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/apps.py +0 -0
  74. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/data.py +0 -0
  75. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
  76. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
  77. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/api.py +0 -0
  78. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
  79. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
  80. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
  81. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
  82. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
  83. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/import_export/template.json +0 -0
  84. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
  85. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
  86. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
  87. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
  88. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
  89. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
  90. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
  91. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
  92. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
  93. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
  94. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
  95. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
  96. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
  97. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
  98. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
  99. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
  100. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +0 -0
  101. {openedx-learning-0.6.3/openedx_tagging/core/tagging/rest_api → openedx-learning-0.9.0/openedx_tagging/core/tagging/migrations}/__init__.py +0 -0
  102. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/models/__init__.py +0 -0
  103. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/models/import_export.py +0 -0
  104. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
  105. {openedx-learning-0.6.3/openedx_tagging/core/tagging/rest_api/v1 → openedx-learning-0.9.0/openedx_tagging/core/tagging/rest_api}/__init__.py +0 -0
  106. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
  107. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
  108. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
  109. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
  110. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
  111. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
  112. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/rules.py +0 -0
  113. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/openedx_tagging/core/tagging/urls.py +0 -0
  114. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/requirements/base.in +0 -0
  115. {openedx-learning-0.6.3 → openedx-learning-0.9.0}/setup.cfg +0 -0
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.1
2
+ Name: openedx-learning
3
+ Version: 0.9.0
4
+ Summary: An experiment.
5
+ Home-page: https://github.com/openedx/openedx-learning
6
+ Author: David Ormsbee
7
+ Author-email: dave@tcril.org
8
+ License: AGPL 3.0
9
+ Keywords: Python edx
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 3.2
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ License-File: LICENSE.txt
22
+ Requires-Dist: attrs
23
+ Requires-Dist: Django<5.0
24
+ Requires-Dist: edx-drf-extensions
25
+ Requires-Dist: djangorestframework<4.0
26
+ Requires-Dist: celery
27
+ Requires-Dist: rules<4.0
28
+
29
+ openedx-learning
30
+ =============================
31
+
32
+ |pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge|
33
+ |license-badge|
34
+
35
+ This is experimentation/prototyping and not in any way production ready!
36
+ ------------------------------------------------------------------------
37
+
38
+ Overview
39
+ --------
40
+
41
+ The Open edX Learning repository holds Django apps that represent core learning concepts and data models that have been extracted from edx-platform.
42
+
43
+ Motivation
44
+ ----------
45
+
46
+ The short term goal of this project is to create a small, extensible core that is easier to reason about and write extensions for than edx-platform. The longer term goal is to create a more nimble core learning platform, enabling rapid experimentation and drastic changes to the learner experience that are difficult to implement with Open edX today.
47
+
48
+ Replacing edx-platform is explicitly *not* a goal of this project, as only a small fraction of the concepts in edx-platform make sense to carry over here. When these core concepts are extracted and the data migrated, edx-platform will import apps from this repo and make use of their public in-process APIs.
49
+
50
+ Architecture
51
+ ------------
52
+
53
+ Parts
54
+ ~~~~~
55
+
56
+ * ``openedx_learning.lib`` is for shared utilities, and may include things like custom field types, plugin registration code, etc.
57
+ * ``openedx_learning.core`` contains our Core Django apps, where foundational data structures and APIs will live.
58
+ * ``openedx_tagging.core`` contains the core Tagging app, which provides data structures and apis for tagging Open edX objects.
59
+
60
+ App Dependencies
61
+ ~~~~~~~~~~~~~~~~
62
+
63
+ Anything can import from ``openedx_learning.lib``.
64
+
65
+ Core apps can import from each other, but cannot import from other apps outside of core. For those apps:
66
+
67
+ * ``learning_publishing`` has no dependencies. All the other apps depend on it.
68
+ * ``learning_composition`` and ``learning_navigation`` both depend on ``learning_partitioning``
69
+
70
+ Model Conventions
71
+ ~~~~~~~~~~~~~~~~~
72
+
73
+ We have a few different identifier types in the schema, and we try to avoid ``_id`` for this because Django uses that convention to reference IDs in other models/tables. So instead we have:
74
+
75
+ * ``id`` is the auto-generated, internal row ID and primary key. This never changes. Data models should make foreign keys to this field, as per Django convention.
76
+ * ``uuid`` is a randomly generated UUID4. This is the stable way to refer to a row/resource from an external service. This never changes. This is separate from ``id`` mostly because there are performance penalties when using UUIDs as primary keys with MySQL.
77
+ * ``key`` is intended to be a case-sensitive, alphanumeric key, which holds some meaning to library clients. This is usually stable, but can be changed, depending on the business logic of the client. The apps in this repo should make no assumptions about it being stable. It can be used as a suffix.
78
+ * ``num`` is like ``key``, but for use when it's strictly numeric. It can also be used as a suffix.
79
+
80
+
81
+ See Also
82
+ ~~~~~~~~
83
+
84
+ The structure of this repo follows [OEP-0049](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0049-django-app-patterns.html) where possible, and also borrows inspiration from:
85
+
86
+ * [Scaling Django to 500 apps](https://2021.djangocon.us/talks/scaling-django-to-500-apps/) (Dan Palmer, DjangoCon US 2021)
87
+ * [Django structure for scale and longevity](https://www.youtube.com/watch?v=yG3ZdxBb1oo) (Radoslav Georgiev, EuroPython 2018)
88
+
89
+ Code Overview
90
+ -------------
91
+
92
+ The ``openedx_learning.apps`` package contains all our Django applications. All apps are named with a ``learning_`` prefix to better avoid name conflicts, because Django's app namespace is flat. Apps will adhere to `OEP-0049: Django App Patterns <https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0049-django-app-patterns.html>`_.
93
+
94
+ Development Workflow
95
+ --------------------
96
+
97
+ One Time Setup
98
+ ~~~~~~~~~~~~~~
99
+ .. code-block::
100
+
101
+ # Clone the repository
102
+ git clone git@github.com:ormsbee/openedx-learning.git
103
+ cd openedx-learning
104
+
105
+ # Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it
106
+ mkvirtualenv -p python3.8 openedx-learning
107
+
108
+
109
+ Every time you develop something in this repo
110
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
111
+ .. code-block::
112
+
113
+ # Activate the virtualenv
114
+ workon openedx-learning
115
+
116
+ # Grab the latest code
117
+ git checkout master
118
+ git pull
119
+
120
+ # Install/update the dev requirements
121
+ make requirements
122
+
123
+ # Run the tests and quality checks (to verify the status before you make any changes)
124
+ make validate
125
+
126
+ # Make a new branch for your changes
127
+ git checkout -b <your_github_username>/<short_description>
128
+
129
+ # Using your favorite editor, edit the code to make your change.
130
+ vim …
131
+
132
+ # Run your new tests
133
+ pytest ./path/to/new/tests
134
+
135
+ # Run all the tests and quality checks
136
+ make validate
137
+
138
+ # Commit all your changes
139
+ git commit …
140
+ git push
141
+
142
+ # Open a PR and ask for review.
143
+
144
+ License
145
+ -------
146
+
147
+ The code in this repository is licensed under the AGPL 3.0 unless otherwise noted.
148
+
149
+ Please see `LICENSE.txt <LICENSE.txt>`_ for details.
150
+
151
+ How To Contribute
152
+ -----------------
153
+
154
+ This repo is in a very experimental state. Discussion using GitHub Issues is welcome, but you probably don't want to make contributions as everything can shift around drastically with little notice.
155
+
156
+ Reporting Security Issues
157
+ -------------------------
158
+
159
+ Please do not report security issues in public. Please email security@openedx.org.
160
+
161
+ Help
162
+ ----
163
+
164
+ If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community.
165
+
166
+ Our real-time conversations are on Slack. You can request a `Slack invitation`_, then join our `community Slack workspace`_.
167
+
168
+ For more information about these options, see the `Getting Help`_ page.
169
+
170
+ .. _Slack invitation: https://openedx.org/slack
171
+ .. _community Slack workspace: https://openedx.slack.com/
172
+ .. _Getting Help: https://openedx.org/getting-help
173
+
174
+ .. |pypi-badge| image:: https://img.shields.io/pypi/v/openedx-learning.svg
175
+ :target: https://pypi.python.org/pypi/openedx-learning/
176
+ :alt: PyPI
177
+
178
+ .. |ci-badge| image:: https://github.com/openedx/openedx-learning/workflows/Python%20CI/badge.svg?branch=master
179
+ :target: https://github.com/openedx/openedx-learning/actions
180
+ :alt: CI
181
+
182
+ .. |codecov-badge| image:: https://codecov.io/github/edx/openedx-learning/coverage.svg?branch=master
183
+ :target: https://codecov.io/github/edx/openedx-learning?branch=master
184
+ :alt: Codecov
185
+
186
+ .. |doc-badge| image:: https://readthedocs.org/projects/openedx-learning/badge/?version=latest
187
+ :target: https://openedx-learning.readthedocs.io/en/latest/
188
+ :alt: Documentation
189
+
190
+ .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/openedx-learning.svg
191
+ :target: https://pypi.python.org/pypi/openedx-learning/
192
+ :alt: Supported Python versions
193
+
194
+ .. |license-badge| image:: https://img.shields.io/github/license/edx/openedx-learning.svg
195
+ :target: https://github.com/openedx/openedx-learning/blob/master/LICENSE.txt
196
+ :alt: License
197
+
198
+
199
+ Change Log
200
+ ----------
201
+
202
+ ..
203
+ All enhancements and patches to openedx-learning will be documented
204
+ in this file. It adheres to the structure of https://keepachangelog.com/ ,
205
+ but in reStructuredText instead of Markdown (for ease of incorporation into
206
+ Sphinx documentation and the PyPI description).
207
+
208
+ This project adheres to Semantic Versioning (https://semver.org/).
209
+
210
+ .. There should always be an "Unreleased" section for changes pending release.
211
+
212
+ Unreleased
213
+ ~~~~~~~~~~
214
+
215
+ * Removed usage of ``tox-battery`` and added support for ``tox 4.0``
216
+ * Switch from ``edx-sphinx-theme`` to ``sphinx-book-theme`` since the former is
217
+ deprecated. See https://github.com/openedx/edx-sphinx-theme/issues/184 for
218
+ more details.
219
+
220
+ [0.1.0] - 2021-08-08
221
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
+
223
+ Added
224
+ _____
225
+
226
+ * First release on PyPI.
@@ -1,4 +1,4 @@
1
1
  """
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
- __version__ = "0.6.3"
4
+ __version__ = "0.9.0"
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.1
2
+ Name: openedx-learning
3
+ Version: 0.9.0
4
+ Summary: An experiment.
5
+ Home-page: https://github.com/openedx/openedx-learning
6
+ Author: David Ormsbee
7
+ Author-email: dave@tcril.org
8
+ License: AGPL 3.0
9
+ Keywords: Python edx
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 3.2
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ License-File: LICENSE.txt
22
+ Requires-Dist: attrs
23
+ Requires-Dist: Django<5.0
24
+ Requires-Dist: edx-drf-extensions
25
+ Requires-Dist: djangorestframework<4.0
26
+ Requires-Dist: celery
27
+ Requires-Dist: rules<4.0
28
+
29
+ openedx-learning
30
+ =============================
31
+
32
+ |pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge|
33
+ |license-badge|
34
+
35
+ This is experimentation/prototyping and not in any way production ready!
36
+ ------------------------------------------------------------------------
37
+
38
+ Overview
39
+ --------
40
+
41
+ The Open edX Learning repository holds Django apps that represent core learning concepts and data models that have been extracted from edx-platform.
42
+
43
+ Motivation
44
+ ----------
45
+
46
+ The short term goal of this project is to create a small, extensible core that is easier to reason about and write extensions for than edx-platform. The longer term goal is to create a more nimble core learning platform, enabling rapid experimentation and drastic changes to the learner experience that are difficult to implement with Open edX today.
47
+
48
+ Replacing edx-platform is explicitly *not* a goal of this project, as only a small fraction of the concepts in edx-platform make sense to carry over here. When these core concepts are extracted and the data migrated, edx-platform will import apps from this repo and make use of their public in-process APIs.
49
+
50
+ Architecture
51
+ ------------
52
+
53
+ Parts
54
+ ~~~~~
55
+
56
+ * ``openedx_learning.lib`` is for shared utilities, and may include things like custom field types, plugin registration code, etc.
57
+ * ``openedx_learning.core`` contains our Core Django apps, where foundational data structures and APIs will live.
58
+ * ``openedx_tagging.core`` contains the core Tagging app, which provides data structures and apis for tagging Open edX objects.
59
+
60
+ App Dependencies
61
+ ~~~~~~~~~~~~~~~~
62
+
63
+ Anything can import from ``openedx_learning.lib``.
64
+
65
+ Core apps can import from each other, but cannot import from other apps outside of core. For those apps:
66
+
67
+ * ``learning_publishing`` has no dependencies. All the other apps depend on it.
68
+ * ``learning_composition`` and ``learning_navigation`` both depend on ``learning_partitioning``
69
+
70
+ Model Conventions
71
+ ~~~~~~~~~~~~~~~~~
72
+
73
+ We have a few different identifier types in the schema, and we try to avoid ``_id`` for this because Django uses that convention to reference IDs in other models/tables. So instead we have:
74
+
75
+ * ``id`` is the auto-generated, internal row ID and primary key. This never changes. Data models should make foreign keys to this field, as per Django convention.
76
+ * ``uuid`` is a randomly generated UUID4. This is the stable way to refer to a row/resource from an external service. This never changes. This is separate from ``id`` mostly because there are performance penalties when using UUIDs as primary keys with MySQL.
77
+ * ``key`` is intended to be a case-sensitive, alphanumeric key, which holds some meaning to library clients. This is usually stable, but can be changed, depending on the business logic of the client. The apps in this repo should make no assumptions about it being stable. It can be used as a suffix.
78
+ * ``num`` is like ``key``, but for use when it's strictly numeric. It can also be used as a suffix.
79
+
80
+
81
+ See Also
82
+ ~~~~~~~~
83
+
84
+ The structure of this repo follows [OEP-0049](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0049-django-app-patterns.html) where possible, and also borrows inspiration from:
85
+
86
+ * [Scaling Django to 500 apps](https://2021.djangocon.us/talks/scaling-django-to-500-apps/) (Dan Palmer, DjangoCon US 2021)
87
+ * [Django structure for scale and longevity](https://www.youtube.com/watch?v=yG3ZdxBb1oo) (Radoslav Georgiev, EuroPython 2018)
88
+
89
+ Code Overview
90
+ -------------
91
+
92
+ The ``openedx_learning.apps`` package contains all our Django applications. All apps are named with a ``learning_`` prefix to better avoid name conflicts, because Django's app namespace is flat. Apps will adhere to `OEP-0049: Django App Patterns <https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0049-django-app-patterns.html>`_.
93
+
94
+ Development Workflow
95
+ --------------------
96
+
97
+ One Time Setup
98
+ ~~~~~~~~~~~~~~
99
+ .. code-block::
100
+
101
+ # Clone the repository
102
+ git clone git@github.com:ormsbee/openedx-learning.git
103
+ cd openedx-learning
104
+
105
+ # Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it
106
+ mkvirtualenv -p python3.8 openedx-learning
107
+
108
+
109
+ Every time you develop something in this repo
110
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
111
+ .. code-block::
112
+
113
+ # Activate the virtualenv
114
+ workon openedx-learning
115
+
116
+ # Grab the latest code
117
+ git checkout master
118
+ git pull
119
+
120
+ # Install/update the dev requirements
121
+ make requirements
122
+
123
+ # Run the tests and quality checks (to verify the status before you make any changes)
124
+ make validate
125
+
126
+ # Make a new branch for your changes
127
+ git checkout -b <your_github_username>/<short_description>
128
+
129
+ # Using your favorite editor, edit the code to make your change.
130
+ vim …
131
+
132
+ # Run your new tests
133
+ pytest ./path/to/new/tests
134
+
135
+ # Run all the tests and quality checks
136
+ make validate
137
+
138
+ # Commit all your changes
139
+ git commit …
140
+ git push
141
+
142
+ # Open a PR and ask for review.
143
+
144
+ License
145
+ -------
146
+
147
+ The code in this repository is licensed under the AGPL 3.0 unless otherwise noted.
148
+
149
+ Please see `LICENSE.txt <LICENSE.txt>`_ for details.
150
+
151
+ How To Contribute
152
+ -----------------
153
+
154
+ This repo is in a very experimental state. Discussion using GitHub Issues is welcome, but you probably don't want to make contributions as everything can shift around drastically with little notice.
155
+
156
+ Reporting Security Issues
157
+ -------------------------
158
+
159
+ Please do not report security issues in public. Please email security@openedx.org.
160
+
161
+ Help
162
+ ----
163
+
164
+ If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community.
165
+
166
+ Our real-time conversations are on Slack. You can request a `Slack invitation`_, then join our `community Slack workspace`_.
167
+
168
+ For more information about these options, see the `Getting Help`_ page.
169
+
170
+ .. _Slack invitation: https://openedx.org/slack
171
+ .. _community Slack workspace: https://openedx.slack.com/
172
+ .. _Getting Help: https://openedx.org/getting-help
173
+
174
+ .. |pypi-badge| image:: https://img.shields.io/pypi/v/openedx-learning.svg
175
+ :target: https://pypi.python.org/pypi/openedx-learning/
176
+ :alt: PyPI
177
+
178
+ .. |ci-badge| image:: https://github.com/openedx/openedx-learning/workflows/Python%20CI/badge.svg?branch=master
179
+ :target: https://github.com/openedx/openedx-learning/actions
180
+ :alt: CI
181
+
182
+ .. |codecov-badge| image:: https://codecov.io/github/edx/openedx-learning/coverage.svg?branch=master
183
+ :target: https://codecov.io/github/edx/openedx-learning?branch=master
184
+ :alt: Codecov
185
+
186
+ .. |doc-badge| image:: https://readthedocs.org/projects/openedx-learning/badge/?version=latest
187
+ :target: https://openedx-learning.readthedocs.io/en/latest/
188
+ :alt: Documentation
189
+
190
+ .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/openedx-learning.svg
191
+ :target: https://pypi.python.org/pypi/openedx-learning/
192
+ :alt: Supported Python versions
193
+
194
+ .. |license-badge| image:: https://img.shields.io/github/license/edx/openedx-learning.svg
195
+ :target: https://github.com/openedx/openedx-learning/blob/master/LICENSE.txt
196
+ :alt: License
197
+
198
+
199
+ Change Log
200
+ ----------
201
+
202
+ ..
203
+ All enhancements and patches to openedx-learning will be documented
204
+ in this file. It adheres to the structure of https://keepachangelog.com/ ,
205
+ but in reStructuredText instead of Markdown (for ease of incorporation into
206
+ Sphinx documentation and the PyPI description).
207
+
208
+ This project adheres to Semantic Versioning (https://semver.org/).
209
+
210
+ .. There should always be an "Unreleased" section for changes pending release.
211
+
212
+ Unreleased
213
+ ~~~~~~~~~~
214
+
215
+ * Removed usage of ``tox-battery`` and added support for ``tox 4.0``
216
+ * Switch from ``edx-sphinx-theme`` to ``sphinx-book-theme`` since the former is
217
+ deprecated. See https://github.com/openedx/edx-sphinx-theme/issues/184 for
218
+ more details.
219
+
220
+ [0.1.0] - 2021-08-08
221
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
+
223
+ Added
224
+ _____
225
+
226
+ * First release on PyPI.
@@ -5,6 +5,7 @@ README.rst
5
5
  setup.cfg
6
6
  setup.py
7
7
  openedx_learning/__init__.py
8
+ openedx_learning/py.typed
8
9
  openedx_learning.egg-info/PKG-INFO
9
10
  openedx_learning.egg-info/SOURCES.txt
10
11
  openedx_learning.egg-info/dependency_links.txt
@@ -56,6 +57,7 @@ openedx_learning/rest_api/v1/__init__.py
56
57
  openedx_learning/rest_api/v1/components.py
57
58
  openedx_learning/rest_api/v1/urls.py
58
59
  openedx_tagging/__init__.py
60
+ openedx_tagging/py.typed
59
61
  openedx_tagging/core/__init__.py
60
62
  openedx_tagging/core/tagging/__init__.py
61
63
  openedx_tagging/core/tagging/admin.py
@@ -90,6 +92,7 @@ openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py
90
92
  openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py
91
93
  openedx_tagging/core/tagging/migrations/0014_minor_fixes.py
92
94
  openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py
95
+ openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py
93
96
  openedx_tagging/core/tagging/migrations/__init__.py
94
97
  openedx_tagging/core/tagging/models/__init__.py
95
98
  openedx_tagging/core/tagging/models/base.py
@@ -34,7 +34,7 @@ class ObjectTagAdmin(admin.ModelAdmin):
34
34
  """
35
35
  fields = ["object_id", "taxonomy", "tag", "_value"]
36
36
  autocomplete_fields = ["tag"]
37
- list_display = ["object_id", "name", "value"]
37
+ list_display = ["object_id", "export_id", "value"]
38
38
  readonly_fields = ["object_id"]
39
39
 
40
40
  def has_add_permission(self, request):
@@ -205,7 +205,7 @@ def get_object_tags(
205
205
  Value("\t"),
206
206
  output_field=models.CharField(),
207
207
  )))
208
- .annotate(taxonomy_name=Coalesce(F("taxonomy__name"), F("_name")))
208
+ .annotate(taxonomy_name=Coalesce(F("taxonomy__name"), F("_export_id")))
209
209
  # Sort first by taxonomy name, then by tag value in tree order:
210
210
  .order_by("taxonomy_name", "sort_key")
211
211
  )
@@ -274,11 +274,58 @@ def delete_object_tags(object_id: str):
274
274
  tags.delete()
275
275
 
276
276
 
277
+ def _check_new_tag_count(
278
+ new_tag_count: int,
279
+ taxonomy: Taxonomy | None,
280
+ object_id: str,
281
+ taxonomy_export_id: str | None = None,
282
+ ) -> None:
283
+ """
284
+ Checks if the new count of tags for the object is equal or less than 100
285
+ """
286
+ # Exclude to avoid counting the tags that are going to be updated
287
+ if taxonomy:
288
+ current_count = ObjectTag.objects.filter(object_id=object_id).exclude(taxonomy_id=taxonomy.id).count()
289
+ else:
290
+ current_count = ObjectTag.objects.filter(object_id=object_id).exclude(_export_id=taxonomy_export_id).count()
291
+
292
+ if current_count + new_tag_count > 100:
293
+ raise ValueError(
294
+ _("Cannot add more than 100 tags to ({object_id}).").format(object_id=object_id)
295
+ )
296
+
297
+
298
+ def _get_current_tags(
299
+ taxonomy: Taxonomy | None,
300
+ tags: list[str],
301
+ object_id: str,
302
+ object_tag_class: type[ObjectTag] = ObjectTag,
303
+ taxonomy_export_id: str | None = None,
304
+ ) -> list[ObjectTag]:
305
+ """
306
+ Returns the current object tags of the related object_id with taxonomy
307
+ """
308
+ ObjectTagClass = object_tag_class
309
+ if taxonomy:
310
+ if not taxonomy.allow_multiple and len(tags) > 1:
311
+ raise ValueError(_("Taxonomy ({name}) only allows one tag per object.").format(name=taxonomy.name))
312
+ current_tags = list(
313
+ ObjectTagClass.objects.filter(taxonomy=taxonomy, object_id=object_id)
314
+ )
315
+ else:
316
+ current_tags = list(
317
+ ObjectTagClass.objects.filter(_export_id=taxonomy_export_id, object_id=object_id)
318
+ )
319
+ return current_tags
320
+
321
+
277
322
  def tag_object(
278
323
  object_id: str,
279
- taxonomy: Taxonomy,
324
+ taxonomy: Taxonomy | None,
280
325
  tags: list[str],
281
326
  object_tag_class: type[ObjectTag] = ObjectTag,
327
+ create_invalid: bool = False,
328
+ taxonomy_export_id: str | None = None,
282
329
  ) -> None:
283
330
  """
284
331
  Replaces the existing ObjectTag entries for the given taxonomy + object_id
@@ -292,37 +339,34 @@ def tag_object(
292
339
  Raised Tag.DoesNotExist if the proposed tags are invalid for this taxonomy.
293
340
  Preserves existing (valid) tags, adds new (valid) tags, and removes omitted
294
341
  (or invalid) tags.
295
- """
296
-
297
- def _check_new_tag_count(new_tag_count: int) -> None:
298
- """
299
- Checks if the new count of tags for the object is equal or less than 100
300
- """
301
- # Exclude self.id to avoid counting the tags that are going to be updated
302
- current_count = ObjectTag.objects.filter(object_id=object_id).exclude(taxonomy_id=taxonomy.id).count()
303
-
304
- if current_count + new_tag_count > 100:
305
- raise ValueError(
306
- _("Cannot add more than 100 tags to ({object_id}).").format(object_id=object_id)
307
- )
342
+ create_invalid: You can create invalid tags and avoid the previous behavior using.
308
343
 
344
+ taxonomy_export_id: You can create object tags without taxonomy using this param
345
+ and `taxonomy` as None. You need to use the taxonomy.export_id, so you can resync
346
+ this object tag if the taxonomy is created in the future.
347
+ """
309
348
  if not isinstance(tags, list):
310
349
  raise ValueError(_("Tags must be a list, not {type}.").format(type=type(tags).__name__))
311
350
 
312
351
  ObjectTagClass = object_tag_class
313
- taxonomy = taxonomy.cast() # Make sure we're using the right subclass. This is a no-op if we are already.
314
352
  tags = list(dict.fromkeys(tags)) # Remove duplicates preserving order
315
353
 
316
- _check_new_tag_count(len(tags))
317
-
318
- if not taxonomy.allow_multiple and len(tags) > 1:
319
- raise ValueError(_("Taxonomy ({name}) only allows one tag per object.").format(name=taxonomy.name))
320
-
321
- current_tags = list(
322
- ObjectTagClass.objects.filter(taxonomy=taxonomy, object_id=object_id)
354
+ if taxonomy:
355
+ taxonomy = taxonomy.cast() # Make sure we're using the right subclass. This is a no-op if we are already.
356
+ elif not taxonomy_export_id:
357
+ raise ValueError("`taxonomy_export_id` can't be None if `taxonomy` is None")
358
+
359
+ _check_new_tag_count(len(tags), taxonomy, object_id, taxonomy_export_id)
360
+ current_tags = _get_current_tags(
361
+ taxonomy,
362
+ tags,
363
+ object_id,
364
+ object_tag_class,
365
+ taxonomy_export_id
323
366
  )
367
+
324
368
  updated_tags = []
325
- if taxonomy.allow_free_text:
369
+ if taxonomy and taxonomy.allow_free_text:
326
370
  for tag_value in tags:
327
371
  object_tag_index = next((i for (i, t) in enumerate(current_tags) if t.value == tag_value), -1)
328
372
  if object_tag_index >= 0:
@@ -334,19 +378,46 @@ def tag_object(
334
378
  else:
335
379
  # Handle closed taxonomies:
336
380
  for tag_value in tags:
337
- tag = taxonomy.tag_for_value(tag_value) # Will raise Tag.DoesNotExist if the value is invalid.
338
- object_tag_index = next((i for (i, t) in enumerate(current_tags) if t.tag_id == tag.id), -1)
339
- if object_tag_index >= 0:
340
- # This tag is already applied.
341
- object_tag = current_tags.pop(object_tag_index)
342
- if object_tag._value != tag.value: # pylint: disable=protected-access
343
- # The ObjectTag's cached '_value' is out of sync with the Tag, so update it:
344
- object_tag._value = tag.value # pylint: disable=protected-access
381
+ tag = None
382
+ # When export, sometimes, the value has a space at the beginning and end.
383
+ tag_value = tag_value.strip()
384
+ if taxonomy:
385
+ try:
386
+ tag = taxonomy.tag_for_value(tag_value) # Will raise Tag.DoesNotExist if the value is invalid.
387
+ except Tag.DoesNotExist as e:
388
+ if not create_invalid:
389
+ raise e
390
+
391
+ if tag:
392
+ # Tag exists in the taxonomy
393
+ object_tag_index = next((i for (i, t) in enumerate(current_tags) if t.tag_id == tag.id), -1)
394
+ if object_tag_index >= 0:
395
+ # This tag is already applied.
396
+ object_tag = current_tags.pop(object_tag_index)
397
+ if object_tag._value != tag.value: # pylint: disable=protected-access
398
+ # The ObjectTag's cached '_value' is out of sync with the Tag, so update it:
399
+ object_tag._value = tag.value # pylint: disable=protected-access
400
+ updated_tags.append(object_tag)
401
+ else:
402
+ # We are newly applying this tag:
403
+ object_tag = ObjectTagClass(taxonomy=taxonomy, object_id=object_id, tag=tag)
345
404
  updated_tags.append(object_tag)
346
- else:
347
- # We are newly applying this tag:
348
- object_tag = ObjectTagClass(taxonomy=taxonomy, object_id=object_id, tag=tag)
405
+ elif taxonomy:
406
+ # Tag doesn't exist in the taxonomy and `create_invalid` is True
407
+ object_tag = ObjectTagClass(taxonomy=taxonomy, object_id=object_id, _value=tag_value)
349
408
  updated_tags.append(object_tag)
409
+ else:
410
+ # Taxonomy is None (also tag doesn't exist)
411
+ if taxonomy_export_id:
412
+ # This will always be true, since it is verified at the beginning of the function.
413
+ # This condition is placed by the type checks.
414
+ object_tag = ObjectTagClass(
415
+ taxonomy=None,
416
+ object_id=object_id,
417
+ _value=tag_value,
418
+ _export_id=taxonomy_export_id
419
+ )
420
+ updated_tags.append(object_tag)
350
421
 
351
422
  # Save all updated tags at once to avoid partial updates
352
423
  with transaction.atomic():