learning-paths-plugin 0.2.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.
- learning_paths/__init__.py +5 -0
- learning_paths/admin.py +155 -0
- learning_paths/api/__init__.py +0 -0
- learning_paths/api/urls.py +7 -0
- learning_paths/api/v1/__init__.py +0 -0
- learning_paths/api/v1/filters.py +16 -0
- learning_paths/api/v1/permissions.py +27 -0
- learning_paths/api/v1/serializers.py +93 -0
- learning_paths/api/v1/urls.py +46 -0
- learning_paths/api/v1/utils.py +65 -0
- learning_paths/api/v1/views.py +341 -0
- learning_paths/apps.py +52 -0
- learning_paths/compat.py +48 -0
- learning_paths/migrations/0001_initial.py +93 -0
- learning_paths/migrations/0002_learningpath_uuid.py +19 -0
- learning_paths/migrations/0003_learningpath_subtitle.py +18 -0
- learning_paths/migrations/0004_auto_20240207_1633.py +36 -0
- learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +29 -0
- learning_paths/migrations/0006_enrollment_models.py +66 -0
- learning_paths/migrations/__init__.py +0 -0
- learning_paths/models.py +283 -0
- learning_paths/receivers.py +52 -0
- learning_paths/settings.py +15 -0
- learning_paths/templates/learning_paths/base.html +26 -0
- learning_paths/urls.py +9 -0
- learning_paths_plugin-0.2.3.dist-info/METADATA +154 -0
- learning_paths_plugin-0.2.3.dist-info/RECORD +31 -0
- learning_paths_plugin-0.2.3.dist-info/WHEEL +6 -0
- learning_paths_plugin-0.2.3.dist-info/entry_points.txt +2 -0
- learning_paths_plugin-0.2.3.dist-info/licenses/LICENSE.txt +1 -0
- learning_paths_plugin-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Views for LearningPath.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.contrib.auth import get_user_model
|
|
11
|
+
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
|
|
12
|
+
from django.core.validators import validate_email
|
|
13
|
+
from django.shortcuts import get_object_or_404
|
|
14
|
+
from rest_framework import generics, status, viewsets
|
|
15
|
+
from rest_framework.pagination import PageNumberPagination
|
|
16
|
+
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
|
17
|
+
from rest_framework.response import Response
|
|
18
|
+
from rest_framework.views import APIView
|
|
19
|
+
|
|
20
|
+
from learning_paths.api.v1.serializers import (
|
|
21
|
+
LearningPathAsProgramSerializer,
|
|
22
|
+
LearningPathEnrollmentSerializer,
|
|
23
|
+
LearningPathGradeSerializer,
|
|
24
|
+
LearningPathProgressSerializer,
|
|
25
|
+
)
|
|
26
|
+
from learning_paths.models import (
|
|
27
|
+
LearningPath,
|
|
28
|
+
LearningPathEnrollment,
|
|
29
|
+
LearningPathEnrollmentAllowed,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .filters import AdminOrSelfFilterBackend
|
|
33
|
+
from .permissions import IsAdminOrSelf
|
|
34
|
+
from .utils import get_aggregate_progress
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
User = get_user_model()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LearningPathAsProgramViewSet(viewsets.ReadOnlyModelViewSet):
|
|
42
|
+
"""
|
|
43
|
+
This viewset exposes LearningPaths as Programs to be ingested
|
|
44
|
+
by the course-discovery's refresh_course_metadata command.
|
|
45
|
+
URL is: GET <LMS_URL>/api/v1/programs
|
|
46
|
+
The command makes use of the ProgramsApiDataLoader.
|
|
47
|
+
https://github.com/openedx/course-discovery/blob/d6a57fd69479b3d5f5afb682d2668b58503a6af6/course_discovery/apps/course_metadata/data_loaders/api.py#L843
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
queryset = LearningPath.objects.all()
|
|
51
|
+
permission_classes = (IsAuthenticated,)
|
|
52
|
+
serializer_class = LearningPathAsProgramSerializer
|
|
53
|
+
pagination_class = PageNumberPagination
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LearningPathUserProgressView(APIView):
|
|
57
|
+
"""
|
|
58
|
+
API view to return the aggregate progress of a user in a learning path.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
permission_classes = (IsAuthenticated,)
|
|
62
|
+
|
|
63
|
+
def get(self, request, learning_path_uuid):
|
|
64
|
+
"""
|
|
65
|
+
Fetch the learning path progress
|
|
66
|
+
"""
|
|
67
|
+
learning_path = get_object_or_404(LearningPath, uuid=learning_path_uuid)
|
|
68
|
+
|
|
69
|
+
progress = get_aggregate_progress(request.user, learning_path)
|
|
70
|
+
required_completion = None
|
|
71
|
+
try:
|
|
72
|
+
grading_criteria = learning_path.grading_criteria
|
|
73
|
+
required_completion = grading_criteria.required_completion
|
|
74
|
+
except ObjectDoesNotExist:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
data = {
|
|
78
|
+
"learning_path_id": learning_path.uuid,
|
|
79
|
+
"progress": progress,
|
|
80
|
+
"required_completion": required_completion,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
serializer = LearningPathProgressSerializer(data=data)
|
|
84
|
+
if serializer.is_valid():
|
|
85
|
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
86
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LearningPathUserGradeView(APIView):
|
|
90
|
+
"""
|
|
91
|
+
API view to return the aggregate grade of a user in a learning path.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
permission_classes = (IsAuthenticated,)
|
|
95
|
+
|
|
96
|
+
def get(self, request, learning_path_uuid):
|
|
97
|
+
"""
|
|
98
|
+
Fetch learning path grade
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
learning_path = get_object_or_404(LearningPath, uuid=learning_path_uuid)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
grading_criteria = learning_path.grading_criteria
|
|
105
|
+
except ObjectDoesNotExist:
|
|
106
|
+
return Response(
|
|
107
|
+
{"detail": "Grading criteria not found for this learning path."},
|
|
108
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
grade = grading_criteria.calculate_grade(request.user)
|
|
112
|
+
|
|
113
|
+
data = {
|
|
114
|
+
"learning_path_id": learning_path_uuid,
|
|
115
|
+
"grade": grade,
|
|
116
|
+
"required_grade": grading_criteria.required_grade,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
serializer = LearningPathGradeSerializer(data=data)
|
|
120
|
+
if serializer.is_valid():
|
|
121
|
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
122
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class LearningPathEnrollmentView(APIView):
|
|
126
|
+
"""
|
|
127
|
+
API View to handle changes to LearningPathEnrollment model
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
permission_classes = [IsAuthenticated, IsAdminOrSelf]
|
|
131
|
+
|
|
132
|
+
def get(self, request, learning_path_id):
|
|
133
|
+
"""Get the learning path of users.
|
|
134
|
+
|
|
135
|
+
Staff/Admin can get all the active enrollments of the learning path.
|
|
136
|
+
Learners can get their enrollments only.
|
|
137
|
+
|
|
138
|
+
Query params:
|
|
139
|
+
username (optional): When provided it returns the enrollment for
|
|
140
|
+
the specified user.
|
|
141
|
+
"""
|
|
142
|
+
learning_path = get_object_or_404(LearningPath, uuid=learning_path_id)
|
|
143
|
+
|
|
144
|
+
enrollments = LearningPathEnrollment.objects.filter(
|
|
145
|
+
learning_path=learning_path, is_active=True
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if request.user.is_staff:
|
|
149
|
+
if username := request.query_params.get("username"):
|
|
150
|
+
enrollments = enrollments.filter(user__username=username)
|
|
151
|
+
else:
|
|
152
|
+
enrollments = enrollments.filter(user=request.user)
|
|
153
|
+
|
|
154
|
+
serializer = LearningPathEnrollmentSerializer(enrollments.all(), many=True)
|
|
155
|
+
return Response(serializer.data)
|
|
156
|
+
|
|
157
|
+
def post(self, request, learning_path_id):
|
|
158
|
+
"""Enroll learners in Learning Paths.
|
|
159
|
+
|
|
160
|
+
Staff/Admin can enroll anyone with the username query param.
|
|
161
|
+
Learners can enroll only themselves.
|
|
162
|
+
|
|
163
|
+
Example payload::
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
"username": "user_1"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
learning_path = get_object_or_404(LearningPath, uuid=learning_path_id)
|
|
171
|
+
username = request.data.get("username")
|
|
172
|
+
user = get_object_or_404(User, username=username) if username else request.user
|
|
173
|
+
|
|
174
|
+
enrollment, created = LearningPathEnrollment.objects.get_or_create(
|
|
175
|
+
learning_path=learning_path, user=user
|
|
176
|
+
)
|
|
177
|
+
if created:
|
|
178
|
+
return Response(
|
|
179
|
+
LearningPathEnrollmentSerializer(enrollment).data,
|
|
180
|
+
status=status.HTTP_201_CREATED,
|
|
181
|
+
)
|
|
182
|
+
if enrollment.is_active:
|
|
183
|
+
return Response(
|
|
184
|
+
{"detail": "Enrollment exists."}, status=status.HTTP_409_CONFLICT
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
enrollment.is_active = True
|
|
188
|
+
enrollment.enrolled_at = datetime.now(timezone.utc)
|
|
189
|
+
enrollment.save()
|
|
190
|
+
return Response(LearningPathEnrollmentSerializer(enrollment).data)
|
|
191
|
+
|
|
192
|
+
def delete(self, request, learning_path_id):
|
|
193
|
+
"""
|
|
194
|
+
Unenroll a learner from a learning path.
|
|
195
|
+
|
|
196
|
+
Staff/admin can unenroll anyone with the username query param.
|
|
197
|
+
Learners can self-unenroll if settings.LEARNING_PATHS_ALLOW_SELF_UNENROLLMENT is True.
|
|
198
|
+
|
|
199
|
+
Example payload::
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
"username": "user_1"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
learning_path = get_object_or_404(LearningPath, uuid=learning_path_id)
|
|
207
|
+
username = request.data.get("username")
|
|
208
|
+
user = get_object_or_404(User, username=username) if username else request.user
|
|
209
|
+
|
|
210
|
+
enrollment = get_object_or_404(
|
|
211
|
+
LearningPathEnrollment,
|
|
212
|
+
learning_path=learning_path,
|
|
213
|
+
is_active=True,
|
|
214
|
+
user=user,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
not request.user.is_staff
|
|
219
|
+
and not settings.LEARNING_PATHS_ALLOW_SELF_UNENROLLMENT
|
|
220
|
+
):
|
|
221
|
+
raise PermissionDenied
|
|
222
|
+
|
|
223
|
+
enrollment.is_active = False
|
|
224
|
+
enrollment.save()
|
|
225
|
+
return Response(
|
|
226
|
+
LearningPathEnrollmentSerializer(enrollment).data,
|
|
227
|
+
status=status.HTTP_204_NO_CONTENT,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ListEnrollmentsView(generics.ListAPIView):
|
|
232
|
+
"""
|
|
233
|
+
List Learning Path Enrollments.
|
|
234
|
+
|
|
235
|
+
For staff, this returns enrollments from all learning paths for all users.
|
|
236
|
+
For non-staff, this returns all enrollments for the current user.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
permission_classes = [IsAuthenticated]
|
|
240
|
+
queryset = LearningPathEnrollment.objects.all()
|
|
241
|
+
serializer_class = LearningPathEnrollmentSerializer
|
|
242
|
+
filter_backends = [AdminOrSelfFilterBackend]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class BulkEnrollView(APIView):
|
|
246
|
+
"""
|
|
247
|
+
Bulk enrollment API for LearningPathEnrollment.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
permission_classes = [IsAdminUser]
|
|
252
|
+
|
|
253
|
+
def post(self, request, *args, **kwargs):
|
|
254
|
+
"""
|
|
255
|
+
Bulk Enroll learners in Learning Paths.
|
|
256
|
+
|
|
257
|
+
The "bulk enroll" API provides a way for the staff to enroll multiple learners
|
|
258
|
+
in multiple learning paths at once.
|
|
259
|
+
|
|
260
|
+
Example payload::
|
|
261
|
+
|
|
262
|
+
{
|
|
263
|
+
"learning_paths": "learning_path_1,learning_path_2",
|
|
264
|
+
"emails": "user_1@example.com,user_2@example.com"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
`learning_paths` (str): A comma separated list of learning path IDs.
|
|
268
|
+
`emails` (str): A comma separated list of email addresses.
|
|
269
|
+
|
|
270
|
+
* For existing users, it creates a new LearningPathEnrollment record, automatically
|
|
271
|
+
enrolling them in the learning path. It also creates a LearningPathAllowed record
|
|
272
|
+
to store the meta-data for audit later.
|
|
273
|
+
* For non-existing users, it creates a new LearningPathEnrollmentAllowed record
|
|
274
|
+
with just the email address, allowing them to get enrolled when they register.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
data = request.data
|
|
278
|
+
learning_paths_keys = data.get("learning_paths", "").split(",")
|
|
279
|
+
emails = data.get("emails", "").split(",")
|
|
280
|
+
|
|
281
|
+
valid_learning_paths_keys = []
|
|
282
|
+
for key in learning_paths_keys:
|
|
283
|
+
try:
|
|
284
|
+
UUID(key)
|
|
285
|
+
except ValueError:
|
|
286
|
+
logger.warning("BulkEnrollView: Invalid learning path key: %s", key)
|
|
287
|
+
continue
|
|
288
|
+
valid_learning_paths_keys.append(key)
|
|
289
|
+
|
|
290
|
+
learning_paths = LearningPath.objects.filter(
|
|
291
|
+
uuid__in=valid_learning_paths_keys
|
|
292
|
+
).all()
|
|
293
|
+
|
|
294
|
+
existing_users = User.objects.filter(email__in=emails).all()
|
|
295
|
+
non_existing_emails = set(emails) - set(u.email for u in existing_users)
|
|
296
|
+
|
|
297
|
+
enrollments_created = []
|
|
298
|
+
enrollment_allowed_created = []
|
|
299
|
+
|
|
300
|
+
for learning_path in learning_paths:
|
|
301
|
+
|
|
302
|
+
# Create LearningPathEnrollment for existing users
|
|
303
|
+
for user in existing_users:
|
|
304
|
+
enrollment = LearningPathEnrollment.objects.filter(
|
|
305
|
+
user=user, learning_path=learning_path
|
|
306
|
+
).first()
|
|
307
|
+
enrolled_now = False
|
|
308
|
+
if not enrollment:
|
|
309
|
+
enrollment = LearningPathEnrollment(
|
|
310
|
+
user=user,
|
|
311
|
+
learning_path=learning_path,
|
|
312
|
+
)
|
|
313
|
+
enrolled_now = True
|
|
314
|
+
if not enrollment.is_active:
|
|
315
|
+
enrollment.is_active = True
|
|
316
|
+
enrollment.enrolled_at = datetime.now(timezone.utc)
|
|
317
|
+
enrolled_now = True
|
|
318
|
+
enrollment.save()
|
|
319
|
+
if enrolled_now:
|
|
320
|
+
enrollments_created.append(enrollment)
|
|
321
|
+
|
|
322
|
+
# Create LearningPathEnrollmentAllowed for non-existing users
|
|
323
|
+
for email in non_existing_emails:
|
|
324
|
+
try:
|
|
325
|
+
validate_email(email)
|
|
326
|
+
except ValidationError:
|
|
327
|
+
logger.warning("BulkEnrollView: Invalid email: %s", email)
|
|
328
|
+
continue
|
|
329
|
+
allowed, created = LearningPathEnrollmentAllowed.objects.get_or_create(
|
|
330
|
+
email=email, learning_path=learning_path
|
|
331
|
+
)
|
|
332
|
+
if created:
|
|
333
|
+
enrollment_allowed_created.append(allowed)
|
|
334
|
+
|
|
335
|
+
return Response(
|
|
336
|
+
{
|
|
337
|
+
"enrollments_created": len(enrollments_created),
|
|
338
|
+
"enrollment_allowed_created": len(enrollment_allowed_created),
|
|
339
|
+
},
|
|
340
|
+
status=status.HTTP_201_CREATED,
|
|
341
|
+
)
|
learning_paths/apps.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
learning_paths Django application initialization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from edx_django_utils.plugins.constants import PluginSettings, PluginSignals, PluginURLs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LearningPathsConfig(AppConfig):
|
|
10
|
+
"""
|
|
11
|
+
Configuration for the learning_paths Django application.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name = "learning_paths"
|
|
15
|
+
|
|
16
|
+
plugin_app = {
|
|
17
|
+
# Configuration setting for Plugin URLs for this app.
|
|
18
|
+
PluginURLs.CONFIG: {
|
|
19
|
+
"lms.djangoapp": {
|
|
20
|
+
# The namespace to provide to django's urls.include.
|
|
21
|
+
PluginURLs.NAMESPACE: "learning_paths",
|
|
22
|
+
# The application namespace to provide to django's urls.include.
|
|
23
|
+
# Optional; Defaults to None.
|
|
24
|
+
PluginURLs.APP_NAME: "learning_paths",
|
|
25
|
+
# The regex to provide to django's urls.url.
|
|
26
|
+
# Optional; Defaults to r''.
|
|
27
|
+
# PluginURLs.REGEX: r'^api/learning_paths/',
|
|
28
|
+
# The python path (relative to this app) to the URLs module to be plugged into the project.
|
|
29
|
+
# Optional; Defaults to 'urls'.
|
|
30
|
+
# PluginURLs.RELATIVE_PATH: 'api.urls',
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
PluginSettings.CONFIG: {
|
|
34
|
+
"lms.djangoapp": {
|
|
35
|
+
"common": {
|
|
36
|
+
PluginSettings.RELATIVE_PATH: "settings",
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
PluginSignals.CONFIG: {
|
|
41
|
+
"lms.djangoapp": {
|
|
42
|
+
PluginSignals.RELATIVE_PATH: "receivers",
|
|
43
|
+
PluginSignals.RECEIVERS: [
|
|
44
|
+
{
|
|
45
|
+
PluginSignals.RECEIVER_FUNC_NAME: "process_pending_enrollments",
|
|
46
|
+
PluginSignals.SIGNAL_PATH: "django.db.models.signals.post_save",
|
|
47
|
+
PluginSignals.SENDER_PATH: "django.contrib.auth.models.User",
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
learning_paths/compat.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compatibility layer for testing without Open edX.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth.models import AbstractBaseUser
|
|
6
|
+
from opaque_keys.edx.keys import CourseKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_user_course_grade(user: AbstractBaseUser, course_key: CourseKey):
|
|
10
|
+
"""
|
|
11
|
+
Retrieve the CourseGrade object for a user in a specific course.
|
|
12
|
+
"""
|
|
13
|
+
# pylint: disable=import-outside-toplevel, import-error
|
|
14
|
+
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
|
15
|
+
|
|
16
|
+
course_grade = CourseGradeFactory().read(user, course_key=course_key)
|
|
17
|
+
return course_grade
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_catalog_api_client(user: AbstractBaseUser):
|
|
21
|
+
"""
|
|
22
|
+
Retrieve the api client for user.
|
|
23
|
+
"""
|
|
24
|
+
# pylint: disable=import-outside-toplevel, import-error
|
|
25
|
+
from openedx.core.djangoapps.catalog.utils import (
|
|
26
|
+
get_catalog_api_client as api_client,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return api_client(user)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_course_keys_with_outlines():
|
|
33
|
+
"""
|
|
34
|
+
Retrieve course keys.
|
|
35
|
+
"""
|
|
36
|
+
# pylint: disable=import-outside-toplevel, import-error
|
|
37
|
+
from openedx.core.djangoapps.content.learning_sequences.api import (
|
|
38
|
+
get_course_keys_with_outlines as course_keys_with_outlines,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return course_keys_with_outlines()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"get_course_keys_with_outlines",
|
|
46
|
+
"get_catalog_api_client",
|
|
47
|
+
"get_user_course_grade",
|
|
48
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Generated by Django 3.2.23 on 2024-01-23 12:16
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import django.utils.timezone
|
|
6
|
+
import model_utils.fields
|
|
7
|
+
import opaque_keys.edx.django.models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
initial = True
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='LearningPath',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
23
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
24
|
+
('slug', models.SlugField(help_text='Custom unique code identifying this Learning Path.', unique=True)),
|
|
25
|
+
('display_name', models.CharField(max_length=255)),
|
|
26
|
+
('description', models.TextField(blank=True)),
|
|
27
|
+
('image_url', models.CharField(blank=True, help_text='URL to an image representing this Learning Path.', max_length=200, verbose_name='Image URL')),
|
|
28
|
+
('level', models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advanced', 'Advanced')], max_length=255)),
|
|
29
|
+
('duration_in_days', models.PositiveIntegerField(blank=True, help_text='Approximate time (in days) it should take to complete this Learning Path.', null=True, verbose_name='Duration (days)')),
|
|
30
|
+
('sequential', models.BooleanField(help_text='Whether the courses in this Learning Path are meant to be taken sequentially.', verbose_name='Is sequential')),
|
|
31
|
+
],
|
|
32
|
+
options={
|
|
33
|
+
'abstract': False,
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
migrations.CreateModel(
|
|
37
|
+
name='Skill',
|
|
38
|
+
fields=[
|
|
39
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
40
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
41
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
42
|
+
('display_name', models.CharField(max_length=255)),
|
|
43
|
+
],
|
|
44
|
+
options={
|
|
45
|
+
'abstract': False,
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
migrations.CreateModel(
|
|
49
|
+
name='RequiredSkill',
|
|
50
|
+
fields=[
|
|
51
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
52
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
53
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
54
|
+
('level', models.PositiveIntegerField(help_text='The skill level associated with this course.')),
|
|
55
|
+
('learning_path', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.learningpath')),
|
|
56
|
+
('skill', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.skill')),
|
|
57
|
+
],
|
|
58
|
+
options={
|
|
59
|
+
'abstract': False,
|
|
60
|
+
'unique_together': {('learning_path', 'skill')},
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
migrations.CreateModel(
|
|
64
|
+
name='LearningPathStep',
|
|
65
|
+
fields=[
|
|
66
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
67
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
68
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
69
|
+
('course_key', opaque_keys.edx.django.models.CourseKeyField(max_length=255)),
|
|
70
|
+
('relative_due_date_in_days', models.PositiveIntegerField(blank=True, help_text='Used to calculate the due date from the starting date of the course.', null=True, verbose_name='Due date (days)')),
|
|
71
|
+
('order', models.PositiveIntegerField(blank=True, help_text='Ordinal position of this step in the sequence of the Learning Path, if applicable.', null=True, verbose_name='Sequential order')),
|
|
72
|
+
('learning_path', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='learning_paths.learningpath')),
|
|
73
|
+
],
|
|
74
|
+
options={
|
|
75
|
+
'unique_together': {('learning_path', 'course_key')},
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
migrations.CreateModel(
|
|
79
|
+
name='AcquiredSkill',
|
|
80
|
+
fields=[
|
|
81
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
82
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
83
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
84
|
+
('level', models.PositiveIntegerField(help_text='The skill level associated with this course.')),
|
|
85
|
+
('learning_path', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.learningpath')),
|
|
86
|
+
('skill', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.skill')),
|
|
87
|
+
],
|
|
88
|
+
options={
|
|
89
|
+
'abstract': False,
|
|
90
|
+
'unique_together': {('learning_path', 'skill')},
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated by Django 3.2.23 on 2024-01-24 10:48
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('learning_paths', '0001_initial'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='learningpath',
|
|
16
|
+
name='uuid',
|
|
17
|
+
field=models.UUIDField(blank=True, default=uuid.uuid4, editable=False, unique=True),
|
|
18
|
+
),
|
|
19
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 3.2.23 on 2024-01-31 17:52
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('learning_paths', '0002_learningpath_uuid'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='learningpath',
|
|
15
|
+
name='subtitle',
|
|
16
|
+
field=models.CharField(max_length=255),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 3.2.21 on 2024-02-07 16:33
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
import django.utils.timezone
|
|
7
|
+
import model_utils.fields
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
('learning_paths', '0003_learningpath_subtitle'),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='LearningPathEnrollment',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
23
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
24
|
+
('learning_path', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.learningpath')),
|
|
25
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'unique_together': {('user', 'learning_path')},
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
migrations.AddField(
|
|
32
|
+
model_name='learningpath',
|
|
33
|
+
name='enrolled_users',
|
|
34
|
+
field=models.ManyToManyField(through='learning_paths.LearningPathEnrollment', to=settings.AUTH_USER_MODEL),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-12-05 14:02
|
|
2
|
+
|
|
3
|
+
import django.core.validators
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('learning_paths', '0004_auto_20240207_1633'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AddField(
|
|
16
|
+
model_name='learningpathstep',
|
|
17
|
+
name='weight',
|
|
18
|
+
field=models.FloatField(default=1.0, help_text="Weight of this course in the learning path's aggregate grade.Specify as a floating point number between 0 and 1, where 1 represents 100%.", validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)]),
|
|
19
|
+
),
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='LearningPathGradingCriteria',
|
|
22
|
+
fields=[
|
|
23
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
24
|
+
('required_completion', models.FloatField(default=0.8, help_text='The minimum average completion (0.0-1.0) across all steps in the learning path required to mark it as completed.')),
|
|
25
|
+
('required_grade', models.FloatField(default=0.75, help_text='Minimum weighted arithmetic mean grade (0.0-1.0) required across all steps to pass this learning path. The weight of each step is determined by its `weight` field.')),
|
|
26
|
+
('learning_path', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='grading_criteria', to='learning_paths.learningpath')),
|
|
27
|
+
],
|
|
28
|
+
),
|
|
29
|
+
]
|