openedx-learning 0.16.1__py2.py3-none-any.whl → 0.16.3__py2.py3-none-any.whl

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.
@@ -2,4 +2,4 @@
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
4
 
5
- __version__ = "0.16.1"
5
+ __version__ = "0.16.3"
@@ -12,6 +12,7 @@ are stored in this app.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import mimetypes
15
16
  from datetime import datetime, timezone
16
17
  from enum import StrEnum, auto
17
18
  from logging import getLogger
@@ -129,7 +130,7 @@ def create_component_version(
129
130
  def create_next_component_version(
130
131
  component_pk: int,
131
132
  /,
132
- content_to_replace: dict[str, int | None],
133
+ content_to_replace: dict[str, int | None | bytes],
133
134
  created: datetime,
134
135
  title: str | None = None,
135
136
  created_by: int | None = None,
@@ -140,11 +141,14 @@ def create_next_component_version(
140
141
  A very common pattern for making a new ComponentVersion is going to be "make
141
142
  it just like the last version, except changing these one or two things".
142
143
  Before calling this, you should create any new contents via the contents
143
- API, since ``content_to_replace`` needs Content IDs for the values.
144
+ API or send the content bytes as part of ``content_to_replace`` values.
144
145
 
145
146
  The ``content_to_replace`` dict is a mapping of strings representing the
146
- local path/key for a file, to ``Content.id`` values. Using a `None` for
147
- a value in this dict means to delete that key in the next version.
147
+ local path/key for a file, to ``Content.id`` or content bytes values. Using
148
+ `None` for a value in this dict means to delete that key in the next version.
149
+
150
+ Make sure to wrap the function call on a atomic statement:
151
+ ``with transaction.atomic():``
148
152
 
149
153
  It is okay to mark entries for deletion that don't exist. For instance, if a
150
154
  version has ``a.txt`` and ``b.txt``, sending a ``content_to_replace`` value
@@ -186,11 +190,27 @@ def create_next_component_version(
186
190
  component_id=component_pk,
187
191
  )
188
192
  # First copy the new stuff over...
189
- for key, content_pk in content_to_replace.items():
193
+ for key, content_pk_or_bytes in content_to_replace.items():
190
194
  # If the content_pk is None, it means we want to remove the
191
195
  # content represented by our key from the next version. Otherwise,
192
196
  # we add our key->content_pk mapping to the next version.
193
- if content_pk is not None:
197
+ if content_pk_or_bytes is not None:
198
+ if isinstance(content_pk_or_bytes, bytes):
199
+ file_path, file_content = key, content_pk_or_bytes
200
+ media_type_str, _encoding = mimetypes.guess_type(file_path)
201
+ # We use "application/octet-stream" as a generic fallback media type, per
202
+ # RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046
203
+ media_type_str = media_type_str or "application/octet-stream"
204
+ media_type = contents_api.get_or_create_media_type(media_type_str)
205
+ content = contents_api.get_or_create_file_content(
206
+ component.learning_package.id,
207
+ media_type.id,
208
+ data=file_content,
209
+ created=created,
210
+ )
211
+ content_pk = content.pk
212
+ else:
213
+ content_pk = content_pk_or_bytes
194
214
  ComponentVersionContent.objects.create(
195
215
  content_id=content_pk,
196
216
  component_version=component_version,
@@ -4,14 +4,11 @@ Management command to add files to a Component.
4
4
  This is mostly meant to be a debugging tool to let us to easily load some test
5
5
  asset data into the system.
6
6
  """
7
- import mimetypes
8
7
  import pathlib
9
8
  from datetime import datetime, timezone
10
9
 
11
10
  from django.core.management.base import BaseCommand
12
11
 
13
- from ....components.api import create_component_version_content
14
- from ....contents.api import get_or_create_file_content, get_or_create_media_type
15
12
  from ....publishing.api import get_learning_package_by_key
16
13
  from ...api import create_next_component_version, get_component_by_key
17
14
 
@@ -69,39 +66,18 @@ class Command(BaseCommand):
69
66
  )
70
67
 
71
68
  created = datetime.now(tz=timezone.utc)
72
- keys_to_remove = set()
73
- local_keys_to_content = {}
69
+ local_keys_to_content_bytes = {}
74
70
 
75
71
  for file_mapping in file_mappings:
76
72
  local_key, file_path = file_mapping.split(":", 1)
77
73
 
78
- # No file_path means to delete this entry from the next version.
79
- if not file_path:
80
- keys_to_remove.add(local_key)
81
- continue
82
-
83
- media_type_str, _encoding = mimetypes.guess_type(file_path)
84
- media_type = get_or_create_media_type(media_type_str)
85
- content = get_or_create_file_content(
86
- learning_package.id,
87
- media_type.id,
88
- data=pathlib.Path(file_path).read_bytes(),
89
- created=created,
90
- )
91
- local_keys_to_content[local_key] = content.id
74
+ local_keys_to_content_bytes[local_key] = pathlib.Path(file_path).read_bytes() if file_path else None
92
75
 
93
76
  next_version = create_next_component_version(
94
77
  component.pk,
95
- content_to_replace={local_key: None for local_key in keys_to_remove},
78
+ content_to_replace=local_keys_to_content_bytes,
96
79
  created=created,
97
80
  )
98
- for local_key, content_id in sorted(local_keys_to_content.items()):
99
- create_component_version_content(
100
- next_version.pk,
101
- content_id,
102
- key=local_key,
103
- learner_downloadable=True,
104
- )
105
81
 
106
82
  self.stdout.write(
107
83
  f"Created v{next_version.version_num} of "
@@ -6,6 +6,7 @@ more intelligent data models to be useful.
6
6
  from __future__ import annotations
7
7
 
8
8
  from functools import cache, cached_property
9
+ from logging import getLogger
9
10
 
10
11
  from django.conf import settings
11
12
  from django.core.exceptions import ImproperlyConfigured, ValidationError
@@ -19,6 +20,8 @@ from ....lib.fields import MultiCollationTextField, case_insensitive_char_field,
19
20
  from ....lib.managers import WithRelationsManager
20
21
  from ..publishing.models import LearningPackage
21
22
 
23
+ logger = getLogger()
24
+
22
25
  __all__ = [
23
26
  "MediaType",
24
27
  "Content",
@@ -316,8 +319,11 @@ class Content(models.Model):
316
319
 
317
320
  This will return ``None`` if there is no backing file (has_file=False).
318
321
  """
319
- if self.has_file:
320
- return get_storage().path(self.path)
322
+ try:
323
+ if self.has_file:
324
+ return get_storage().path(self.path)
325
+ except NotImplementedError:
326
+ logger.warning("Storage backend does not support path()")
321
327
  return None
322
328
 
323
329
  def read_file(self) -> File:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openedx-learning
3
- Version: 0.16.1
3
+ Version: 0.16.3
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Requires-Python: >=3.11
20
20
  License-File: LICENSE.txt
21
- Requires-Dist: attrs
22
- Requires-Dist: celery
23
- Requires-Dist: rules<4.0
24
- Requires-Dist: Django<5.0
25
21
  Requires-Dist: edx-drf-extensions
22
+ Requires-Dist: Django<5.0
23
+ Requires-Dist: celery
26
24
  Requires-Dist: djangorestframework<4.0
25
+ Requires-Dist: attrs
26
+ Requires-Dist: rules<4.0
27
27
 
28
28
  Open edX Learning Core (and Tagging)
29
29
  ====================================
@@ -1,4 +1,4 @@
1
- openedx_learning/__init__.py,sha256=kCKLJzAlWTUBzj2ePkbJPdM3AEukJRgn6BpyRHwAbdo,69
1
+ openedx_learning/__init__.py,sha256=bkrow8r93asdRsP0nSEgewxu6wo1JajZgTv--0P_RKk,69
2
2
  openedx_learning/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  openedx_learning/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  openedx_learning/api/authoring.py,sha256=vbRpiQ2wOfN3oR2bbN0-bI2ra0QRtha9tVixKW1ENis,929
@@ -18,12 +18,12 @@ openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_opt
18
18
  openedx_learning/apps/authoring/collections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  openedx_learning/apps/authoring/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  openedx_learning/apps/authoring/components/admin.py,sha256=3kFu_PR0BFb8U0zVv3WWhi27i__TDuJG0pFlwr3tKAw,4614
21
- openedx_learning/apps/authoring/components/api.py,sha256=TCLFCPb7ScoRWhe7YpQ7hBiJGjoa47OmazQWcN2hIwU,25759
21
+ openedx_learning/apps/authoring/components/api.py,sha256=BhLlEEBzxFNDHA1dBxoNvIFj92t-vaDF44VExWBbWrc,26861
22
22
  openedx_learning/apps/authoring/components/apps.py,sha256=YoYPsI9gcleA3uEs8CiLIrjUncRMo2DKbYt4mDfzePg,770
23
23
  openedx_learning/apps/authoring/components/models.py,sha256=T-wc7vxaWMlulQmMsVH7m6Pd857P3Eguo0vtflTLURI,13415
24
24
  openedx_learning/apps/authoring/components/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  openedx_learning/apps/authoring/components/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py,sha256=v7mJfnu25M8w4-U1lPPn575W7lYAaOapcHEAZVaxil4,4142
26
+ openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py,sha256=0dJ77NZZoNzYheOdFPXtJrjdL_Z-pCNg3l1rbEGnMCY,3175
27
27
  openedx_learning/apps/authoring/components/migrations/0001_initial.py,sha256=446LkJSFeK8J_-l-bxakZ_BVx_CiJIllGcBYqWcEenA,4664
28
28
  openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py,sha256=98724dtucRjJCRyLt5p45qXYb2d6-ouVGp7PB6zTG6E,539
29
29
  openedx_learning/apps/authoring/components/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -31,7 +31,7 @@ openedx_learning/apps/authoring/contents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
31
31
  openedx_learning/apps/authoring/contents/admin.py,sha256=9Njd_lje1emcd168KBWUTGf0mVJ6K-dMYMcqHNjRU4k,1761
32
32
  openedx_learning/apps/authoring/contents/api.py,sha256=bXb9yQjPfoP1Ynf1aAYz3BEPffK7H5cnba6KdPFSiG0,8818
33
33
  openedx_learning/apps/authoring/contents/apps.py,sha256=EEUZEnww7TcYcyxMovZthG2muNxd7j7nxBIf21gKrp4,398
34
- openedx_learning/apps/authoring/contents/models.py,sha256=nv6T0SXHJovs0FeAtED1iPg_HbkJF5vz2N6WyzYXS6Q,17720
34
+ openedx_learning/apps/authoring/contents/models.py,sha256=RobNGdqkFhChcpub9FszOeqft4c1TJQgS5vQ3Cp2NCA,17899
35
35
  openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTmIGX2KHpjw-PHbfRjxkFEomI5CEDhNKCZ7IpFeE,3060
36
36
  openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  openedx_learning/apps/authoring/publishing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -111,8 +111,8 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=D7brBbgmU7MnbU7Ln
111
111
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
112
112
  openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=LA0EF7-p91JgVrLZpZaG474elKD1dswODGvoPIw47Mg,35837
113
113
  openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
114
- openedx_learning-0.16.1.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
115
- openedx_learning-0.16.1.dist-info/METADATA,sha256=hJck_BnmUyKCAGNXMfVpiJrfiI5iZGoAq6OSR88enOI,8777
116
- openedx_learning-0.16.1.dist-info/WHEEL,sha256=AHX6tWk3qWuce7vKLrj7lnulVHEdWoltgauo8bgCXgU,109
117
- openedx_learning-0.16.1.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
118
- openedx_learning-0.16.1.dist-info/RECORD,,
114
+ openedx_learning-0.16.3.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
115
+ openedx_learning-0.16.3.dist-info/METADATA,sha256=DmGqGCSBExCF06ztJhI0YSnT_IFUnbLt__WYV9He0qU,8777
116
+ openedx_learning-0.16.3.dist-info/WHEEL,sha256=TJ49d73sNs10F0aze1W_bTW2P_X7-F4YXOlBqoqA-jY,109
117
+ openedx_learning-0.16.3.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
118
+ openedx_learning-0.16.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any