learning-paths-plugin 0.2.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.
- learning_paths_plugin-0.2.3/CHANGELOG.rst +50 -0
- learning_paths_plugin-0.2.3/LICENSE.txt +1 -0
- learning_paths_plugin-0.2.3/MANIFEST.in +6 -0
- learning_paths_plugin-0.2.3/PKG-INFO +154 -0
- learning_paths_plugin-0.2.3/README.rst +65 -0
- learning_paths_plugin-0.2.3/learning_paths/__init__.py +5 -0
- learning_paths_plugin-0.2.3/learning_paths/admin.py +155 -0
- learning_paths_plugin-0.2.3/learning_paths/api/__init__.py +0 -0
- learning_paths_plugin-0.2.3/learning_paths/api/urls.py +7 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/__init__.py +0 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/filters.py +16 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/permissions.py +27 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/serializers.py +93 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/urls.py +46 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/utils.py +65 -0
- learning_paths_plugin-0.2.3/learning_paths/api/v1/views.py +341 -0
- learning_paths_plugin-0.2.3/learning_paths/apps.py +52 -0
- learning_paths_plugin-0.2.3/learning_paths/compat.py +48 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0001_initial.py +93 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0002_learningpath_uuid.py +19 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0003_learningpath_subtitle.py +18 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0004_auto_20240207_1633.py +36 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +29 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/0006_enrollment_models.py +66 -0
- learning_paths_plugin-0.2.3/learning_paths/migrations/__init__.py +0 -0
- learning_paths_plugin-0.2.3/learning_paths/models.py +283 -0
- learning_paths_plugin-0.2.3/learning_paths/receivers.py +52 -0
- learning_paths_plugin-0.2.3/learning_paths/settings.py +15 -0
- learning_paths_plugin-0.2.3/learning_paths/templates/learning_paths/base.html +26 -0
- learning_paths_plugin-0.2.3/learning_paths/urls.py +9 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/PKG-INFO +154 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/SOURCES.txt +43 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/dependency_links.txt +1 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/entry_points.txt +2 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/not-zip-safe +1 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/requires.txt +8 -0
- learning_paths_plugin-0.2.3/learning_paths_plugin.egg-info/top_level.txt +1 -0
- learning_paths_plugin-0.2.3/pyproject.toml +9 -0
- learning_paths_plugin-0.2.3/requirements/base.in +12 -0
- learning_paths_plugin-0.2.3/requirements/constraints.txt +17 -0
- learning_paths_plugin-0.2.3/setup.cfg +16 -0
- learning_paths_plugin-0.2.3/setup.py +181 -0
- learning_paths_plugin-0.2.3/tests/test_models.py +15 -0
- learning_paths_plugin-0.2.3/tests/test_receivers.py +78 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Change Log
|
|
2
|
+
##########
|
|
3
|
+
|
|
4
|
+
..
|
|
5
|
+
All enhancements and patches to learning_paths will be documented
|
|
6
|
+
in this file. It adheres to the structure of https://keepachangelog.com/ ,
|
|
7
|
+
but in reStructuredText instead of Markdown (for ease of incorporation into
|
|
8
|
+
Sphinx documentation and the PyPI description).
|
|
9
|
+
|
|
10
|
+
This project adheres to Semantic Versioning (https://semver.org/).
|
|
11
|
+
|
|
12
|
+
.. There should always be an "Unreleased" section for changes pending release.
|
|
13
|
+
|
|
14
|
+
Unreleased
|
|
15
|
+
**********
|
|
16
|
+
|
|
17
|
+
*
|
|
18
|
+
|
|
19
|
+
0.2.3 - 2025-03-31
|
|
20
|
+
******************
|
|
21
|
+
|
|
22
|
+
Added
|
|
23
|
+
=====
|
|
24
|
+
|
|
25
|
+
* Enrollment API.
|
|
26
|
+
|
|
27
|
+
0.2.2 - 2024-12-05
|
|
28
|
+
******************
|
|
29
|
+
|
|
30
|
+
Added
|
|
31
|
+
=====
|
|
32
|
+
|
|
33
|
+
* User grade API
|
|
34
|
+
|
|
35
|
+
0.2.1 - 2024-10-28
|
|
36
|
+
******************
|
|
37
|
+
|
|
38
|
+
Added
|
|
39
|
+
=====
|
|
40
|
+
|
|
41
|
+
* Pathway progress API
|
|
42
|
+
|
|
43
|
+
0.2.0 - 2024-01-23
|
|
44
|
+
******************
|
|
45
|
+
|
|
46
|
+
Added
|
|
47
|
+
=====
|
|
48
|
+
|
|
49
|
+
* Database models
|
|
50
|
+
* Django Admin interface
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: learning-paths-plugin
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Learning Paths plugin
|
|
5
|
+
Home-page: https://github.com/open-craft/learning-paths-plugin
|
|
6
|
+
Author: OpenCraft
|
|
7
|
+
Author-email: help@opencraft.com
|
|
8
|
+
Keywords: Python edx
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4.2
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
Requires-Dist: Django
|
|
20
|
+
Requires-Dist: django-model-utils
|
|
21
|
+
Requires-Dist: django-simple-history==3.4.0
|
|
22
|
+
Requires-Dist: djangorestframework
|
|
23
|
+
Requires-Dist: edx-django-utils
|
|
24
|
+
Requires-Dist: edx-opaque-keys
|
|
25
|
+
Requires-Dist: openedx-atlas
|
|
26
|
+
Requires-Dist: openedx-completion-aggregator
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
learning-paths-plugin
|
|
39
|
+
#####################
|
|
40
|
+
|
|
41
|
+
Purpose
|
|
42
|
+
*******
|
|
43
|
+
|
|
44
|
+
A Learning Path consists of a selection of courses bundled together for
|
|
45
|
+
learners to progress through. This plugin enables the creation and
|
|
46
|
+
management of Learning Paths.
|
|
47
|
+
|
|
48
|
+
License
|
|
49
|
+
*******
|
|
50
|
+
|
|
51
|
+
The code in this repository is licensed under the Not open source unless
|
|
52
|
+
otherwise noted.
|
|
53
|
+
|
|
54
|
+
Please see `LICENSE.txt <LICENSE.txt>`_ for details.
|
|
55
|
+
|
|
56
|
+
Installation and Configuration
|
|
57
|
+
******************************
|
|
58
|
+
|
|
59
|
+
1. **Clone the Repository**
|
|
60
|
+
|
|
61
|
+
Clone the repository containing the plugin to the `src` directory under your devstack root:
|
|
62
|
+
|
|
63
|
+
.. code-block:: bash
|
|
64
|
+
|
|
65
|
+
git clone <repository_url> <devstack_root>/src/learning-paths-plugin
|
|
66
|
+
|
|
67
|
+
2. **Install the Plugin**
|
|
68
|
+
|
|
69
|
+
Inside the LMS shell, install the plugin by running:
|
|
70
|
+
|
|
71
|
+
.. code-block:: bash
|
|
72
|
+
|
|
73
|
+
pip install -e /edx/src/learning-paths-plugin/
|
|
74
|
+
|
|
75
|
+
3. **Run Migrations for the Plugin**
|
|
76
|
+
|
|
77
|
+
After installing the plugin, run the database migrations for `learning_paths`:
|
|
78
|
+
|
|
79
|
+
.. code-block:: bash
|
|
80
|
+
|
|
81
|
+
./manage.py lms migrate learning_paths
|
|
82
|
+
|
|
83
|
+
4. **Run Completion Aggregator Migrations**
|
|
84
|
+
|
|
85
|
+
Ensure that the **completion aggregator** service is also up to date by running its migrations:
|
|
86
|
+
|
|
87
|
+
.. code-block:: bash
|
|
88
|
+
|
|
89
|
+
./manage.py lms migrate completion_aggregator
|
|
90
|
+
|
|
91
|
+
.. warning::
|
|
92
|
+
|
|
93
|
+
Please read the section about `synchronous vs asynchronous modes <https://github.com/open-craft/openedx-completion-aggregator/?tab=readme-ov-file#synchronous-vs-asynchronous-calculations>`_
|
|
94
|
+
for completion aggregator before enabling this in a production environment. Running in synchronous mode can lead to an outage.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
Once these steps are complete, the Learning Paths plugin should be successfully installed and ready to use.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
Usage
|
|
101
|
+
*****
|
|
102
|
+
After installing the plugin, a learning path can be created in the django admin panel `{LMS_URL}/admin/learning_paths/learningpath/`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Change Log
|
|
106
|
+
##########
|
|
107
|
+
|
|
108
|
+
..
|
|
109
|
+
All enhancements and patches to learning_paths will be documented
|
|
110
|
+
in this file. It adheres to the structure of https://keepachangelog.com/ ,
|
|
111
|
+
but in reStructuredText instead of Markdown (for ease of incorporation into
|
|
112
|
+
Sphinx documentation and the PyPI description).
|
|
113
|
+
|
|
114
|
+
This project adheres to Semantic Versioning (https://semver.org/).
|
|
115
|
+
|
|
116
|
+
.. There should always be an "Unreleased" section for changes pending release.
|
|
117
|
+
|
|
118
|
+
Unreleased
|
|
119
|
+
**********
|
|
120
|
+
|
|
121
|
+
*
|
|
122
|
+
|
|
123
|
+
0.2.3 - 2025-03-31
|
|
124
|
+
******************
|
|
125
|
+
|
|
126
|
+
Added
|
|
127
|
+
=====
|
|
128
|
+
|
|
129
|
+
* Enrollment API.
|
|
130
|
+
|
|
131
|
+
0.2.2 - 2024-12-05
|
|
132
|
+
******************
|
|
133
|
+
|
|
134
|
+
Added
|
|
135
|
+
=====
|
|
136
|
+
|
|
137
|
+
* User grade API
|
|
138
|
+
|
|
139
|
+
0.2.1 - 2024-10-28
|
|
140
|
+
******************
|
|
141
|
+
|
|
142
|
+
Added
|
|
143
|
+
=====
|
|
144
|
+
|
|
145
|
+
* Pathway progress API
|
|
146
|
+
|
|
147
|
+
0.2.0 - 2024-01-23
|
|
148
|
+
******************
|
|
149
|
+
|
|
150
|
+
Added
|
|
151
|
+
=====
|
|
152
|
+
|
|
153
|
+
* Database models
|
|
154
|
+
* Django Admin interface
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
learning-paths-plugin
|
|
2
|
+
#####################
|
|
3
|
+
|
|
4
|
+
Purpose
|
|
5
|
+
*******
|
|
6
|
+
|
|
7
|
+
A Learning Path consists of a selection of courses bundled together for
|
|
8
|
+
learners to progress through. This plugin enables the creation and
|
|
9
|
+
management of Learning Paths.
|
|
10
|
+
|
|
11
|
+
License
|
|
12
|
+
*******
|
|
13
|
+
|
|
14
|
+
The code in this repository is licensed under the Not open source unless
|
|
15
|
+
otherwise noted.
|
|
16
|
+
|
|
17
|
+
Please see `LICENSE.txt <LICENSE.txt>`_ for details.
|
|
18
|
+
|
|
19
|
+
Installation and Configuration
|
|
20
|
+
******************************
|
|
21
|
+
|
|
22
|
+
1. **Clone the Repository**
|
|
23
|
+
|
|
24
|
+
Clone the repository containing the plugin to the `src` directory under your devstack root:
|
|
25
|
+
|
|
26
|
+
.. code-block:: bash
|
|
27
|
+
|
|
28
|
+
git clone <repository_url> <devstack_root>/src/learning-paths-plugin
|
|
29
|
+
|
|
30
|
+
2. **Install the Plugin**
|
|
31
|
+
|
|
32
|
+
Inside the LMS shell, install the plugin by running:
|
|
33
|
+
|
|
34
|
+
.. code-block:: bash
|
|
35
|
+
|
|
36
|
+
pip install -e /edx/src/learning-paths-plugin/
|
|
37
|
+
|
|
38
|
+
3. **Run Migrations for the Plugin**
|
|
39
|
+
|
|
40
|
+
After installing the plugin, run the database migrations for `learning_paths`:
|
|
41
|
+
|
|
42
|
+
.. code-block:: bash
|
|
43
|
+
|
|
44
|
+
./manage.py lms migrate learning_paths
|
|
45
|
+
|
|
46
|
+
4. **Run Completion Aggregator Migrations**
|
|
47
|
+
|
|
48
|
+
Ensure that the **completion aggregator** service is also up to date by running its migrations:
|
|
49
|
+
|
|
50
|
+
.. code-block:: bash
|
|
51
|
+
|
|
52
|
+
./manage.py lms migrate completion_aggregator
|
|
53
|
+
|
|
54
|
+
.. warning::
|
|
55
|
+
|
|
56
|
+
Please read the section about `synchronous vs asynchronous modes <https://github.com/open-craft/openedx-completion-aggregator/?tab=readme-ov-file#synchronous-vs-asynchronous-calculations>`_
|
|
57
|
+
for completion aggregator before enabling this in a production environment. Running in synchronous mode can lead to an outage.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Once these steps are complete, the Learning Paths plugin should be successfully installed and ready to use.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Usage
|
|
64
|
+
*****
|
|
65
|
+
After installing the plugin, a learning path can be created in the django admin panel `{LMS_URL}/admin/learning_paths/learningpath/`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django Admin for learning_paths.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django import forms
|
|
6
|
+
from django.contrib import admin, auth
|
|
7
|
+
from django.core.exceptions import ValidationError
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
|
|
11
|
+
from .compat import get_course_keys_with_outlines
|
|
12
|
+
from .models import (
|
|
13
|
+
AcquiredSkill,
|
|
14
|
+
LearningPath,
|
|
15
|
+
LearningPathEnrollment,
|
|
16
|
+
LearningPathGradingCriteria,
|
|
17
|
+
LearningPathStep,
|
|
18
|
+
RequiredSkill,
|
|
19
|
+
Skill,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
User = auth.get_user_model()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_course_keys_choices():
|
|
26
|
+
"""Get course keys in an adequate format for a choice field."""
|
|
27
|
+
yield None, ""
|
|
28
|
+
for key in get_course_keys_with_outlines():
|
|
29
|
+
yield key, key
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LearningPathStepForm(forms.ModelForm):
|
|
33
|
+
"""Admin form for Learning Path step."""
|
|
34
|
+
|
|
35
|
+
# TODO: Use autocomplete select instead.
|
|
36
|
+
# See <https://github.com/open-craft/section-to-course/blob/db6fd6f8f4478e91bb531e6c2fa50143e1c2e012/
|
|
37
|
+
# section_to_course/admin.py#L31-L140>
|
|
38
|
+
course_key = forms.ChoiceField(choices=get_course_keys_choices, label=_("Course"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LearningPathStepInline(admin.TabularInline):
|
|
42
|
+
"""Inline Admin for Learning Path step."""
|
|
43
|
+
|
|
44
|
+
model = LearningPathStep
|
|
45
|
+
form = LearningPathStepForm
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AcquiredSkillInline(admin.TabularInline):
|
|
49
|
+
"""Inline Admin for Learning Path acquired skill."""
|
|
50
|
+
|
|
51
|
+
model = AcquiredSkill
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RequiredSkillInline(admin.TabularInline):
|
|
55
|
+
"""Inline Admin for Learning Path required skill."""
|
|
56
|
+
|
|
57
|
+
model = RequiredSkill
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LearningPathGradingCriteriaInline(admin.TabularInline):
|
|
61
|
+
"""Inline Admin for Learning path grading criteria."""
|
|
62
|
+
|
|
63
|
+
model = LearningPathGradingCriteria
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BulkEnrollUsersForm(forms.ModelForm):
|
|
67
|
+
"""Form to bulk enroll users in a learning path."""
|
|
68
|
+
|
|
69
|
+
usernames = forms.CharField(
|
|
70
|
+
widget=forms.Textarea,
|
|
71
|
+
help_text="Enter usernames separated by newlines",
|
|
72
|
+
label="Bulk enroll users",
|
|
73
|
+
required=False,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
class Meta:
|
|
77
|
+
"""Form options."""
|
|
78
|
+
|
|
79
|
+
model = LearningPath
|
|
80
|
+
fields = "__all__"
|
|
81
|
+
|
|
82
|
+
def clean_usernames(self):
|
|
83
|
+
"""Validate usernames and return a list of users."""
|
|
84
|
+
data = self.cleaned_data["usernames"]
|
|
85
|
+
if not data:
|
|
86
|
+
return []
|
|
87
|
+
usernames = [username.strip() for username in data.split("\n")]
|
|
88
|
+
users = User.objects.filter(username__in=usernames)
|
|
89
|
+
found_usernames = list(users.values_list("username", flat=True))
|
|
90
|
+
invalid_usernames = set(usernames) - set(found_usernames)
|
|
91
|
+
if invalid_usernames:
|
|
92
|
+
raise ValidationError(
|
|
93
|
+
f"The following usernames are not valid: {', '.join(invalid_usernames)}"
|
|
94
|
+
)
|
|
95
|
+
return users
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LearningPathAdmin(admin.ModelAdmin):
|
|
99
|
+
"""Admin for Learning Path."""
|
|
100
|
+
|
|
101
|
+
model = LearningPath
|
|
102
|
+
form = BulkEnrollUsersForm
|
|
103
|
+
|
|
104
|
+
search_fields = [
|
|
105
|
+
"slug",
|
|
106
|
+
"display_name",
|
|
107
|
+
]
|
|
108
|
+
list_display = (
|
|
109
|
+
"uuid",
|
|
110
|
+
"slug",
|
|
111
|
+
"display_name",
|
|
112
|
+
"level",
|
|
113
|
+
"duration_in_days",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
inlines = [
|
|
117
|
+
LearningPathStepInline,
|
|
118
|
+
RequiredSkillInline,
|
|
119
|
+
AcquiredSkillInline,
|
|
120
|
+
LearningPathGradingCriteriaInline,
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def save_related(self, request, form, formsets, change):
|
|
124
|
+
"""Save related objects and enroll users in the learning path."""
|
|
125
|
+
super().save_related(request, form, formsets, change)
|
|
126
|
+
with transaction.atomic():
|
|
127
|
+
for user in form.cleaned_data["usernames"]:
|
|
128
|
+
LearningPathEnrollment.objects.get_or_create(
|
|
129
|
+
user=user, learning_path=form.instance
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SkillAdmin(admin.ModelAdmin):
|
|
134
|
+
"""Admin for Learning Path generic skill."""
|
|
135
|
+
|
|
136
|
+
model = Skill
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class EnrolledUsersAdmin(admin.ModelAdmin):
|
|
140
|
+
"""Admin for Learning Path enrollment."""
|
|
141
|
+
|
|
142
|
+
model = LearningPathEnrollment
|
|
143
|
+
|
|
144
|
+
search_fields = [
|
|
145
|
+
"id",
|
|
146
|
+
"user__username",
|
|
147
|
+
"learning_path__uuid",
|
|
148
|
+
"learning_path__slug",
|
|
149
|
+
"learning_path__display_name",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
admin.site.register(LearningPath, LearningPathAdmin)
|
|
154
|
+
admin.site.register(Skill, SkillAdmin)
|
|
155
|
+
admin.site.register(LearningPathEnrollment, EnrolledUsersAdmin)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django REST framework filters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework.filters import BaseFilterBackend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdminOrSelfFilterBackend(BaseFilterBackend):
|
|
9
|
+
"""
|
|
10
|
+
A filter backend that limits the queryset to the current user for non-staff.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def filter_queryset(self, request, queryset, view):
|
|
14
|
+
if request.user.is_staff:
|
|
15
|
+
return queryset
|
|
16
|
+
return queryset.filter(user=request.user)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django REST framework permissions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework.permissions import BasePermission
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IsAdminOrSelf(BasePermission):
|
|
9
|
+
"""
|
|
10
|
+
Permission to allow only admins or the user themselves to access the API.
|
|
11
|
+
|
|
12
|
+
Non-staff users cannot pass "username" that is not their own.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def has_permission(self, request, view):
|
|
16
|
+
if request.user.is_staff:
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
if request.method == "GET":
|
|
20
|
+
username = request.query_params.get("username")
|
|
21
|
+
else:
|
|
22
|
+
username = request.data.get("username")
|
|
23
|
+
|
|
24
|
+
# For learners, the username passed should match the logged in user
|
|
25
|
+
if username:
|
|
26
|
+
return request.user.username == username
|
|
27
|
+
return True
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serializer for LearningPath.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
from learning_paths.models import LearningPath, LearningPathEnrollment
|
|
8
|
+
|
|
9
|
+
DEFAULT_STATUS = "active"
|
|
10
|
+
IMAGE_WIDTH = 1440
|
|
11
|
+
IMAGE_HEIGHT = 480
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LearningPathAsProgramSerializer(serializers.ModelSerializer):
|
|
15
|
+
"""
|
|
16
|
+
Serialize LearningPath as a Program to be ingested by course-discovery.
|
|
17
|
+
|
|
18
|
+
Mocked data example:
|
|
19
|
+
https://github.com/openedx/course-discovery/blob/d6a57fd69479b3d5f5afb682d2668b58503a6af6/course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py#L580
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name = serializers.CharField(source="display_name")
|
|
23
|
+
marketing_slug = serializers.SerializerMethodField()
|
|
24
|
+
title = serializers.CharField(source="display_name")
|
|
25
|
+
status = serializers.SerializerMethodField()
|
|
26
|
+
banner_image_urls = serializers.SerializerMethodField()
|
|
27
|
+
organizations = serializers.SerializerMethodField()
|
|
28
|
+
course_codes = serializers.SerializerMethodField()
|
|
29
|
+
|
|
30
|
+
def get_marketing_slug(self, obj):
|
|
31
|
+
return obj.slug
|
|
32
|
+
|
|
33
|
+
def get_status(self, obj): # pylint: disable=unused-argument
|
|
34
|
+
return DEFAULT_STATUS
|
|
35
|
+
|
|
36
|
+
def get_banner_image_urls(self, obj):
|
|
37
|
+
if obj.image_url:
|
|
38
|
+
image_key = f"w{IMAGE_WIDTH}h{IMAGE_HEIGHT}"
|
|
39
|
+
return {image_key: obj.image_url}
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def get_organizations(self, obj): # pylint: disable=unused-argument
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
def get_course_codes(self, obj):
|
|
46
|
+
"""returns course_codes as expected by course-discovery"""
|
|
47
|
+
course_codes_dict = {}
|
|
48
|
+
learning_path_course_keys = [course.course_key for course in obj.steps.all()]
|
|
49
|
+
for course_key in learning_path_course_keys:
|
|
50
|
+
run_mode = {"course_key": str(course_key), "run_key": course_key.run}
|
|
51
|
+
if course_key.course in course_codes_dict:
|
|
52
|
+
course_codes_dict[course_key.course]["run_modes"].append(run_mode)
|
|
53
|
+
else:
|
|
54
|
+
course_codes_dict[course_key.course] = {"run_modes": [run_mode]}
|
|
55
|
+
|
|
56
|
+
return [{"key": key, **value} for key, value in course_codes_dict.items()]
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
model = LearningPath
|
|
60
|
+
fields = (
|
|
61
|
+
"uuid",
|
|
62
|
+
"name",
|
|
63
|
+
"marketing_slug",
|
|
64
|
+
"title",
|
|
65
|
+
"subtitle",
|
|
66
|
+
"status",
|
|
67
|
+
"banner_image_urls",
|
|
68
|
+
"organizations",
|
|
69
|
+
"course_codes",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# pylint: disable=abstract-method
|
|
74
|
+
class LearningPathProgressSerializer(serializers.Serializer):
|
|
75
|
+
learning_path_id = serializers.UUIDField()
|
|
76
|
+
progress = serializers.FloatField()
|
|
77
|
+
required_completion = serializers.FloatField()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LearningPathGradeSerializer(serializers.Serializer):
|
|
81
|
+
"""
|
|
82
|
+
Serializer for learning path grade.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
learning_path_id = serializers.UUIDField()
|
|
86
|
+
grade = serializers.FloatField()
|
|
87
|
+
required_grade = serializers.FloatField()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LearningPathEnrollmentSerializer(serializers.ModelSerializer):
|
|
91
|
+
class Meta:
|
|
92
|
+
model = LearningPathEnrollment
|
|
93
|
+
fields = ("user", "learning_path", "is_active", "enrolled_at")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""API v1 URLs."""
|
|
2
|
+
|
|
3
|
+
from django.urls import path
|
|
4
|
+
from rest_framework import routers
|
|
5
|
+
|
|
6
|
+
from learning_paths.api.v1.views import (
|
|
7
|
+
BulkEnrollView,
|
|
8
|
+
LearningPathAsProgramViewSet,
|
|
9
|
+
LearningPathEnrollmentView,
|
|
10
|
+
LearningPathUserGradeView,
|
|
11
|
+
LearningPathUserProgressView,
|
|
12
|
+
ListEnrollmentsView,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
router = routers.SimpleRouter()
|
|
16
|
+
router.register(
|
|
17
|
+
r"programs", LearningPathAsProgramViewSet, basename="learning-path-as-program"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
urlpatterns = router.urls + [
|
|
21
|
+
path(
|
|
22
|
+
"<uuid:learning_path_uuid>/progress/",
|
|
23
|
+
LearningPathUserProgressView.as_view(),
|
|
24
|
+
name="learning-path-progress",
|
|
25
|
+
),
|
|
26
|
+
path(
|
|
27
|
+
"<uuid:learning_path_uuid>/grade/",
|
|
28
|
+
LearningPathUserGradeView.as_view(),
|
|
29
|
+
name="learning-path-grade",
|
|
30
|
+
),
|
|
31
|
+
path(
|
|
32
|
+
"<uuid:learning_path_id>/enrollments/",
|
|
33
|
+
LearningPathEnrollmentView.as_view(),
|
|
34
|
+
name="learning-path-enrollments",
|
|
35
|
+
),
|
|
36
|
+
path(
|
|
37
|
+
"enrollments/",
|
|
38
|
+
ListEnrollmentsView.as_view(),
|
|
39
|
+
name="list-enrollments",
|
|
40
|
+
),
|
|
41
|
+
path(
|
|
42
|
+
"enrollments/bulk-enroll/",
|
|
43
|
+
BulkEnrollView.as_view(),
|
|
44
|
+
name="bulk-enroll",
|
|
45
|
+
),
|
|
46
|
+
]
|