openedx-learning 0.2.6__tar.gz → 0.3.3__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.
- {openedx-learning-0.2.6/openedx_learning.egg-info → openedx-learning-0.3.3}/PKG-INFO +2 -2
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/README.rst +1 -1
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/__init__.py +1 -1
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/models.py +1 -1
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/admin.py +34 -8
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/api.py +38 -2
- openedx-learning-0.3.3/openedx_learning/core/publishing/migrations/0002_alter_fk_on_delete.py +30 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/models.py +5 -3
- openedx-learning-0.3.3/openedx_learning/lib/admin_utils.py +100 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3/openedx_learning.egg-info}/PKG-INFO +2 -2
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning.egg-info/SOURCES.txt +5 -0
- openedx-learning-0.3.3/openedx_tagging/core/tagging/admin.py +44 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/api.py +54 -92
- openedx-learning-0.3.3/openedx_tagging/core/tagging/data.py +39 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/api.py +8 -8
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/parsers.py +8 -7
- openedx-learning-0.3.3/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +19 -0
- openedx-learning-0.3.3/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +36 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/models/base.py +259 -70
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/models/system_defined.py +0 -47
- openedx-learning-0.3.3/openedx_tagging/core/tagging/models/utils.py +24 -0
- openedx-learning-0.3.3/openedx_tagging/core/tagging/rest_api/v1/serializers.py +247 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/urls.py +1 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/views.py +205 -189
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rules.py +0 -1
- openedx-learning-0.2.6/openedx_learning/lib/admin_utils.py +0 -28
- openedx-learning-0.2.6/openedx_tagging/core/tagging/admin.py +0 -10
- openedx-learning-0.2.6/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -225
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/CHANGELOG.rst +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/LICENSE.txt +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/MANIFEST.in +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/contrib/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/contrib/media_server/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/contrib/media_server/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/contrib/media_server/urls.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/contrib/media_server/views.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/admin.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/api.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/migrations/0001_initial.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/migrations/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/admin.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/api.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/migrations/0001_initial.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/migrations/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/contents/models.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/migrations/0001_initial.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/migrations/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/model_mixins.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/lib/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/lib/collations.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/lib/fields.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/lib/validators.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/urls.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/v1/components.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/rest_api/v1/urls.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning.egg-info/dependency_links.txt +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning.egg-info/not-zip-safe +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning.egg-info/requires.txt +2 -2
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning.egg-info/top_level.txt +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/apps.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/import_export/template.json +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/models/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/models/import_export.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/utils.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_tagging/core/tagging/urls.py +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/requirements/base.in +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/setup.cfg +0 -0
- {openedx-learning-0.2.6 → openedx-learning-0.3.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.2
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: An experiment.
|
|
5
5
|
Home-page: https://github.com/openedx/openedx-learning
|
|
6
6
|
Author: David Ormsbee
|
|
@@ -136,7 +136,7 @@ Description: openedx-learning
|
|
|
136
136
|
Reporting Security Issues
|
|
137
137
|
-------------------------
|
|
138
138
|
|
|
139
|
-
Please do not report security issues in public. Please email security@
|
|
139
|
+
Please do not report security issues in public. Please email security@openedx.org.
|
|
140
140
|
|
|
141
141
|
Help
|
|
142
142
|
----
|
|
@@ -128,7 +128,7 @@ This repo is in a very experimental state. Discussion using GitHub Issues is wel
|
|
|
128
128
|
Reporting Security Issues
|
|
129
129
|
-------------------------
|
|
130
130
|
|
|
131
|
-
Please do not report security issues in public. Please email security@
|
|
131
|
+
Please do not report security issues in public. Please email security@openedx.org.
|
|
132
132
|
|
|
133
133
|
Help
|
|
134
134
|
----
|
{openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/components/models.py
RENAMED
|
@@ -157,7 +157,7 @@ class ComponentVersion(PublishableEntityVersionMixin):
|
|
|
157
157
|
|
|
158
158
|
# The raw_contents hold the actual interesting data associated with this
|
|
159
159
|
# ComponentVersion.
|
|
160
|
-
raw_contents = models.ManyToManyField(
|
|
160
|
+
raw_contents: models.ManyToManyField[RawContent, ComponentVersionRawContent] = models.ManyToManyField(
|
|
161
161
|
RawContent,
|
|
162
162
|
through="ComponentVersionRawContent",
|
|
163
163
|
related_name="component_versions",
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
from django.contrib import admin
|
|
7
7
|
|
|
8
|
-
from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin
|
|
8
|
+
from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, one_to_one_related_model_html
|
|
9
9
|
|
|
10
10
|
from .models import LearningPackage, PublishableEntity, Published, PublishLog, PublishLogRecord
|
|
11
11
|
|
|
@@ -77,7 +77,7 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
|
|
|
77
77
|
"""
|
|
78
78
|
Read-only admin view for Publishable Entities
|
|
79
79
|
"""
|
|
80
|
-
|
|
80
|
+
list_display = [
|
|
81
81
|
"key",
|
|
82
82
|
"draft_version",
|
|
83
83
|
"published_version",
|
|
@@ -85,23 +85,49 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
|
|
|
85
85
|
"learning_package",
|
|
86
86
|
"created",
|
|
87
87
|
"created_by",
|
|
88
|
-
|
|
89
|
-
readonly_fields = fields
|
|
90
|
-
list_display = fields
|
|
88
|
+
]
|
|
91
89
|
list_filter = ["learning_package"]
|
|
92
90
|
search_fields = ["key", "uuid"]
|
|
93
91
|
|
|
92
|
+
fields = [
|
|
93
|
+
"key",
|
|
94
|
+
"draft_version",
|
|
95
|
+
"published_version",
|
|
96
|
+
"uuid",
|
|
97
|
+
"learning_package",
|
|
98
|
+
"created",
|
|
99
|
+
"created_by",
|
|
100
|
+
"see_also",
|
|
101
|
+
]
|
|
102
|
+
readonly_fields = [
|
|
103
|
+
"key",
|
|
104
|
+
"draft_version",
|
|
105
|
+
"published_version",
|
|
106
|
+
"uuid",
|
|
107
|
+
"learning_package",
|
|
108
|
+
"created",
|
|
109
|
+
"created_by",
|
|
110
|
+
"see_also",
|
|
111
|
+
]
|
|
112
|
+
|
|
94
113
|
def get_queryset(self, request):
|
|
95
114
|
queryset = super().get_queryset(request)
|
|
96
115
|
return queryset.select_related(
|
|
97
|
-
"learning_package", "published__version",
|
|
116
|
+
"learning_package", "published__version",
|
|
98
117
|
)
|
|
99
118
|
|
|
119
|
+
def see_also(self, entity):
|
|
120
|
+
return one_to_one_related_model_html(entity)
|
|
121
|
+
|
|
100
122
|
def draft_version(self, entity):
|
|
101
|
-
|
|
123
|
+
if entity.draft.version:
|
|
124
|
+
return entity.draft.version.version_num
|
|
125
|
+
return None
|
|
102
126
|
|
|
103
127
|
def published_version(self, entity):
|
|
104
|
-
|
|
128
|
+
if entity.published.version:
|
|
129
|
+
return entity.published.version.version_num
|
|
130
|
+
return None
|
|
105
131
|
|
|
106
132
|
|
|
107
133
|
@admin.register(Published)
|
|
@@ -93,9 +93,9 @@ def create_publishable_entity_version(
|
|
|
93
93
|
created=created,
|
|
94
94
|
created_by_id=created_by,
|
|
95
95
|
)
|
|
96
|
-
Draft.objects.
|
|
96
|
+
Draft.objects.update_or_create(
|
|
97
97
|
entity_id=entity_id,
|
|
98
|
-
|
|
98
|
+
defaults={"version": version},
|
|
99
99
|
)
|
|
100
100
|
return version
|
|
101
101
|
|
|
@@ -180,6 +180,42 @@ def publish_from_drafts(
|
|
|
180
180
|
return publish_log
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def get_draft_version(publishable_entity_id: int) -> PublishableEntityVersion | None:
|
|
184
|
+
"""
|
|
185
|
+
Return current draft PublishableEntityVersion for this PublishableEntity.
|
|
186
|
+
|
|
187
|
+
This function will return None if there is no current draft.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
draft = Draft.objects.select_related("version").get(
|
|
191
|
+
entity_id=publishable_entity_id
|
|
192
|
+
)
|
|
193
|
+
except Draft.DoesNotExist:
|
|
194
|
+
# No draft was ever created.
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
# draft.version could be None if it was set that way by set_draft_version.
|
|
198
|
+
# Setting the Draft.version to None is how we show that we've "deleted" the
|
|
199
|
+
# content in Studio.
|
|
200
|
+
return draft.version
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def set_draft_version(publishable_entity_id: int, publishable_entity_version_pk: int | None) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Modify the Draft of a PublishableEntity to be a PublishableEntityVersion.
|
|
206
|
+
|
|
207
|
+
This would most commonly be used to set the Draft to point to a newly
|
|
208
|
+
created PublishableEntityVersion that was created in Studio (because someone
|
|
209
|
+
edited some content). Setting a Draft's version to None is like deleting it
|
|
210
|
+
from Studio's editing point of view. We don't actually delete the Draft row
|
|
211
|
+
because we'll need that for publishing purposes (i.e. to delete content from
|
|
212
|
+
the published branch).
|
|
213
|
+
"""
|
|
214
|
+
draft = Draft.objects.get(entity_id=publishable_entity_id)
|
|
215
|
+
draft.version_id = publishable_entity_version_pk
|
|
216
|
+
draft.save()
|
|
217
|
+
|
|
218
|
+
|
|
183
219
|
def register_content_models(
|
|
184
220
|
content_model_cls: type[PublishableEntityMixin],
|
|
185
221
|
content_version_model_cls: type[PublishableEntityVersionMixin],
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated by Django 3.2.21 on 2023-10-13 14:25
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
"""
|
|
9
|
+
Make it so that Draft and Published cascade deletes from PublishableEntity.
|
|
10
|
+
|
|
11
|
+
This makes it so that deleting a LearningPackage properly cleans up these
|
|
12
|
+
models as well.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
('oel_publishing', '0001_initial'),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.AlterField(
|
|
21
|
+
model_name='draft',
|
|
22
|
+
name='entity',
|
|
23
|
+
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentity'),
|
|
24
|
+
),
|
|
25
|
+
migrations.AlterField(
|
|
26
|
+
model_name='published',
|
|
27
|
+
name='entity',
|
|
28
|
+
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentity'),
|
|
29
|
+
),
|
|
30
|
+
]
|
{openedx-learning-0.2.6 → openedx-learning-0.3.3}/openedx_learning/core/publishing/models.py
RENAMED
|
@@ -308,10 +308,12 @@ class Draft(models.Model):
|
|
|
308
308
|
are updated, not when publishing happens. The Published model only changes
|
|
309
309
|
when something is published.
|
|
310
310
|
"""
|
|
311
|
-
|
|
311
|
+
# If we're removing a PublishableEntity entirely, also remove the Draft
|
|
312
|
+
# entry for it. This isn't a normal operation, but can happen if you're
|
|
313
|
+
# deleting an entire LearningPackage.
|
|
312
314
|
entity = models.OneToOneField(
|
|
313
315
|
PublishableEntity,
|
|
314
|
-
on_delete=models.
|
|
316
|
+
on_delete=models.CASCADE,
|
|
315
317
|
primary_key=True,
|
|
316
318
|
)
|
|
317
319
|
version = models.OneToOneField(
|
|
@@ -425,7 +427,7 @@ class Published(models.Model):
|
|
|
425
427
|
|
|
426
428
|
entity = models.OneToOneField(
|
|
427
429
|
PublishableEntity,
|
|
428
|
-
on_delete=models.
|
|
430
|
+
on_delete=models.CASCADE,
|
|
429
431
|
primary_key=True,
|
|
430
432
|
)
|
|
431
433
|
version = models.OneToOneField(
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convenience utilities for the Django Admin.
|
|
3
|
+
"""
|
|
4
|
+
from django.contrib import admin
|
|
5
|
+
from django.db.models.fields.reverse_related import OneToOneRel
|
|
6
|
+
from django.urls import NoReverseMatch, reverse
|
|
7
|
+
from django.utils.html import format_html, format_html_join
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReadOnlyModelAdmin(admin.ModelAdmin):
|
|
11
|
+
"""
|
|
12
|
+
ModelAdmin subclass that removes any editing ability.
|
|
13
|
+
|
|
14
|
+
The Django Admin is really useful for quickly examining model data. At the
|
|
15
|
+
same time, model creation and updates follow specific rules that are meant
|
|
16
|
+
to be enforced above the model layer (in api.py files), so making edits in
|
|
17
|
+
the Django Admin is potentially dangerous.
|
|
18
|
+
|
|
19
|
+
In general, if you're providing Django Admin interfaces for your
|
|
20
|
+
openedx-learning related app data models, you should subclass this class
|
|
21
|
+
instead of subclassing admin.ModelAdmin directly.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def has_add_permission(self, request):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
def has_change_permission(self, request, obj=None):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def has_delete_permission(self, request, obj=None):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def one_to_one_related_model_html(model_obj):
|
|
35
|
+
"""
|
|
36
|
+
HTML for clickable list of a models that are 1:1-related to ``model_obj``.
|
|
37
|
+
|
|
38
|
+
Our design pattern encourages people to hang models off of our lower-level
|
|
39
|
+
core lib models. For example, Component has a OneToOneField that references
|
|
40
|
+
PublishableEntity. It would be really convenient to have PublishableEntity's
|
|
41
|
+
admin page display the link to Component, but the ``publishable`` app is
|
|
42
|
+
intended to be a lower-level app than ``components`` and isn't supposed to
|
|
43
|
+
be aware of it. The same situation occurs for third-party apps that might
|
|
44
|
+
want to extend Component.
|
|
45
|
+
|
|
46
|
+
So instead of creating a circular dependency by having ``publishing``
|
|
47
|
+
referencing ``components``, we use Django model introspection to iterate
|
|
48
|
+
over all models that have a OneToOneField to the passe din``model_obj``.
|
|
49
|
+
This allows us to preserve our dependency boundaries within openedx-learning
|
|
50
|
+
and accomodate any third party apps that might further extend these models.
|
|
51
|
+
|
|
52
|
+
This will output a list with one entry for each related field.
|
|
53
|
+
|
|
54
|
+
* If the field's value is None, we output f"{field_name}: -"
|
|
55
|
+
* If the field has a value but no "change" admin page, we output the string
|
|
56
|
+
representation of the model obj referenced by that field, i.e.
|
|
57
|
+
f{"field_name: {related_model_obj}"}.
|
|
58
|
+
* If the field has a value and an admin page, we output the same as above,
|
|
59
|
+
but we make the related model object's string representation a link to its
|
|
60
|
+
"change" admin page.
|
|
61
|
+
"""
|
|
62
|
+
one_to_one_field_names = [
|
|
63
|
+
field.name
|
|
64
|
+
for field in model_obj._meta.related_objects
|
|
65
|
+
if isinstance(field, OneToOneRel)
|
|
66
|
+
]
|
|
67
|
+
text = []
|
|
68
|
+
for field_name in one_to_one_field_names:
|
|
69
|
+
related_model_obj = getattr(model_obj, field_name, None)
|
|
70
|
+
|
|
71
|
+
# No instance of the related model was found, so just use "-"
|
|
72
|
+
if related_model_obj is None:
|
|
73
|
+
text.append(f"{field_name}: -")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
app_label = related_model_obj._meta.app_label
|
|
77
|
+
model_name = related_model_obj._meta.model_name
|
|
78
|
+
try:
|
|
79
|
+
details_url = reverse(
|
|
80
|
+
f"admin:{app_label}_{model_name}_change",
|
|
81
|
+
args=(related_model_obj.pk,)
|
|
82
|
+
)
|
|
83
|
+
except NoReverseMatch:
|
|
84
|
+
# No Admin URL available, so just put the str representation of the
|
|
85
|
+
# related model instance.
|
|
86
|
+
text.append(f"{field_name}: {related_model_obj}")
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# If we go this far, there is a related model instance and it has a
|
|
90
|
+
# "change" admin page (even though it's probably read-only via
|
|
91
|
+
# permissions).
|
|
92
|
+
html = format_html(
|
|
93
|
+
'{}: <a href="{}">{}</a>',
|
|
94
|
+
field_name,
|
|
95
|
+
details_url,
|
|
96
|
+
related_model_obj,
|
|
97
|
+
)
|
|
98
|
+
text.append(html)
|
|
99
|
+
|
|
100
|
+
return format_html_join("\n", "<li>{}</li>", ((t,) for t in text))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.2
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: An experiment.
|
|
5
5
|
Home-page: https://github.com/openedx/openedx-learning
|
|
6
6
|
Author: David Ormsbee
|
|
@@ -136,7 +136,7 @@ Description: openedx-learning
|
|
|
136
136
|
Reporting Security Issues
|
|
137
137
|
-------------------------
|
|
138
138
|
|
|
139
|
-
Please do not report security issues in public. Please email security@
|
|
139
|
+
Please do not report security issues in public. Please email security@openedx.org.
|
|
140
140
|
|
|
141
141
|
Help
|
|
142
142
|
----
|
|
@@ -38,6 +38,7 @@ openedx_learning/core/publishing/apps.py
|
|
|
38
38
|
openedx_learning/core/publishing/model_mixins.py
|
|
39
39
|
openedx_learning/core/publishing/models.py
|
|
40
40
|
openedx_learning/core/publishing/migrations/0001_initial.py
|
|
41
|
+
openedx_learning/core/publishing/migrations/0002_alter_fk_on_delete.py
|
|
41
42
|
openedx_learning/core/publishing/migrations/__init__.py
|
|
42
43
|
openedx_learning/lib/__init__.py
|
|
43
44
|
openedx_learning/lib/admin_utils.py
|
|
@@ -56,6 +57,7 @@ openedx_tagging/core/tagging/__init__.py
|
|
|
56
57
|
openedx_tagging/core/tagging/admin.py
|
|
57
58
|
openedx_tagging/core/tagging/api.py
|
|
58
59
|
openedx_tagging/core/tagging/apps.py
|
|
60
|
+
openedx_tagging/core/tagging/data.py
|
|
59
61
|
openedx_tagging/core/tagging/rules.py
|
|
60
62
|
openedx_tagging/core/tagging/urls.py
|
|
61
63
|
openedx_tagging/core/tagging/import_export/__init__.py
|
|
@@ -81,11 +83,14 @@ openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py
|
|
|
81
83
|
openedx_tagging/core/tagging/migrations/0010_cleanups.py
|
|
82
84
|
openedx_tagging/core/tagging/migrations/0011_remove_required.py
|
|
83
85
|
openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py
|
|
86
|
+
openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py
|
|
87
|
+
openedx_tagging/core/tagging/migrations/0014_minor_fixes.py
|
|
84
88
|
openedx_tagging/core/tagging/migrations/__init__.py
|
|
85
89
|
openedx_tagging/core/tagging/models/__init__.py
|
|
86
90
|
openedx_tagging/core/tagging/models/base.py
|
|
87
91
|
openedx_tagging/core/tagging/models/import_export.py
|
|
88
92
|
openedx_tagging/core/tagging/models/system_defined.py
|
|
93
|
+
openedx_tagging/core/tagging/models/utils.py
|
|
89
94
|
openedx_tagging/core/tagging/rest_api/__init__.py
|
|
90
95
|
openedx_tagging/core/tagging/rest_api/paginators.py
|
|
91
96
|
openedx_tagging/core/tagging/rest_api/urls.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tagging app admin
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from django.contrib import admin
|
|
7
|
+
|
|
8
|
+
from .models import ObjectTag, Tag, Taxonomy
|
|
9
|
+
|
|
10
|
+
admin.site.register(Taxonomy)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@admin.register(Tag)
|
|
14
|
+
class TagAdmin(admin.ModelAdmin):
|
|
15
|
+
"""
|
|
16
|
+
Admin definition for Tag model
|
|
17
|
+
"""
|
|
18
|
+
autocomplete_fields = ["parent"]
|
|
19
|
+
search_fields = ["value", "external_id"]
|
|
20
|
+
list_display = ["__str__", "taxonomy", "external_id"]
|
|
21
|
+
list_filter = ["taxonomy"]
|
|
22
|
+
|
|
23
|
+
def has_add_permission(self, request):
|
|
24
|
+
"""
|
|
25
|
+
Don't create Tags using the django admin. Use the API or UI.
|
|
26
|
+
"""
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@admin.register(ObjectTag)
|
|
31
|
+
class ObjectTagAdmin(admin.ModelAdmin):
|
|
32
|
+
"""
|
|
33
|
+
Admin definition for ObjectTag model
|
|
34
|
+
"""
|
|
35
|
+
fields = ["object_id", "taxonomy", "tag", "_value"]
|
|
36
|
+
autocomplete_fields = ["tag"]
|
|
37
|
+
list_display = ["object_id", "name", "value"]
|
|
38
|
+
readonly_fields = ["object_id"]
|
|
39
|
+
|
|
40
|
+
def has_add_permission(self, request):
|
|
41
|
+
"""
|
|
42
|
+
Don't create ObjectTags using the django admin. Use the API or UI.
|
|
43
|
+
"""
|
|
44
|
+
return False
|
|
@@ -12,11 +12,14 @@ are stored in this app.
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
from django.db import transaction
|
|
16
|
-
from django.db.models import F, QuerySet
|
|
15
|
+
from django.db import models, transaction
|
|
16
|
+
from django.db.models import F, QuerySet, Value
|
|
17
|
+
from django.db.models.functions import Coalesce, Concat, Lower
|
|
17
18
|
from django.utils.translation import gettext as _
|
|
18
19
|
|
|
20
|
+
from .data import TagDataQuerySet
|
|
19
21
|
from .models import ObjectTag, Tag, Taxonomy
|
|
22
|
+
from .models.utils import ConcatNull
|
|
20
23
|
|
|
21
24
|
# Export this as part of the API
|
|
22
25
|
TagDoesNotExist = Tag.DoesNotExist
|
|
@@ -70,54 +73,66 @@ def get_taxonomies(enabled=True) -> QuerySet[Taxonomy]:
|
|
|
70
73
|
return queryset.filter(enabled=enabled)
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
def get_tags(taxonomy: Taxonomy) ->
|
|
76
|
+
def get_tags(taxonomy: Taxonomy) -> TagDataQuerySet:
|
|
74
77
|
"""
|
|
75
|
-
Returns a
|
|
78
|
+
Returns a QuerySet of all the tags in the given taxonomy.
|
|
76
79
|
|
|
77
|
-
Note that if the taxonomy
|
|
80
|
+
Note that if the taxonomy is dynamic or free-text, only tags that have
|
|
81
|
+
already been applied to some object will be returned.
|
|
78
82
|
"""
|
|
79
|
-
return taxonomy.cast().
|
|
83
|
+
return taxonomy.cast().get_filtered_tags()
|
|
80
84
|
|
|
81
85
|
|
|
82
|
-
def get_root_tags(taxonomy: Taxonomy) ->
|
|
86
|
+
def get_root_tags(taxonomy: Taxonomy) -> TagDataQuerySet:
|
|
83
87
|
"""
|
|
84
88
|
Returns a list of the root tags for the given taxonomy.
|
|
85
89
|
|
|
86
90
|
Note that if the taxonomy allows free-text tags, then the returned list will be empty.
|
|
87
91
|
"""
|
|
88
|
-
return
|
|
92
|
+
return taxonomy.cast().get_filtered_tags(depth=1)
|
|
89
93
|
|
|
90
94
|
|
|
91
|
-
def search_tags(
|
|
95
|
+
def search_tags(
|
|
96
|
+
taxonomy: Taxonomy,
|
|
97
|
+
search_term: str,
|
|
98
|
+
exclude_object_id: str | None = None,
|
|
99
|
+
include_counts: bool = False,
|
|
100
|
+
) -> TagDataQuerySet:
|
|
92
101
|
"""
|
|
93
|
-
Returns a list of all tags that contains `search_term` of the given
|
|
102
|
+
Returns a list of all tags that contains `search_term` of the given
|
|
103
|
+
taxonomy, as well as their ancestors (so they can be displayed in a tree).
|
|
94
104
|
|
|
95
|
-
|
|
105
|
+
If exclude_object_id is set, any tags applied to that object will be
|
|
106
|
+
excluded from the results, e.g. to power an autocomplete search when adding
|
|
107
|
+
additional tags to an object.
|
|
96
108
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
excluded_values = None
|
|
110
|
+
if exclude_object_id:
|
|
111
|
+
# Fetch tags that the object already has to exclude them from the result.
|
|
112
|
+
# Note: this adds a fair bit of complexity. In the future, maybe we can just do this filtering on the frontend?
|
|
113
|
+
excluded_values = list(
|
|
114
|
+
taxonomy.objecttag_set.filter(object_id=exclude_object_id).values_list(
|
|
115
|
+
"_value", flat=True
|
|
116
|
+
)
|
|
101
117
|
)
|
|
118
|
+
qs = taxonomy.cast().get_filtered_tags(
|
|
119
|
+
search_term=search_term,
|
|
120
|
+
excluded_values=excluded_values,
|
|
121
|
+
include_counts=include_counts,
|
|
102
122
|
)
|
|
123
|
+
return qs
|
|
103
124
|
|
|
104
125
|
|
|
105
126
|
def get_children_tags(
|
|
106
127
|
taxonomy: Taxonomy,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
) -> list[Tag]:
|
|
128
|
+
parent_tag_value: str,
|
|
129
|
+
) -> TagDataQuerySet:
|
|
110
130
|
"""
|
|
111
|
-
Returns a
|
|
131
|
+
Returns a QuerySet of children tags for the given parent tag.
|
|
112
132
|
|
|
113
133
|
Note that if the taxonomy allows free-text tags, then the returned list will be empty.
|
|
114
134
|
"""
|
|
115
|
-
return
|
|
116
|
-
taxonomy.cast().get_filtered_tags(
|
|
117
|
-
parent_tag_id=parent_tag_id,
|
|
118
|
-
search_term=search_term,
|
|
119
|
-
)
|
|
120
|
-
)
|
|
135
|
+
return taxonomy.cast().get_filtered_tags(parent_tag_value=parent_tag_value, depth=1)
|
|
121
136
|
|
|
122
137
|
|
|
123
138
|
def resync_object_tags(object_tags: QuerySet | None = None) -> int:
|
|
@@ -152,8 +167,20 @@ def get_object_tags(
|
|
|
152
167
|
filters = {"taxonomy_id": taxonomy_id} if taxonomy_id else {}
|
|
153
168
|
tags = (
|
|
154
169
|
object_tag_class.objects.filter(object_id=object_id, **filters)
|
|
155
|
-
|
|
156
|
-
.
|
|
170
|
+
# Preload related objects, including data for the "get_lineage" method on ObjectTag/Tag:
|
|
171
|
+
.select_related("taxonomy", "tag", "tag__parent", "tag__parent__parent")
|
|
172
|
+
# Sort the tags within each taxonomy in "tree order". See Taxonomy._get_filtered_tags_deep for details on this:
|
|
173
|
+
.annotate(sort_key=Lower(Concat(
|
|
174
|
+
ConcatNull(F("tag__parent__parent__parent__value"), Value("\t")),
|
|
175
|
+
ConcatNull(F("tag__parent__parent__value"), Value("\t")),
|
|
176
|
+
ConcatNull(F("tag__parent__value"), Value("\t")),
|
|
177
|
+
Coalesce(F("tag__value"), F("_value")),
|
|
178
|
+
Value("\t"),
|
|
179
|
+
output_field=models.CharField(),
|
|
180
|
+
)))
|
|
181
|
+
.annotate(taxonomy_name=Coalesce(F("taxonomy__name"), F("_name")))
|
|
182
|
+
# Sort first by taxonomy name, then by tag value in tree order:
|
|
183
|
+
.order_by("taxonomy_name", "sort_key")
|
|
157
184
|
)
|
|
158
185
|
return tags
|
|
159
186
|
|
|
@@ -253,71 +280,6 @@ def tag_object(
|
|
|
253
280
|
object_tag.save()
|
|
254
281
|
|
|
255
282
|
|
|
256
|
-
# TODO: return tags from closed taxonomies as well as the count of how many times each is used.
|
|
257
|
-
def autocomplete_tags(
|
|
258
|
-
taxonomy: Taxonomy,
|
|
259
|
-
search: str,
|
|
260
|
-
object_id: str | None = None,
|
|
261
|
-
object_tags_only=True,
|
|
262
|
-
) -> QuerySet:
|
|
263
|
-
"""
|
|
264
|
-
Provides auto-complete suggestions by matching the `search` string against existing
|
|
265
|
-
ObjectTags linked to the given taxonomy. A case-insensitive search is used in order
|
|
266
|
-
to return the highest number of relevant tags.
|
|
267
|
-
|
|
268
|
-
If `object_id` is provided, then object tag values already linked to this object
|
|
269
|
-
are omitted from the returned suggestions. (ObjectTag values must be unique for a
|
|
270
|
-
given object + taxonomy, and so omitting these suggestions helps users avoid
|
|
271
|
-
duplication errors.).
|
|
272
|
-
|
|
273
|
-
Returns a QuerySet of dictionaries containing distinct `value` (string) and
|
|
274
|
-
`tag` (numeric ID) values, sorted alphabetically by `value`.
|
|
275
|
-
The `value` is what should be shown as a suggestion to users,
|
|
276
|
-
and if it's a free-text taxonomy, `tag` will be `None`: we include the `tag` ID
|
|
277
|
-
in anticipation of the second use case listed below.
|
|
278
|
-
|
|
279
|
-
Use cases:
|
|
280
|
-
* This method is useful for reducing tag variation in free-text taxonomies by showing
|
|
281
|
-
users tags that are similar to what they're typing. E.g., if the `search` string "dn"
|
|
282
|
-
shows that other objects have been tagged with "DNA", "DNA electrophoresis", and "DNA fingerprinting",
|
|
283
|
-
this encourages users to use those existing tags if relevant, instead of creating new ones that
|
|
284
|
-
look similar (e.g. "dna finger-printing").
|
|
285
|
-
* It could also be used to assist tagging for closed taxonomies with a list of possible tags which is too
|
|
286
|
-
large to return all at once, e.g. a user model taxonomy that dynamically creates tags on request for any
|
|
287
|
-
registered user in the database. (Note that this is not implemented yet, but may be as part of a future change.)
|
|
288
|
-
"""
|
|
289
|
-
if not object_tags_only:
|
|
290
|
-
raise NotImplementedError(
|
|
291
|
-
_(
|
|
292
|
-
"Using this would return a query set of tags instead of object tags."
|
|
293
|
-
"For now we recommend fetching all of the taxonomy's tags "
|
|
294
|
-
"using get_tags() and filtering them on the frontend."
|
|
295
|
-
)
|
|
296
|
-
)
|
|
297
|
-
# Fetch tags that the object already has to exclude them from the result
|
|
298
|
-
excluded_tags: list[str] = []
|
|
299
|
-
if object_id:
|
|
300
|
-
excluded_tags = list(
|
|
301
|
-
taxonomy.objecttag_set.filter(object_id=object_id).values_list(
|
|
302
|
-
"_value", flat=True
|
|
303
|
-
)
|
|
304
|
-
)
|
|
305
|
-
return (
|
|
306
|
-
# Fetch object tags from this taxonomy whose value contains the search
|
|
307
|
-
taxonomy.objecttag_set.filter(_value__icontains=search)
|
|
308
|
-
# omit any tags whose values match the tags on the given object
|
|
309
|
-
.exclude(_value__in=excluded_tags)
|
|
310
|
-
# alphabetical ordering
|
|
311
|
-
.order_by("_value")
|
|
312
|
-
# Alias the `_value` field to `value` to make it nicer for users
|
|
313
|
-
.annotate(value=F("_value"))
|
|
314
|
-
# obtain tag values
|
|
315
|
-
.values("value", "tag_id")
|
|
316
|
-
# remove repeats
|
|
317
|
-
.distinct()
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
283
|
def add_tag_to_taxonomy(
|
|
322
284
|
taxonomy: Taxonomy,
|
|
323
285
|
tag: str,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models used by openedx-tagging
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
7
|
+
|
|
8
|
+
from django.db.models import QuerySet
|
|
9
|
+
from typing_extensions import NotRequired, TypeAlias
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TagData(TypedDict):
|
|
13
|
+
"""
|
|
14
|
+
Data about a single tag. Many of the tagging API methods return Django
|
|
15
|
+
QuerySets that resolve to these dictionaries.
|
|
16
|
+
|
|
17
|
+
Even though the data will be in this same format, it will not necessarily
|
|
18
|
+
be an instance of this class but rather a plain dictionary. This is more a
|
|
19
|
+
type than a class.
|
|
20
|
+
"""
|
|
21
|
+
value: str
|
|
22
|
+
external_id: str | None
|
|
23
|
+
child_count: int
|
|
24
|
+
depth: int
|
|
25
|
+
parent_value: str | None
|
|
26
|
+
# Note: usage_count may or may not be present, depending on the request.
|
|
27
|
+
usage_count: NotRequired[int]
|
|
28
|
+
# Internal database ID, if any. Generally should not be used; prefer 'value' which is unique within each taxonomy.
|
|
29
|
+
_id: int | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from django_stubs_ext import ValuesQuerySet
|
|
34
|
+
TagDataQuerySet: TypeAlias = ValuesQuerySet[Any, TagData]
|
|
35
|
+
# The following works better for pyright (provides proper VS Code autocompletions),
|
|
36
|
+
# but I can't find any way to specify different types for pyright vs mypy :/
|
|
37
|
+
# TagDataQuerySet: TypeAlias = QuerySet[TagData]
|
|
38
|
+
else:
|
|
39
|
+
TagDataQuerySet = QuerySet[TagData]
|