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,66 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2025-03-28 09:54
|
|
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
|
+
import simple_history.models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Migration(migrations.Migration):
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
('learning_paths', '0005_learningpathstep_weight_learningpathgradingcriteria'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='learningpathenrollment',
|
|
21
|
+
name='enrolled_at',
|
|
22
|
+
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, help_text='Timestamp of enrollment or un-enrollment. To be explicitly set when performing a learner enrollment.'),
|
|
23
|
+
preserve_default=False,
|
|
24
|
+
),
|
|
25
|
+
migrations.AddField(
|
|
26
|
+
model_name='learningpathenrollment',
|
|
27
|
+
name='is_active',
|
|
28
|
+
field=models.BooleanField(default=True, help_text='Indicates if the learner is enrolled or not in the Learning Path'),
|
|
29
|
+
),
|
|
30
|
+
migrations.CreateModel(
|
|
31
|
+
name='HistoricalLearningPathEnrollment',
|
|
32
|
+
fields=[
|
|
33
|
+
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
|
34
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
|
35
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
|
36
|
+
('is_active', models.BooleanField(default=True, help_text='Indicates if the learner is enrolled or not in the Learning Path')),
|
|
37
|
+
('enrolled_at', models.DateTimeField(blank=True, editable=False, help_text='Timestamp of enrollment or un-enrollment. To be explicitly set when performing a learner enrollment.')),
|
|
38
|
+
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
|
39
|
+
('history_date', models.DateTimeField(db_index=True)),
|
|
40
|
+
('history_change_reason', models.CharField(max_length=100, null=True)),
|
|
41
|
+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
|
42
|
+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
|
43
|
+
('learning_path', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='learning_paths.learningpath')),
|
|
44
|
+
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
|
45
|
+
],
|
|
46
|
+
options={
|
|
47
|
+
'verbose_name': 'historical learning path enrollment',
|
|
48
|
+
'verbose_name_plural': 'historical learning path enrollments',
|
|
49
|
+
'ordering': ('-history_date', '-history_id'),
|
|
50
|
+
'get_latest_by': ('history_date', 'history_id'),
|
|
51
|
+
},
|
|
52
|
+
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
53
|
+
),
|
|
54
|
+
migrations.CreateModel(
|
|
55
|
+
name='LearningPathEnrollmentAllowed',
|
|
56
|
+
fields=[
|
|
57
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
58
|
+
('email', models.EmailField(max_length=254)),
|
|
59
|
+
('learning_path', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_paths.learningpath')),
|
|
60
|
+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
61
|
+
],
|
|
62
|
+
options={
|
|
63
|
+
'unique_together': {('email', 'learning_path')},
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
]
|
|
File without changes
|
learning_paths/models.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database models for learning_paths.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from django.contrib import auth
|
|
9
|
+
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.utils.translation import gettext_lazy as _
|
|
12
|
+
from model_utils.models import TimeStampedModel
|
|
13
|
+
from opaque_keys.edx.django.models import CourseKeyField
|
|
14
|
+
from simple_history.models import HistoricalRecords
|
|
15
|
+
|
|
16
|
+
from .compat import get_user_course_grade
|
|
17
|
+
|
|
18
|
+
User = auth.get_user_model()
|
|
19
|
+
|
|
20
|
+
LEVEL_CHOICES = [
|
|
21
|
+
("beginner", _("Beginner")),
|
|
22
|
+
("intermediate", _("Intermediate")),
|
|
23
|
+
("advanced", _("Advanced")),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LearningPath(TimeStampedModel):
|
|
28
|
+
"""
|
|
29
|
+
A Learning Path, containing a sequence of courses.
|
|
30
|
+
|
|
31
|
+
.. no_pii:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# LearningPath is consumed as a course-discovery Program.
|
|
35
|
+
# Programs are identified by UUIDs and this why we must have this UUID field.
|
|
36
|
+
uuid = models.UUIDField(blank=True, default=uuid4, editable=False, unique=True)
|
|
37
|
+
slug = models.SlugField(
|
|
38
|
+
db_index=True,
|
|
39
|
+
unique=True,
|
|
40
|
+
help_text=_("Custom unique code identifying this Learning Path."),
|
|
41
|
+
)
|
|
42
|
+
display_name = models.CharField(max_length=255)
|
|
43
|
+
subtitle = models.CharField(max_length=255)
|
|
44
|
+
description = models.TextField(blank=True)
|
|
45
|
+
# We don't use URLField here in order to allow e.g. relative URLs.
|
|
46
|
+
# max_length=200 as from URLField.
|
|
47
|
+
image_url = models.CharField(
|
|
48
|
+
max_length=200,
|
|
49
|
+
blank=True,
|
|
50
|
+
verbose_name=_("Image URL"),
|
|
51
|
+
help_text=_("URL to an image representing this Learning Path."),
|
|
52
|
+
)
|
|
53
|
+
level = models.CharField(max_length=255, blank=True, choices=LEVEL_CHOICES)
|
|
54
|
+
duration_in_days = models.PositiveIntegerField(
|
|
55
|
+
blank=True,
|
|
56
|
+
null=True,
|
|
57
|
+
verbose_name=_("Duration (days)"),
|
|
58
|
+
help_text=_(
|
|
59
|
+
"Approximate time (in days) it should take to complete this Learning Path."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
sequential = models.BooleanField(
|
|
63
|
+
verbose_name=_("Is sequential"),
|
|
64
|
+
help_text=_(
|
|
65
|
+
"Whether the courses in this Learning Path are meant to be taken sequentially."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
enrolled_users = models.ManyToManyField(User, through="LearningPathEnrollment")
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
"""User-friendly string representation of this model."""
|
|
72
|
+
return self.display_name
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LearningPathStep(TimeStampedModel):
|
|
76
|
+
"""
|
|
77
|
+
A step in a Learning Path, consisting of a course and an ordinal position.
|
|
78
|
+
|
|
79
|
+
.. no_pii:
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
class Meta:
|
|
83
|
+
"""Model options."""
|
|
84
|
+
|
|
85
|
+
unique_together = ("learning_path", "course_key")
|
|
86
|
+
|
|
87
|
+
course_key = CourseKeyField(max_length=255)
|
|
88
|
+
learning_path = models.ForeignKey(
|
|
89
|
+
LearningPath, related_name="steps", on_delete=models.CASCADE
|
|
90
|
+
)
|
|
91
|
+
relative_due_date_in_days = models.PositiveIntegerField(
|
|
92
|
+
blank=True,
|
|
93
|
+
null=True,
|
|
94
|
+
verbose_name=_("Due date (days)"),
|
|
95
|
+
help_text=_(
|
|
96
|
+
"Used to calculate the due date from the starting date of the course."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
order = models.PositiveIntegerField(
|
|
100
|
+
blank=True,
|
|
101
|
+
null=True,
|
|
102
|
+
verbose_name=_("Sequential order"),
|
|
103
|
+
help_text=_(
|
|
104
|
+
"Ordinal position of this step in the sequence of the Learning Path, if applicable."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
weight = models.FloatField(
|
|
108
|
+
default=1.0,
|
|
109
|
+
validators=[MinValueValidator(0.0), MaxValueValidator(1.0)],
|
|
110
|
+
help_text=_(
|
|
111
|
+
"Weight of this course in the learning path's aggregate grade."
|
|
112
|
+
"Specify as a floating point number between 0 and 1, where 1 represents 100%."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
"""User-friendly string representation of this model."""
|
|
118
|
+
return "{}: {}".format(self.order, self.course_key)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Skill(TimeStampedModel):
|
|
122
|
+
"""
|
|
123
|
+
A skill that can be associated with Learning Paths.
|
|
124
|
+
|
|
125
|
+
.. no_pii:
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
display_name = models.CharField(max_length=255)
|
|
129
|
+
|
|
130
|
+
def __str__(self):
|
|
131
|
+
"""User-friendly string representation of this model."""
|
|
132
|
+
return self.display_name
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class LearningPathSkill(TimeStampedModel):
|
|
136
|
+
"""
|
|
137
|
+
Abstract base model for a skill required or acquired in a Learning Path..
|
|
138
|
+
|
|
139
|
+
.. no_pii:
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
class Meta:
|
|
143
|
+
"""Model options."""
|
|
144
|
+
|
|
145
|
+
abstract = True
|
|
146
|
+
unique_together = ("learning_path", "skill")
|
|
147
|
+
|
|
148
|
+
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
149
|
+
skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
|
|
150
|
+
level = models.PositiveIntegerField(
|
|
151
|
+
help_text=_("The skill level associated with this course.")
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def __str__(self):
|
|
155
|
+
"""User-friendly string representation of this model."""
|
|
156
|
+
return "{}: {}".format(self.skill, self.level)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class RequiredSkill(LearningPathSkill):
|
|
160
|
+
"""
|
|
161
|
+
A required skill for a Learning Path.
|
|
162
|
+
|
|
163
|
+
.. no_pii:
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class AcquiredSkill(LearningPathSkill):
|
|
168
|
+
"""
|
|
169
|
+
A skill acquired in a Learning Path.
|
|
170
|
+
|
|
171
|
+
.. no_pii:
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LearningPathEnrollment(TimeStampedModel):
|
|
176
|
+
"""
|
|
177
|
+
A user enrolled in a Learning Path.
|
|
178
|
+
|
|
179
|
+
.. no_pii:
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
class Meta:
|
|
183
|
+
"""Model options."""
|
|
184
|
+
|
|
185
|
+
unique_together = ("user", "learning_path")
|
|
186
|
+
|
|
187
|
+
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
188
|
+
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
189
|
+
is_active = models.BooleanField(
|
|
190
|
+
default=True,
|
|
191
|
+
help_text=_("Indicates if the learner is enrolled or not in the Learning Path"),
|
|
192
|
+
)
|
|
193
|
+
enrolled_at = models.DateTimeField(
|
|
194
|
+
auto_now_add=True,
|
|
195
|
+
help_text=_(
|
|
196
|
+
"Timestamp of enrollment or un-enrollment. To be explicitly set when performing"
|
|
197
|
+
" a learner enrollment."
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
history = HistoricalRecords()
|
|
202
|
+
|
|
203
|
+
def __str__(self):
|
|
204
|
+
"""User-friendly string representation of this model."""
|
|
205
|
+
return "{}: {}".format(self.user, self.learning_path)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def estimated_end_date(self):
|
|
209
|
+
"""Estimated end date of the learning path."""
|
|
210
|
+
if self.learning_path.duration_in_days is None:
|
|
211
|
+
return None
|
|
212
|
+
return self.created + timedelta(days=self.learning_path.duration_in_days)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class LearningPathGradingCriteria(models.Model):
|
|
216
|
+
"""
|
|
217
|
+
Grading criteria for a learning path.
|
|
218
|
+
|
|
219
|
+
.. no_pii:
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
learning_path = models.OneToOneField(
|
|
223
|
+
LearningPath, related_name="grading_criteria", on_delete=models.CASCADE
|
|
224
|
+
)
|
|
225
|
+
required_completion = models.FloatField(
|
|
226
|
+
default=0.80,
|
|
227
|
+
help_text=(
|
|
228
|
+
"The minimum average completion (0.0-1.0) across all steps in the learning path "
|
|
229
|
+
"required to mark it as completed."
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
required_grade = models.FloatField(
|
|
233
|
+
default=0.75,
|
|
234
|
+
help_text=(
|
|
235
|
+
"Minimum weighted arithmetic mean grade (0.0-1.0) required across all steps "
|
|
236
|
+
"to pass this learning path. The weight of each step is determined by its `weight` field."
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def __str__(self):
|
|
241
|
+
"""User-friendly string representation of this model."""
|
|
242
|
+
return f"{self.learning_path.display_name} Grading Criteria"
|
|
243
|
+
|
|
244
|
+
def calculate_grade(self, user):
|
|
245
|
+
"""
|
|
246
|
+
Calculate the aggregate grade for a user across the learning path.
|
|
247
|
+
"""
|
|
248
|
+
total_weight = 0.0
|
|
249
|
+
weighted_sum = 0.0
|
|
250
|
+
|
|
251
|
+
for step in self.learning_path.steps.all():
|
|
252
|
+
course_grade = get_user_course_grade(user, step.course_key)
|
|
253
|
+
course_weight = step.weight
|
|
254
|
+
weighted_sum += course_grade.percent * course_weight
|
|
255
|
+
total_weight += course_weight
|
|
256
|
+
|
|
257
|
+
return weighted_sum / total_weight if total_weight > 0 else 0.0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class LearningPathEnrollmentAllowed(models.Model):
|
|
261
|
+
"""
|
|
262
|
+
Represents an allowed enrollment in a learning path for a user email.
|
|
263
|
+
|
|
264
|
+
These objects can be created when learners are invited/enrolled by staff before
|
|
265
|
+
they have registered and created an account, allowing future learners to enroll.
|
|
266
|
+
|
|
267
|
+
.. pii: The email field is not retired to allow future learners to enroll.
|
|
268
|
+
.. pii_types: email_address
|
|
269
|
+
.. pii_retirement: retained
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
class Meta:
|
|
273
|
+
"""Model options."""
|
|
274
|
+
|
|
275
|
+
unique_together = ("email", "learning_path")
|
|
276
|
+
|
|
277
|
+
email = models.EmailField()
|
|
278
|
+
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
279
|
+
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
|
|
280
|
+
|
|
281
|
+
def __str__(self):
|
|
282
|
+
"""User-friendly string representation of this model."""
|
|
283
|
+
return f"LearningPathEnrollmentAllowed for {self.user.username} in {self.learning_path.display_name}"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Django signal handler for learning paths plugin."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from learning_paths.models import LearningPathEnrollment, LearningPathEnrollmentAllowed
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_pending_enrollments(
|
|
11
|
+
sender, instance, created, **kwargs
|
|
12
|
+
): # pylint: disable=unused-argument
|
|
13
|
+
"""
|
|
14
|
+
Process pending enrollments after a user instance has been created.
|
|
15
|
+
|
|
16
|
+
Bulk enrollment API allows enrolling users with just the email. So learners who
|
|
17
|
+
do not have an account yet would also be enrolled. This information is stored
|
|
18
|
+
in the LearningPathEnrollmentAllowed model. This signal handler processes such
|
|
19
|
+
instances and created the corresponding LearningPathEnrollment objects.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
sender: User model class.
|
|
23
|
+
instance: The actual instance being saved.
|
|
24
|
+
created: A boolean indicating whether this is a creation and not an update.
|
|
25
|
+
"""
|
|
26
|
+
if not created:
|
|
27
|
+
logger.debug(
|
|
28
|
+
"[LearningPaths] Skipping processing of pending enrollments for user %s.",
|
|
29
|
+
instance,
|
|
30
|
+
)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
logger.info("[LearningPaths] Processing pending enrollments for user %s", instance)
|
|
34
|
+
pending_enrollments = LearningPathEnrollmentAllowed.objects.filter(
|
|
35
|
+
email=instance.email
|
|
36
|
+
).all()
|
|
37
|
+
|
|
38
|
+
enrollments = []
|
|
39
|
+
|
|
40
|
+
for entry in pending_enrollments:
|
|
41
|
+
entry.user = instance
|
|
42
|
+
entry.save()
|
|
43
|
+
|
|
44
|
+
enrollments.append(
|
|
45
|
+
LearningPathEnrollment(learning_path=entry.learning_path, user=instance)
|
|
46
|
+
)
|
|
47
|
+
new_enrollments = LearningPathEnrollment.objects.bulk_create(enrollments)
|
|
48
|
+
logger.info(
|
|
49
|
+
"[LearningPaths] Processed %d pending Learning Path enrollments for user %s.",
|
|
50
|
+
instance,
|
|
51
|
+
len(new_enrollments),
|
|
52
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Django settings for the learning_paths app."""
|
|
2
|
+
|
|
3
|
+
from django.conf import Settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def plugin_settings(settings: Settings):
|
|
7
|
+
"""
|
|
8
|
+
Define plugin settings.
|
|
9
|
+
|
|
10
|
+
See: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html
|
|
11
|
+
"""
|
|
12
|
+
# By default, un-enrolling from learning paths is only possible with staff
|
|
13
|
+
# action. Learners cannot un-enroll themselves.
|
|
14
|
+
# Set this True, if the learners should be allowed to un-enroll themselves.
|
|
15
|
+
settings.LEARNING_PATHS_ALLOW_SELF_UNENROLLMENT = False
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
{% load i18n %}
|
|
4
|
+
{% trans "Dummy text to generate a translation (.po) source file. It is safe to delete this line. It is also safe to delete (load i18n) above if there are no other (trans) tags in the file" %}
|
|
5
|
+
|
|
6
|
+
{% comment %}
|
|
7
|
+
As the developer of this package, don't place anything here if you can help it
|
|
8
|
+
since this allows developers to have interoperability between your template
|
|
9
|
+
structure and their own.
|
|
10
|
+
|
|
11
|
+
Example: Developer melding the 2SoD pattern to fit inside with another pattern::
|
|
12
|
+
|
|
13
|
+
{% extends "base.html" %}
|
|
14
|
+
{% load static %}
|
|
15
|
+
|
|
16
|
+
<!-- Their site uses old school block layout -->
|
|
17
|
+
{% block extra_js %}
|
|
18
|
+
|
|
19
|
+
<!-- Your package using 2SoD block layout -->
|
|
20
|
+
{% block javascript %}
|
|
21
|
+
<script src="{% static 'js/ninja.js' %}" type="text/javascript"></script>
|
|
22
|
+
{% endblock javascript %}
|
|
23
|
+
|
|
24
|
+
{% endblock extra_js %}
|
|
25
|
+
{% endcomment %}
|
|
26
|
+
|
learning_paths/urls.py
ADDED
|
@@ -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,31 @@
|
|
|
1
|
+
learning_paths/__init__.py,sha256=c0GTsCXvSs7qB0UJZ3GBVv4-UXhJWdy4EY3Gc9fpl8c,54
|
|
2
|
+
learning_paths/admin.py,sha256=EuoxJH3izHBS4N-Yftz9ngUlPhNYf9fpMunO0vFuw64,4180
|
|
3
|
+
learning_paths/apps.py,sha256=fbtuJUulJztnYfJpa5B9znNriplm6qrybcRUjijMHIQ,1900
|
|
4
|
+
learning_paths/compat.py,sha256=JIh12SJMYj--5o4G059ZmP_La5rJxdnnFo4c8yc7UG8,1301
|
|
5
|
+
learning_paths/models.py,sha256=0qJkJEIEwaChitx0wKo3JPXlOHGycYYp_pi7ZWkutp8,8630
|
|
6
|
+
learning_paths/receivers.py,sha256=vyvaVCfT4ihdWsQi--G2A2zzSMeud8iJkLyF7uYO_sQ,1755
|
|
7
|
+
learning_paths/settings.py,sha256=hMmsYncOOxyfoGoUHxhsMXT8M4U0EUv2QEP9kPK052E,561
|
|
8
|
+
learning_paths/urls.py,sha256=0nBnFJ5Oew8gNB1KkREuQLXu_pgUOoTRU6RzPDSR9W8,160
|
|
9
|
+
learning_paths/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
learning_paths/api/urls.py,sha256=DmzrNT1nyTJCD52CRqJu4psEcqu8fPt-6XlaQ70l24o,130
|
|
11
|
+
learning_paths/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
learning_paths/api/v1/filters.py,sha256=23Bfssm0wmGTnEdyl25BZKfn8zlOF5B_g9rd0usIt4w,412
|
|
13
|
+
learning_paths/api/v1/permissions.py,sha256=w2ZyiZktNsDBFDzcHSJ7jpq8K9tSsrwG7jNKLmrJKGI,736
|
|
14
|
+
learning_paths/api/v1/serializers.py,sha256=GMZsM-a-WbRsFddjOmsjlmP6HsPRbrWHoN6JXSyKcQs,3066
|
|
15
|
+
learning_paths/api/v1/urls.py,sha256=JFRPPsDIfPTt4Kzd115sCrYPhmbMixE7VQGlP2g3XkM,1147
|
|
16
|
+
learning_paths/api/v1/utils.py,sha256=4MtJdO9kbkl0BzojHg9qh71f4hk9bF-C5HUr2Z3a8TQ,1952
|
|
17
|
+
learning_paths/api/v1/views.py,sha256=WktrExUeEdGW3oEXK2GbuhIXsoQbZAhtwE4M38FYlrw,11689
|
|
18
|
+
learning_paths/migrations/0001_initial.py,sha256=ZEq2dR5YqSiRoNENDr8P0NswhEYUkIRlEhuc-v4hx08,5872
|
|
19
|
+
learning_paths/migrations/0002_learningpath_uuid.py,sha256=GAgnRSHOfkJ557B4_HjDpyLCRYVwj8VCgn-GBu2f5Mg,443
|
|
20
|
+
learning_paths/migrations/0003_learningpath_subtitle.py,sha256=aKSQrXEgS7HwbOSo8Jt1FB-CdINhjt2eXoLUBnKMwxI,400
|
|
21
|
+
learning_paths/migrations/0004_auto_20240207_1633.py,sha256=uoDW6bpBEyRw6d3kPdUpWvJWi5b6jFfof5X0uNwCnRM,1545
|
|
22
|
+
learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py,sha256=lFEY2_WveuhvjHb6ks6SnPS1LyYf8mIiLELEuuiZRmw,1578
|
|
23
|
+
learning_paths/migrations/0006_enrollment_models.py,sha256=FTauwwIvUK06IbejKtLZPvjjal_f370AQgCOvs43GWc,3963
|
|
24
|
+
learning_paths/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
learning_paths/templates/learning_paths/base.html,sha256=NHmMV45xJTnPEKQltuhe5Ddw9MDZLIQD_rK8GRXhqrU,873
|
|
26
|
+
learning_paths_plugin-0.2.3.dist-info/licenses/LICENSE.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
27
|
+
learning_paths_plugin-0.2.3.dist-info/METADATA,sha256=y-iCJ4xfnekiT5YerFZ3YRhNeefaVmcf_mUX7W3B-Eg,3765
|
|
28
|
+
learning_paths_plugin-0.2.3.dist-info/WHEEL,sha256=MAQBAzGbXNI3bUmkDsiV_duv8i-gcdnLzw7cfUFwqhU,109
|
|
29
|
+
learning_paths_plugin-0.2.3.dist-info/entry_points.txt,sha256=ss-iTtSrRCEoDmMJUEdl2jlLWD6g7Hv2UJPnbWBMLxg,73
|
|
30
|
+
learning_paths_plugin-0.2.3.dist-info/top_level.txt,sha256=GxFiqsMbNKF-RWbZo6o49OjxVJFmbv5lIhu9FX4pehM,15
|
|
31
|
+
learning_paths_plugin-0.2.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
learning_paths
|