openedx-learning 0.16.3__py2.py3-none-any.whl → 0.18.0__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.
- openedx_learning/__init__.py +1 -1
- openedx_learning/apps/authoring/components/admin.py +0 -1
- openedx_learning/apps/authoring/components/api.py +2 -36
- openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +17 -0
- openedx_learning/apps/authoring/components/models.py +0 -37
- openedx_learning/apps/authoring/publishing/api.py +20 -0
- openedx_learning/contrib/media_server/views.py +1 -6
- {openedx_learning-0.16.3.dist-info → openedx_learning-0.18.0.dist-info}/METADATA +5 -5
- {openedx_learning-0.16.3.dist-info → openedx_learning-0.18.0.dist-info}/RECORD +12 -11
- {openedx_learning-0.16.3.dist-info → openedx_learning-0.18.0.dist-info}/WHEEL +1 -1
- {openedx_learning-0.16.3.dist-info → openedx_learning-0.18.0.dist-info}/LICENSE.txt +0 -0
- {openedx_learning-0.16.3.dist-info → openedx_learning-0.18.0.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -215,7 +215,6 @@ def create_next_component_version(
|
|
|
215
215
|
content_id=content_pk,
|
|
216
216
|
component_version=component_version,
|
|
217
217
|
key=key,
|
|
218
|
-
learner_downloadable=False,
|
|
219
218
|
)
|
|
220
219
|
# Now copy any old associations that existed, as long as they aren't
|
|
221
220
|
# in conflict with the new stuff or marked for deletion.
|
|
@@ -227,7 +226,6 @@ def create_next_component_version(
|
|
|
227
226
|
content_id=cvrc.content_id,
|
|
228
227
|
component_version=component_version,
|
|
229
228
|
key=cvrc.key,
|
|
230
|
-
learner_downloadable=cvrc.learner_downloadable,
|
|
231
229
|
)
|
|
232
230
|
|
|
233
231
|
return component_version
|
|
@@ -422,7 +420,6 @@ def create_component_version_content(
|
|
|
422
420
|
content_id: int,
|
|
423
421
|
/,
|
|
424
422
|
key: str,
|
|
425
|
-
learner_downloadable: bool = False,
|
|
426
423
|
) -> ComponentVersionContent:
|
|
427
424
|
"""
|
|
428
425
|
Add a Content to the given ComponentVersion
|
|
@@ -445,7 +442,6 @@ def create_component_version_content(
|
|
|
445
442
|
component_version_id=component_version_id,
|
|
446
443
|
content_id=content_id,
|
|
447
444
|
key=key,
|
|
448
|
-
learner_downloadable=learner_downloadable,
|
|
449
445
|
)
|
|
450
446
|
return cvrc
|
|
451
447
|
|
|
@@ -453,7 +449,6 @@ def create_component_version_content(
|
|
|
453
449
|
class AssetError(StrEnum):
|
|
454
450
|
"""Error codes related to fetching ComponentVersion assets."""
|
|
455
451
|
ASSET_PATH_NOT_FOUND_FOR_COMPONENT_VERSION = auto()
|
|
456
|
-
ASSET_NOT_LEARNER_DOWNLOADABLE = auto()
|
|
457
452
|
ASSET_HAS_NO_DOWNLOAD_FILE = auto()
|
|
458
453
|
|
|
459
454
|
|
|
@@ -484,7 +479,6 @@ def get_redirect_response_for_component_asset(
|
|
|
484
479
|
component_version_uuid: UUID,
|
|
485
480
|
asset_path: Path,
|
|
486
481
|
public: bool = False,
|
|
487
|
-
learner_downloadable_only: bool = True,
|
|
488
482
|
) -> HttpResponse:
|
|
489
483
|
"""
|
|
490
484
|
``HttpResponse`` for a reverse-proxy to serve a ``ComponentVersion`` asset.
|
|
@@ -498,11 +492,6 @@ def get_redirect_response_for_component_asset(
|
|
|
498
492
|
If ``True``, this will return an ``HttpResponse`` that can be cached in
|
|
499
493
|
a CDN and shared across many clients.
|
|
500
494
|
|
|
501
|
-
:param learner_downloadable_only: Only return assets that are meant to be
|
|
502
|
-
downloadable by Learners, i.e. in the LMS experience. If this is
|
|
503
|
-
``True``, then requests for assets that are not meant for student
|
|
504
|
-
download will return a ``404`` error response.
|
|
505
|
-
|
|
506
495
|
**Response Codes**
|
|
507
496
|
|
|
508
497
|
If the asset exists for this ``ComponentVersion``, this function will return
|
|
@@ -512,15 +501,10 @@ def get_redirect_response_for_component_asset(
|
|
|
512
501
|
the ``ComponentVersion`` itself does not exist, the response code will be
|
|
513
502
|
``404``.
|
|
514
503
|
|
|
515
|
-
|
|
516
|
-
*this function does not do auth checking of any sort*–it will never return
|
|
504
|
+
This function does not do auth checking of any sort. It will never return
|
|
517
505
|
a ``401`` or ``403`` response code. That is by design. Figuring out who is
|
|
518
506
|
making the request and whether they have permission to do so is the
|
|
519
|
-
responsiblity of whatever is calling this function.
|
|
520
|
-
``learner_downloadable_only`` flag is intended to be a filter for the entire
|
|
521
|
-
view. When it's True, not even staff can download component-internal assets.
|
|
522
|
-
This is intended to protect us from accidentally allowing sensitive grading
|
|
523
|
-
code to get leaked out.
|
|
507
|
+
responsiblity of whatever is calling this function.
|
|
524
508
|
|
|
525
509
|
**Metadata Headers**
|
|
526
510
|
|
|
@@ -596,24 +580,6 @@ def get_redirect_response_for_component_asset(
|
|
|
596
580
|
)
|
|
597
581
|
return HttpResponseNotFound(headers=info_headers)
|
|
598
582
|
|
|
599
|
-
# Check: If we're asking only for Learner Downloadable assets, and the asset
|
|
600
|
-
# in question is not supposed to be downloadable by learners, then we give a
|
|
601
|
-
# 404 error. Even staff members are not expected to be able to download
|
|
602
|
-
# these assets via the LMS endpoint that serves students. Studio would be
|
|
603
|
-
# expected to have an entirely different view to serve these assets in that
|
|
604
|
-
# context (along with different timeouts, auth, and cache settings). So in
|
|
605
|
-
# that sense, the asset doesn't exist for that particular endpoint.
|
|
606
|
-
if learner_downloadable_only and (not cv_content.learner_downloadable):
|
|
607
|
-
logger.error(
|
|
608
|
-
f"ComponentVersion {component_version_uuid} has asset {asset_path}, "
|
|
609
|
-
"but it is not meant to be downloadable by learners "
|
|
610
|
-
"(ComponentVersionContent.learner_downloadable=False)."
|
|
611
|
-
)
|
|
612
|
-
info_headers.update(
|
|
613
|
-
_error_header(AssetError.ASSET_NOT_LEARNER_DOWNLOADABLE)
|
|
614
|
-
)
|
|
615
|
-
return HttpResponseNotFound(headers=info_headers)
|
|
616
|
-
|
|
617
583
|
# At this point, we know that there is valid Content that we want to send.
|
|
618
584
|
# This adds Content-level headers, like the hash/etag and content type.
|
|
619
585
|
info_headers.update(contents_api.get_content_info_headers(content))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-11-06 17:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('oel_components', '0002_alter_componentversioncontent_key'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='componentversioncontent',
|
|
15
|
+
name='learner_downloadable',
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -254,43 +254,6 @@ class ComponentVersionContent(models.Model):
|
|
|
254
254
|
# identifiers that don't map as cleanly to file paths at some point.
|
|
255
255
|
key = key_field(db_column="_key")
|
|
256
256
|
|
|
257
|
-
# Long explanation for the ``learner_downloadable`` field:
|
|
258
|
-
#
|
|
259
|
-
# Is this Content downloadable during the learning experience? This is
|
|
260
|
-
# NOT about public vs. private permissions on course assets, as that will be
|
|
261
|
-
# a policy that can be changed independently of new versions of the content.
|
|
262
|
-
# For instance, a course team could decide to flip their course assets from
|
|
263
|
-
# private to public for CDN caching reasons, and that should not require
|
|
264
|
-
# new ComponentVersions to be created.
|
|
265
|
-
#
|
|
266
|
-
# What the ``learner_downloadable`` field refers to is whether this asset is
|
|
267
|
-
# supposed to *ever* be directly downloadable by browsers during the
|
|
268
|
-
# learning experience. This will be True for things like images, PDFs, and
|
|
269
|
-
# video transcript files. This field will be False for things like:
|
|
270
|
-
#
|
|
271
|
-
# * Problem Block OLX will contain the answers to the problem. The XBlock
|
|
272
|
-
# runtime and ProblemBlock will use this information to generate HTML and
|
|
273
|
-
# grade responses, but the the user's browser is never permitted to
|
|
274
|
-
# actually download the raw OLX itself.
|
|
275
|
-
# * Many courses include a python_lib.zip file holding custom Python code
|
|
276
|
-
# to be used by codejail to assess student answers. This code will also
|
|
277
|
-
# potentially reveal answers, and is never intended to be downloadable by
|
|
278
|
-
# the student's browser.
|
|
279
|
-
# * Some course teams will upload other file formats that their OLX is
|
|
280
|
-
# derived from (e.g. specially formatted LaTeX files). These files will
|
|
281
|
-
# likewise contain answers and should never be downloadable by the
|
|
282
|
-
# student.
|
|
283
|
-
# * Other custom metadata may be attached as files in the import, such as
|
|
284
|
-
# custom identifiers, author information, etc.
|
|
285
|
-
#
|
|
286
|
-
# Even if ``learner_downloadble`` is True, the LMS may decide that this
|
|
287
|
-
# particular student isn't allowed to see this particular piece of content
|
|
288
|
-
# yet–e.g. because they are not enrolled, or because the exam this Component
|
|
289
|
-
# is a part of hasn't started yet. That's a matter of LMS permissions and
|
|
290
|
-
# policy that is not intrinsic to the content itself, and exists at a layer
|
|
291
|
-
# above this.
|
|
292
|
-
learner_downloadable = models.BooleanField(default=False)
|
|
293
|
-
|
|
294
257
|
class Meta:
|
|
295
258
|
constraints = [
|
|
296
259
|
# Uniqueness is only by ComponentVersion and key. If for some reason
|
|
@@ -50,6 +50,7 @@ __all__ = [
|
|
|
50
50
|
"soft_delete_draft",
|
|
51
51
|
"reset_drafts_to_published",
|
|
52
52
|
"register_content_models",
|
|
53
|
+
"filter_publishable_entities",
|
|
53
54
|
]
|
|
54
55
|
|
|
55
56
|
|
|
@@ -493,3 +494,22 @@ def register_content_models(
|
|
|
493
494
|
return PublishableContentModelRegistry.register(
|
|
494
495
|
content_model_cls, content_version_model_cls
|
|
495
496
|
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def filter_publishable_entities(
|
|
500
|
+
entities: QuerySet[PublishableEntity],
|
|
501
|
+
has_draft=None,
|
|
502
|
+
has_published=None
|
|
503
|
+
) -> QuerySet[PublishableEntity]:
|
|
504
|
+
"""
|
|
505
|
+
Filter an entities query set.
|
|
506
|
+
|
|
507
|
+
has_draft: You can filter by entities that has a draft or not.
|
|
508
|
+
has_published: You can filter by entities that has a published version or not.
|
|
509
|
+
"""
|
|
510
|
+
if has_draft is not None:
|
|
511
|
+
entities = entities.filter(draft__version__isnull=not has_draft)
|
|
512
|
+
if has_published is not None:
|
|
513
|
+
entities = entities.filter(published__version__isnull=not has_published)
|
|
514
|
+
|
|
515
|
+
return entities
|
|
@@ -5,7 +5,7 @@ Views for the media server application
|
|
|
5
5
|
"""
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
9
9
|
from django.http import FileResponse, Http404
|
|
10
10
|
|
|
11
11
|
from openedx_learning.apps.authoring.components.api import look_up_component_version_content
|
|
@@ -34,11 +34,6 @@ def component_asset(
|
|
|
34
34
|
except ObjectDoesNotExist:
|
|
35
35
|
raise Http404("File not found") # pylint: disable=raise-missing-from
|
|
36
36
|
|
|
37
|
-
if not cvc.learner_downloadable and not (
|
|
38
|
-
request.user and request.user.is_superuser
|
|
39
|
-
):
|
|
40
|
-
raise PermissionDenied("This file is not publicly downloadable.")
|
|
41
|
-
|
|
42
37
|
response = FileResponse(cvc.raw_content.file, filename=Path(asset_path).name)
|
|
43
38
|
response["Content-Type"] = cvc.raw_content.mime_type
|
|
44
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Open edX Learning Core and Tagging.
|
|
5
5
|
Home-page: https://github.com/openedx/openedx-learning
|
|
6
6
|
Author: David Ormsbee
|
|
@@ -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: edx-drf-extensions
|
|
22
|
-
Requires-Dist: Django<5.0
|
|
23
|
-
Requires-Dist: celery
|
|
24
|
-
Requires-Dist: djangorestframework<4.0
|
|
25
21
|
Requires-Dist: attrs
|
|
26
22
|
Requires-Dist: rules<4.0
|
|
23
|
+
Requires-Dist: Django<5.0
|
|
24
|
+
Requires-Dist: djangorestframework<4.0
|
|
25
|
+
Requires-Dist: edx-drf-extensions
|
|
26
|
+
Requires-Dist: celery
|
|
27
27
|
|
|
28
28
|
Open edX Learning Core (and Tagging)
|
|
29
29
|
====================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
openedx_learning/__init__.py,sha256=
|
|
1
|
+
openedx_learning/__init__.py,sha256=WQCJfrV8nUpwAaGx2NCUAw0d22YgqZqryjy0wIiV01M,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
|
|
@@ -17,15 +17,16 @@ openedx_learning/apps/authoring/collections/migrations/0004_collection_key.py,sh
|
|
|
17
17
|
openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py,sha256=HdU_3zxN32nzzvOFpiVpQXleHleJhnq2d8k7jAxhUTM,504
|
|
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
|
-
openedx_learning/apps/authoring/components/admin.py,sha256=
|
|
21
|
-
openedx_learning/apps/authoring/components/api.py,sha256=
|
|
20
|
+
openedx_learning/apps/authoring/components/admin.py,sha256=zfEpuBEySMYpUZzygaE2MDoI8SH-2H3xIL20YCSCMLo,4582
|
|
21
|
+
openedx_learning/apps/authoring/components/api.py,sha256=Kd8dscUAB5HWX84upmpfK-HjB_1olR7QBP7Ybxn0FVM,24894
|
|
22
22
|
openedx_learning/apps/authoring/components/apps.py,sha256=YoYPsI9gcleA3uEs8CiLIrjUncRMo2DKbYt4mDfzePg,770
|
|
23
|
-
openedx_learning/apps/authoring/components/models.py,sha256=
|
|
23
|
+
openedx_learning/apps/authoring/components/models.py,sha256=VX-pi4ZPJkCP3UtyTcVihgBdBj2pmenwdVC_xx8wCi0,11190
|
|
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
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
|
+
openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py,sha256=hDAkKdBvKULepML9pVMqkZg31nAyCeszQHJsFJ4qGws,382
|
|
29
30
|
openedx_learning/apps/authoring/components/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
31
|
openedx_learning/apps/authoring/contents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
32
|
openedx_learning/apps/authoring/contents/admin.py,sha256=9Njd_lje1emcd168KBWUTGf0mVJ6K-dMYMcqHNjRU4k,1761
|
|
@@ -36,7 +37,7 @@ openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTm
|
|
|
36
37
|
openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
openedx_learning/apps/authoring/publishing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
39
|
openedx_learning/apps/authoring/publishing/admin.py,sha256=F-0QlVQmuovLIF258XK_vKJdOnn7lLa_0A5veE72TKc,4830
|
|
39
|
-
openedx_learning/apps/authoring/publishing/api.py,sha256=
|
|
40
|
+
openedx_learning/apps/authoring/publishing/api.py,sha256=dawwRDbqbA_02xtPAwFIukWShrkO6MoQptmEKdAN7uA,17475
|
|
40
41
|
openedx_learning/apps/authoring/publishing/apps.py,sha256=jUfd78xvXaZg3dwkqXihatbeajJGm3Uz1rJpuyd-3g0,402
|
|
41
42
|
openedx_learning/apps/authoring/publishing/model_mixins.py,sha256=DGM6cZSNX4KZHy5xWtNy6yO4bRT04VDus8yZJ4v3vI0,14447
|
|
42
43
|
openedx_learning/apps/authoring/publishing/models.py,sha256=ImMAujPDc2-CECZw_yvVlUOdtGYwmt99TJ2r1HJkkV8,20488
|
|
@@ -47,7 +48,7 @@ openedx_learning/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
47
48
|
openedx_learning/contrib/media_server/__init__.py,sha256=iYijWFCl5RNR9omSu22kMl49EfponoqXBqXr0HMp4QI,56
|
|
48
49
|
openedx_learning/contrib/media_server/apps.py,sha256=FPT0rsUFtPyhFpWKjSI1e_s58wU0IbDyaAW_66V6sY4,816
|
|
49
50
|
openedx_learning/contrib/media_server/urls.py,sha256=newNjV41sM9A9Oy_rgnZSXdkTFxSHiupIiAsVIGE2CE,365
|
|
50
|
-
openedx_learning/contrib/media_server/views.py,sha256=
|
|
51
|
+
openedx_learning/contrib/media_server/views.py,sha256=qZPhdEW_oYj1MEdgLVP6Cq3tRiZtp7dTb7ASaSKZ2HY,1350
|
|
51
52
|
openedx_learning/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
53
|
openedx_learning/lib/admin_utils.py,sha256=5z9NrXxmT5j8azx9u1t0AgxV5PIDTc2jPyM5z5yW8cw,4021
|
|
53
54
|
openedx_learning/lib/cache.py,sha256=ppT36KiPLdsAF3GfZCF0IdiHodckd2gLiF1sNhjSJuk,958
|
|
@@ -111,8 +112,8 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=D7brBbgmU7MnbU7Ln
|
|
|
111
112
|
openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
|
|
112
113
|
openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=LA0EF7-p91JgVrLZpZaG474elKD1dswODGvoPIw47Mg,35837
|
|
113
114
|
openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
|
|
114
|
-
openedx_learning-0.
|
|
115
|
-
openedx_learning-0.
|
|
116
|
-
openedx_learning-0.
|
|
117
|
-
openedx_learning-0.
|
|
118
|
-
openedx_learning-0.
|
|
115
|
+
openedx_learning-0.18.0.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
|
|
116
|
+
openedx_learning-0.18.0.dist-info/METADATA,sha256=0TzunzFQsdhZ0vYpqBy0XCsW02pFP0mACyskAekcA4w,8777
|
|
117
|
+
openedx_learning-0.18.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
|
|
118
|
+
openedx_learning-0.18.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
|
|
119
|
+
openedx_learning-0.18.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|