openedx-progress 1.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_progress/__init__.py +5 -0
- openedx_progress/apps.py +51 -0
- openedx_progress/management/__init__.py +3 -0
- openedx_progress/management/commands/__init__.py +3 -0
- openedx_progress/management/commands/backfill_course_completion_summaries.py +239 -0
- openedx_progress/management/commands/process_dirty_course_completion_summaries.py +122 -0
- openedx_progress/migrations/0001_initial.py +130 -0
- openedx_progress/migrations/__init__.py +3 -0
- openedx_progress/models.py +91 -0
- openedx_progress/services.py +155 -0
- openedx_progress/settings/__init__.py +3 -0
- openedx_progress/settings/common.py +12 -0
- openedx_progress/signals.py +89 -0
- openedx_progress/templates/openedx_progress/base.html +26 -0
- openedx_progress/urls.py +10 -0
- openedx_progress-1.0.0.dist-info/METADATA +224 -0
- openedx_progress-1.0.0.dist-info/RECORD +21 -0
- openedx_progress-1.0.0.dist-info/WHEEL +6 -0
- openedx_progress-1.0.0.dist-info/entry_points.txt +5 -0
- openedx_progress-1.0.0.dist-info/licenses/LICENSE.txt +664 -0
- openedx_progress-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Services for materializing Open edX learner course completion summaries.
|
|
3
|
+
"""
|
|
4
|
+
# pylint: disable=import-outside-toplevel,import-error
|
|
5
|
+
import logging
|
|
6
|
+
from decimal import ROUND_HALF_UP, Decimal
|
|
7
|
+
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
|
|
11
|
+
from openedx_progress.models import CourseCompletionSummary, CourseCompletionSummaryDirty
|
|
12
|
+
|
|
13
|
+
PERCENT_COMPLETE_QUANT = Decimal('0.00001')
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _course_id_for_storage(course_key):
|
|
18
|
+
"""
|
|
19
|
+
Convert an opaque course key to its stable stored string form.
|
|
20
|
+
"""
|
|
21
|
+
return str(course_key)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _user_id(user):
|
|
25
|
+
"""
|
|
26
|
+
Return the integer primary key from a user object or user id value.
|
|
27
|
+
"""
|
|
28
|
+
return int(getattr(user, 'id', user))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_course_blocks_completion_summary(course_key, user):
|
|
32
|
+
"""
|
|
33
|
+
Load edx-platform's completion summary function lazily.
|
|
34
|
+
"""
|
|
35
|
+
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary
|
|
36
|
+
|
|
37
|
+
return get_course_blocks_completion_summary(course_key, user)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_unfiltered_course_unit_count(course_key, user):
|
|
41
|
+
"""
|
|
42
|
+
Return the number of course units before learner access transformers prune them.
|
|
43
|
+
"""
|
|
44
|
+
from lms.djangoapps.course_blocks.api import get_course_blocks
|
|
45
|
+
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
|
|
46
|
+
from xmodule.modulestore.django import modulestore
|
|
47
|
+
|
|
48
|
+
store = modulestore()
|
|
49
|
+
course_usage_key = store.make_course_usage_key(course_key)
|
|
50
|
+
block_data = get_course_blocks(
|
|
51
|
+
user,
|
|
52
|
+
course_usage_key,
|
|
53
|
+
transformers=BlockStructureTransformers([]),
|
|
54
|
+
allow_start_dates_in_future=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
unit_count = 0
|
|
58
|
+
for section_key in block_data.get_children(course_usage_key):
|
|
59
|
+
for subsection_key in block_data.get_children(section_key):
|
|
60
|
+
unit_count += len(block_data.get_children(subsection_key))
|
|
61
|
+
return unit_count
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def calculate_locked_count(summary_locked_count, complete_count, incomplete_count, course_key, user):
|
|
65
|
+
"""
|
|
66
|
+
Calculate locked units, including units removed by learner access filtering.
|
|
67
|
+
"""
|
|
68
|
+
locked_count = int(summary_locked_count)
|
|
69
|
+
try:
|
|
70
|
+
total_unit_count = _get_unfiltered_course_unit_count(course_key, user)
|
|
71
|
+
except ImportError:
|
|
72
|
+
return locked_count
|
|
73
|
+
except Exception: # pylint: disable=broad-except
|
|
74
|
+
log.exception('Unable to calculate unfiltered course unit count for %s.', course_key)
|
|
75
|
+
return locked_count
|
|
76
|
+
|
|
77
|
+
derived_locked_count = max(total_unit_count - complete_count - incomplete_count, 0)
|
|
78
|
+
return max(locked_count, derived_locked_count)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def calculate_percent_complete(complete_count, incomplete_count):
|
|
82
|
+
"""
|
|
83
|
+
Calculate complete / (complete + incomplete), excluding locked blocks.
|
|
84
|
+
"""
|
|
85
|
+
denominator = complete_count + incomplete_count
|
|
86
|
+
if denominator == 0:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
Decimal(complete_count) / Decimal(denominator)
|
|
91
|
+
).quantize(PERCENT_COMPLETE_QUANT, rounding=ROUND_HALF_UP)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def compute_completion_summary(course_key, user):
|
|
95
|
+
"""
|
|
96
|
+
Return persisted completion summary values for a learner and course.
|
|
97
|
+
"""
|
|
98
|
+
summary = _get_course_blocks_completion_summary(course_key, user)
|
|
99
|
+
complete_count = int(summary['complete_count'])
|
|
100
|
+
incomplete_count = int(summary['incomplete_count'])
|
|
101
|
+
locked_count = calculate_locked_count(
|
|
102
|
+
summary['locked_count'],
|
|
103
|
+
complete_count,
|
|
104
|
+
incomplete_count,
|
|
105
|
+
course_key,
|
|
106
|
+
user,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
'course_id': _course_id_for_storage(course_key),
|
|
111
|
+
'user_id': _user_id(user),
|
|
112
|
+
'complete_count': complete_count,
|
|
113
|
+
'incomplete_count': incomplete_count,
|
|
114
|
+
'locked_count': locked_count,
|
|
115
|
+
'percent_complete': calculate_percent_complete(complete_count, incomplete_count),
|
|
116
|
+
'computed_at': timezone.now(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def upsert_completion_summary(course_key, user):
|
|
121
|
+
"""
|
|
122
|
+
Compute and persist the summary row for a learner and course.
|
|
123
|
+
"""
|
|
124
|
+
values = compute_completion_summary(course_key, user)
|
|
125
|
+
course_id = values.pop('course_id')
|
|
126
|
+
user_id = values.pop('user_id')
|
|
127
|
+
|
|
128
|
+
summary, _created = CourseCompletionSummary.objects.update_or_create(
|
|
129
|
+
course_id=course_id,
|
|
130
|
+
user_id=user_id,
|
|
131
|
+
defaults=values,
|
|
132
|
+
)
|
|
133
|
+
return summary
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def mark_completion_summary_dirty(course_key, user, reason=''):
|
|
137
|
+
"""
|
|
138
|
+
Mark a learner/course summary for later recomputation.
|
|
139
|
+
"""
|
|
140
|
+
dirty, _created = CourseCompletionSummaryDirty.objects.update_or_create(
|
|
141
|
+
course_id=_course_id_for_storage(course_key),
|
|
142
|
+
user_id=_user_id(user),
|
|
143
|
+
defaults={
|
|
144
|
+
'reason': reason,
|
|
145
|
+
'last_error': '',
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
return dirty
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def mark_completion_summary_dirty_on_commit(course_key, user, reason=''):
|
|
152
|
+
"""
|
|
153
|
+
Mark a learner/course summary dirty after the current transaction commits.
|
|
154
|
+
"""
|
|
155
|
+
transaction.on_commit(lambda: mark_completion_summary_dirty(course_key, user, reason=reason))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common settings for openedx_progress.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def plugin_settings(settings): # pylint: disable=unused-argument
|
|
7
|
+
"""
|
|
8
|
+
Apply common Open edX settings for openedx_progress.
|
|
9
|
+
|
|
10
|
+
The plugin currently does not require runtime settings, but the hook is
|
|
11
|
+
registered so LMS and CMS can load the Django plugin consistently.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional signal handlers for keeping completion summaries fresh.
|
|
3
|
+
"""
|
|
4
|
+
# pylint: disable=import-outside-toplevel
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
from django.db.models.signals import post_delete, post_save
|
|
7
|
+
|
|
8
|
+
from openedx_progress import services
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _block_completion_model():
|
|
12
|
+
"""
|
|
13
|
+
Return the Open edX BlockCompletion model when it is installed.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
from completion.models import BlockCompletion
|
|
17
|
+
except (ImportError, RuntimeError):
|
|
18
|
+
return None
|
|
19
|
+
return BlockCompletion
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _course_key_from_completion(instance):
|
|
23
|
+
"""
|
|
24
|
+
Extract a course key from a BlockCompletion instance.
|
|
25
|
+
"""
|
|
26
|
+
context_key = getattr(instance, 'context_key', None)
|
|
27
|
+
if context_key is not None:
|
|
28
|
+
# BlockCompletion also supports non-course learning contexts (libraries).
|
|
29
|
+
if not getattr(context_key, 'is_course', True):
|
|
30
|
+
return None
|
|
31
|
+
return context_key
|
|
32
|
+
|
|
33
|
+
block_key = getattr(instance, 'block_key', None)
|
|
34
|
+
return getattr(block_key, 'course_key', None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _user_id_from_completion(instance):
|
|
38
|
+
"""
|
|
39
|
+
Extract a user id from a BlockCompletion instance.
|
|
40
|
+
"""
|
|
41
|
+
return getattr(instance, 'user_id', None) or getattr(getattr(instance, 'user', None), 'id', None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mark_dirty_from_completion(instance, reason):
|
|
45
|
+
"""
|
|
46
|
+
Queue a summary recomputation for a BlockCompletion change.
|
|
47
|
+
"""
|
|
48
|
+
course_key = _course_key_from_completion(instance)
|
|
49
|
+
user_id = _user_id_from_completion(instance)
|
|
50
|
+
if course_key is None or user_id is None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
transaction.on_commit(
|
|
54
|
+
lambda: services.mark_completion_summary_dirty(course_key, user_id, reason=reason)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def block_completion_saved(sender, instance, **kwargs): # pylint: disable=unused-argument
|
|
59
|
+
"""
|
|
60
|
+
Mark a learner/course summary dirty after a completion save.
|
|
61
|
+
"""
|
|
62
|
+
_mark_dirty_from_completion(instance, 'block_completion_saved')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def block_completion_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
|
|
66
|
+
"""
|
|
67
|
+
Mark a learner/course summary dirty after a completion delete.
|
|
68
|
+
"""
|
|
69
|
+
_mark_dirty_from_completion(instance, 'block_completion_deleted')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def register_signal_handlers():
|
|
73
|
+
"""
|
|
74
|
+
Connect optional completion signals.
|
|
75
|
+
"""
|
|
76
|
+
block_completion = _block_completion_model()
|
|
77
|
+
if block_completion is None:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
post_save.connect(
|
|
81
|
+
block_completion_saved,
|
|
82
|
+
sender=block_completion,
|
|
83
|
+
dispatch_uid='openedx_progress.block_completion_saved',
|
|
84
|
+
)
|
|
85
|
+
post_delete.connect(
|
|
86
|
+
block_completion_deleted,
|
|
87
|
+
sender=block_completion,
|
|
88
|
+
dispatch_uid='openedx_progress.block_completion_deleted',
|
|
89
|
+
)
|
|
@@ -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
|
+
|
openedx_progress/urls.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URLs for openedx_progress.
|
|
3
|
+
"""
|
|
4
|
+
from django.urls import re_path # pylint: disable=unused-import
|
|
5
|
+
from django.views.generic import TemplateView # pylint: disable=unused-import
|
|
6
|
+
|
|
7
|
+
urlpatterns = [
|
|
8
|
+
# TODO: Fill in URL patterns and views here.
|
|
9
|
+
# re_path(r'', TemplateView.as_view(template_name="openedx_progress/base.html")),
|
|
10
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openedx-progress
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Progress tracking for Open edX students
|
|
5
|
+
Home-page: https://github.com/aulasneo/openedx-progress
|
|
6
|
+
Author: Andrés González
|
|
7
|
+
Author-email: andres@aulasneo.com
|
|
8
|
+
License: AGPL 3.0
|
|
9
|
+
Keywords: Python edx
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 5.2
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.txt
|
|
21
|
+
Requires-Dist: Django<5.3,>=5.2.13
|
|
22
|
+
Requires-Dist: django-model-utils<6.0,>=5.0.0
|
|
23
|
+
Requires-Dist: edx-django-utils<9.0,>=8.0.1
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# openedx-progress
|
|
38
|
+
|
|
39
|
+
Progress tracking for Open edX students.
|
|
40
|
+
|
|
41
|
+
This Django plugin materializes learner course completion summaries for Open edX
|
|
42
|
+
analytics. It stores one row per learner/course pair with the completion counts
|
|
43
|
+
returned by `lms.djangoapps.courseware.courses.get_course_blocks_completion_summary`,
|
|
44
|
+
derives locked units from the unfiltered course structure when available, and
|
|
45
|
+
uses `computed_at` for when the values were last computed.
|
|
46
|
+
|
|
47
|
+
## Compatibility
|
|
48
|
+
|
|
49
|
+
Targets **Open edX Verawood**, using **Python 3.12+ and Django 5.2**.
|
|
50
|
+
Runtime dependency ranges allow compatible patch updates; development lockfiles
|
|
51
|
+
are not intended to replace the LMS requirements. Install the plugin into the
|
|
52
|
+
platform environment using its constraints.
|
|
53
|
+
|
|
54
|
+
See [the Verawood compatibility review](docs/verawood.md) for the checked
|
|
55
|
+
upstream versions, integration points, release impacts, and deployment checks.
|
|
56
|
+
|
|
57
|
+
## Why this exists
|
|
58
|
+
|
|
59
|
+
In the LMS progress page, progress, also known as completion, is calculated at
|
|
60
|
+
runtime as:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
units completed / (units completed + units incomplete)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The caveat is that these unit counts do not include units that are locked for
|
|
67
|
+
that specific learner at that time. A unit can be locked because of visibility
|
|
68
|
+
controls, dated gates, or other course access rules.
|
|
69
|
+
|
|
70
|
+
Analytics add-ons such as Panorama often only have the total number of units
|
|
71
|
+
that exist in the course. They do not have learner-specific detail about how many
|
|
72
|
+
of those units are currently hidden or locked. As a result, analytics systems can
|
|
73
|
+
show a completion percentage that differs from the percentage shown in the LMS
|
|
74
|
+
progress page.
|
|
75
|
+
|
|
76
|
+
This plugin stores the learner-specific `complete_count`, `incomplete_count`,
|
|
77
|
+
and `locked_count` values in the summary table. By querying this table,
|
|
78
|
+
analytics systems can count locked units separately and produce a completion
|
|
79
|
+
calculation that more closely matches what the LMS knows about that learner at
|
|
80
|
+
the time the summary was computed.
|
|
81
|
+
|
|
82
|
+
For an LMS-style completion percentage, use:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
complete_count / (complete_count + incomplete_count)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If an analytics system starts from a course-wide `total_units` value, it can use
|
|
89
|
+
`locked_count` to derive the learner-specific denominator:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
complete_count / (total_units - locked_count)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
In practice, `total_units - locked_count` should correspond to
|
|
96
|
+
`complete_count + incomplete_count` for the same course structure snapshot.
|
|
97
|
+
|
|
98
|
+
## Database tables
|
|
99
|
+
|
|
100
|
+
This plugin creates two tables.
|
|
101
|
+
|
|
102
|
+
### `openedx_progress_coursecompletionsummary`
|
|
103
|
+
|
|
104
|
+
Stores the latest materialized completion summary for each learner/course pair.
|
|
105
|
+
There is one row per `course_id` and `user_id`.
|
|
106
|
+
|
|
107
|
+
Columns:
|
|
108
|
+
|
|
109
|
+
- `id`: primary key.
|
|
110
|
+
- `created`: when the row was first created.
|
|
111
|
+
- `modified`: when the row was last saved.
|
|
112
|
+
- `user_id`: learner user id.
|
|
113
|
+
- `course_id`: Open edX course key stored as a string.
|
|
114
|
+
- `complete_count`: number of units completed by the learner.
|
|
115
|
+
- `incomplete_count`: number of visible, unlocked units not yet completed by the
|
|
116
|
+
learner.
|
|
117
|
+
- `locked_count`: number of units currently locked or hidden for that learner.
|
|
118
|
+
- `percent_complete`: stored LMS-style completion ratio, calculated from
|
|
119
|
+
`complete_count / (complete_count + incomplete_count)`.
|
|
120
|
+
- `computed_at`: when the completion summary was computed from edx-platform.
|
|
121
|
+
|
|
122
|
+
The table has a unique constraint on `course_id` and `user_id`, so repeated
|
|
123
|
+
backfills or dirty-queue processing update the existing learner/course summary
|
|
124
|
+
instead of creating duplicate rows.
|
|
125
|
+
|
|
126
|
+
### `openedx_progress_coursecompletionsummarydirty`
|
|
127
|
+
|
|
128
|
+
Stores a lightweight queue of learner/course pairs that need their completion
|
|
129
|
+
summary recomputed.
|
|
130
|
+
|
|
131
|
+
Columns:
|
|
132
|
+
|
|
133
|
+
- `id`: primary key.
|
|
134
|
+
- `created`: when the queue row was first created.
|
|
135
|
+
- `modified`: when the queue row was last saved.
|
|
136
|
+
- `user_id`: learner user id.
|
|
137
|
+
- `course_id`: Open edX course key stored as a string.
|
|
138
|
+
- `reason`: short reason why the summary was marked dirty, such as a completion
|
|
139
|
+
signal.
|
|
140
|
+
- `attempts`: number of processing attempts.
|
|
141
|
+
- `last_error`: last processing error, if recomputation failed.
|
|
142
|
+
|
|
143
|
+
The table has a unique constraint on `course_id` and `user_id`, so a learner can
|
|
144
|
+
only have one pending recomputation row per course.
|
|
145
|
+
|
|
146
|
+
## Course completion summaries
|
|
147
|
+
|
|
148
|
+
`CourseCompletionSummary` stores:
|
|
149
|
+
|
|
150
|
+
- `user_id`
|
|
151
|
+
- `course_id`
|
|
152
|
+
- `complete_count`
|
|
153
|
+
- `incomplete_count`
|
|
154
|
+
- `locked_count`
|
|
155
|
+
- `percent_complete`
|
|
156
|
+
- `computed_at`
|
|
157
|
+
- `created` and `modified`
|
|
158
|
+
|
|
159
|
+
The stored `percent_complete` is calculated as:
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
complete_count / (complete_count + incomplete_count)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`locked_count` is intentionally excluded from the stored denominator to match the
|
|
166
|
+
LMS progress page calculation. If the denominator is zero, `percent_complete` is
|
|
167
|
+
stored as `NULL`.
|
|
168
|
+
|
|
169
|
+
## Backfill command
|
|
170
|
+
|
|
171
|
+
Run the backfill command inside an LMS environment where edx-platform apps are
|
|
172
|
+
installed:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
./manage.py lms backfill_course_completion_summaries
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Useful options:
|
|
179
|
+
|
|
180
|
+
- `--course-id course-v1:edX+DemoX+Demo_Course` restricts processing to one
|
|
181
|
+
course. When omitted, all courses from CourseOverview are processed.
|
|
182
|
+
- `--user-id 123` can be repeated to process specific learners.
|
|
183
|
+
- `--batch-size 500` controls database iteration batches.
|
|
184
|
+
- `--sleep 0.5` pauses between batches.
|
|
185
|
+
- `--dry-run` selects learners without writing rows.
|
|
186
|
+
- `--force` recomputes rows that already exist.
|
|
187
|
+
|
|
188
|
+
## Dirty queue
|
|
189
|
+
|
|
190
|
+
When `completion.models.BlockCompletion` is importable, the plugin registers
|
|
191
|
+
`post_save` and `post_delete` signal handlers that enqueue learner/course pairs
|
|
192
|
+
in `CourseCompletionSummaryDirty` after transaction commit. Process queued rows
|
|
193
|
+
with:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
./manage.py lms process_dirty_course_completion_summaries
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Getting help
|
|
200
|
+
|
|
201
|
+
For anything non-trivial, open an issue in this repository with as many details
|
|
202
|
+
as you can provide:
|
|
203
|
+
|
|
204
|
+
<https://github.com/aulasneo/openedx-progress/issues>
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
The code in this repository is licensed under the AGPL 3.0 unless otherwise
|
|
209
|
+
noted. See [LICENSE.txt](LICENSE.txt) for details.
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
This project is currently accepting all types of contributions, including bug
|
|
214
|
+
fixes, security fixes, maintenance work, and new features. Please discuss new
|
|
215
|
+
feature ideas with the maintainers before beginning development to maximize the
|
|
216
|
+
chances of your change being accepted.
|
|
217
|
+
|
|
218
|
+
## Security
|
|
219
|
+
|
|
220
|
+
Please do not report security issues in public. Email support@aulasneo.com.
|
|
221
|
+
|
|
222
|
+
## Disclaimer
|
|
223
|
+
|
|
224
|
+
Part of this code was developed with the aid of AI tools.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
openedx_progress/__init__.py,sha256=K9Nmmb7O9-3zMO7obQqjJcIgSAsLmk94-NNW7okKUpI,72
|
|
2
|
+
openedx_progress/apps.py,sha256=Ro433qnbFmkhcP7tpKm4uiIpzuvlOWzFw698gO9m_z0,1539
|
|
3
|
+
openedx_progress/models.py,sha256=dk8_g8yaNe_WrlVKthzoDicYf8cw6ddsaunTfpC7X8E,3012
|
|
4
|
+
openedx_progress/services.py,sha256=2J4klW5baeiox5DQnkA1dsb_h2T2-v3JxYHH2-ZfFvw,5004
|
|
5
|
+
openedx_progress/signals.py,sha256=W54Xfibg5yaJECcD0IaBoat_V0FnlOt2FLmz-bCljcs,2655
|
|
6
|
+
openedx_progress/urls.py,sha256=56MREMo1jAd062a9d-hYqJfqjxJqaJ6EZ6Q64whTXHk,333
|
|
7
|
+
openedx_progress/management/__init__.py,sha256=D2wnKQgkOSVIS4BdvQfh34vtTD5v4hzOZSdgiUBX3lk,57
|
|
8
|
+
openedx_progress/management/commands/__init__.py,sha256=YQl1Jxh3wiFH2CYMheY-78Az4XiSVkXSOIS16Wyfzk4,50
|
|
9
|
+
openedx_progress/management/commands/backfill_course_completion_summaries.py,sha256=vtv3EPVQGv04O12dUd2lXzJZF4Yd7En6L3RBMVJdA7A,7996
|
|
10
|
+
openedx_progress/management/commands/process_dirty_course_completion_summaries.py,sha256=LNBGiWimuud985dqoAWBEp5ugf9a_SvYRL0lhNvr-9w,4408
|
|
11
|
+
openedx_progress/migrations/0001_initial.py,sha256=6SBE_zfG8Rnxv6Q0OHUE89y6sRZOZ_S9xnPY-Yjv67g,4771
|
|
12
|
+
openedx_progress/migrations/__init__.py,sha256=l4oScNBg8EJlkEVn2oMJiUkiC0z8K43YEWFcB7-JBaU,50
|
|
13
|
+
openedx_progress/settings/__init__.py,sha256=KsucKRjkLY6NAbek3A3ey3gXyWPn6aHxcgTIjyNUJJg,47
|
|
14
|
+
openedx_progress/settings/common.py,sha256=YcQ9b8XLVCieACBYQg0XZ6-4LPTODc5pif-V50WTwEw,335
|
|
15
|
+
openedx_progress/templates/openedx_progress/base.html,sha256=NHmMV45xJTnPEKQltuhe5Ddw9MDZLIQD_rK8GRXhqrU,873
|
|
16
|
+
openedx_progress-1.0.0.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
|
|
17
|
+
openedx_progress-1.0.0.dist-info/METADATA,sha256=UVltlpbaHuo4fajmEdzLBY7meN9nEAVEmUPSl38tvyE,7528
|
|
18
|
+
openedx_progress-1.0.0.dist-info/WHEEL,sha256=4YBfCYNH4wlLpv3pzq1hbEuIlXA4WJabKLFurZ7eTL0,109
|
|
19
|
+
openedx_progress-1.0.0.dist-info/entry_points.txt,sha256=EjfJAZ1LtaBeP7xZ4quMgtVXvcv9nAVDnI4qHxZlWgk,159
|
|
20
|
+
openedx_progress-1.0.0.dist-info/top_level.txt,sha256=SfNQVAzyS14tJL4RIpo9uv4L3d6e5fLge_xUK2PRisg,17
|
|
21
|
+
openedx_progress-1.0.0.dist-info/RECORD,,
|