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.
@@ -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
@@ -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,9 @@
1
+ """
2
+ URLs for learning_paths.
3
+ """
4
+
5
+ from django.urls import include, path
6
+
7
+ urlpatterns = [
8
+ path("api/learning_paths/", include("learning_paths.api.urls")),
9
+ ]
@@ -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,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,2 @@
1
+ [lms.djangoapp]
2
+ learning_paths = learning_paths.apps:LearningPathsConfig
@@ -0,0 +1 @@
1
+ learning_paths