edx-completion 5.1.0__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.
- completion/__init__.py +7 -0
- completion/api/__init__.py +0 -0
- completion/api/permissions.py +47 -0
- completion/api/urls.py +11 -0
- completion/api/v1/__init__.py +0 -0
- completion/api/v1/urls.py +18 -0
- completion/api/v1/views.py +256 -0
- completion/apps.py +43 -0
- completion/exceptions.py +16 -0
- completion/handlers.py +53 -0
- completion/migrations/0001_initial.py +41 -0
- completion/migrations/0001_squashed_0003_learning_context.py +79 -0
- completion/migrations/0002_auto_20180125_1510.py +15 -0
- completion/migrations/0003_learning_context.py +39 -0
- completion/migrations/0004_rename_indexes.py +23 -0
- completion/migrations/__init__.py +0 -0
- completion/models.py +367 -0
- completion/services.py +198 -0
- completion/settings/__init__.py +0 -0
- completion/settings/common.py +42 -0
- completion/settings/test_settings.py +21 -0
- completion/test_utils.py +173 -0
- completion/urls.py +2 -0
- completion/utilities.py +30 -0
- completion/waffle.py +22 -0
- edx_completion-5.1.0.dist-info/METADATA +176 -0
- edx_completion-5.1.0.dist-info/RECORD +31 -0
- edx_completion-5.1.0.dist-info/WHEEL +5 -0
- edx_completion-5.1.0.dist-info/entry_points.txt +2 -0
- edx_completion-5.1.0.dist-info/licenses/LICENSE +661 -0
- edx_completion-5.1.0.dist-info/top_level.txt +1 -0
completion/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permissions classes for the API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from django.http import Http404
|
|
7
|
+
from rest_framework.permissions import BasePermission
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IsStaffOrOwner(BasePermission):
|
|
11
|
+
"""
|
|
12
|
+
Permission that allows access to admin users or the owner of an object.
|
|
13
|
+
The owner is considered the User object represented by obj.user.
|
|
14
|
+
|
|
15
|
+
Copied from edx-platform/openedx/core/lib/api/permissions.py
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def has_object_permission(self, request, view, obj):
|
|
19
|
+
return request.user.is_staff or obj.user == request.user
|
|
20
|
+
|
|
21
|
+
def has_permission(self, request, view):
|
|
22
|
+
user = request.user
|
|
23
|
+
return user.is_staff \
|
|
24
|
+
or (user.username == request.GET.get('username')) \
|
|
25
|
+
or (user.username == getattr(request, 'data', {}).get('username')) \
|
|
26
|
+
or (user.username == getattr(request, 'data', {}).get('user')) \
|
|
27
|
+
or (user.username == getattr(view, 'kwargs', {}).get('username'))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IsUserInUrl(BasePermission):
|
|
31
|
+
"""
|
|
32
|
+
Permission that checks to see if the request user matches the user in the URL.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def has_permission(self, request, view):
|
|
36
|
+
"""
|
|
37
|
+
Returns true if the current request is by the user themselves.
|
|
38
|
+
|
|
39
|
+
Note: a 404 is returned for non-staff instead of a 403. This is to prevent
|
|
40
|
+
users from being able to detect the existence of accounts.
|
|
41
|
+
"""
|
|
42
|
+
url_username = request.parser_context.get('kwargs', {}).get('username', '')
|
|
43
|
+
if request.user.username.lower() != url_username.lower():
|
|
44
|
+
if request.user.is_staff:
|
|
45
|
+
return False # staff gets 403
|
|
46
|
+
raise Http404()
|
|
47
|
+
return True
|
completion/api/urls.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API v1 URLs.
|
|
3
|
+
"""
|
|
4
|
+
from django.urls import path, re_path
|
|
5
|
+
|
|
6
|
+
from . import views
|
|
7
|
+
|
|
8
|
+
app_name = 'v1' # pylint: disable=invalid-name
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path('completion-batch', views.CompletionBatchView.as_view(), name='completion-batch'),
|
|
11
|
+
re_path(r'^subsection-completion/{username}/{course_key}/{subsection_id}'.format( # pylint: disable=consider-using-f-string
|
|
12
|
+
username=r'(?P<username>[^/]*)',
|
|
13
|
+
course_key=r'(?P<course_key>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)',
|
|
14
|
+
subsection_id=r'(?P<subsection_id>[^/]*)'
|
|
15
|
+
), # noqa
|
|
16
|
+
views.SubsectionCompletionView.as_view(),
|
|
17
|
+
name='subsection-completion')
|
|
18
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API v1 views.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import permissions, status
|
|
6
|
+
from rest_framework.response import Response
|
|
7
|
+
from rest_framework.views import APIView
|
|
8
|
+
|
|
9
|
+
from django.contrib import auth
|
|
10
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
11
|
+
from django.db import DatabaseError
|
|
12
|
+
from django.utils.translation import gettext as _
|
|
13
|
+
|
|
14
|
+
# pylint: disable=ungrouped-imports
|
|
15
|
+
try:
|
|
16
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
|
17
|
+
except ImportError:
|
|
18
|
+
from edx_rest_framework_extensions.authentication import JwtAuthentication
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
|
22
|
+
except ImportError:
|
|
23
|
+
from edx_rest_framework_extensions.authentication import SessionAuthenticationAllowInactiveUser
|
|
24
|
+
# pylint: enable=ungrouped-imports
|
|
25
|
+
|
|
26
|
+
from opaque_keys import InvalidKeyError
|
|
27
|
+
from opaque_keys.edx.keys import LearningContextKey, UsageKey
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from common.djangoapps.student.models import CourseEnrollment
|
|
31
|
+
from lms.djangoapps.course_api.blocks.api import get_blocks
|
|
32
|
+
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
|
33
|
+
except ImportError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
from completion import waffle
|
|
37
|
+
from completion.api.permissions import IsStaffOrOwner, IsUserInUrl
|
|
38
|
+
from completion.models import BlockCompletion
|
|
39
|
+
|
|
40
|
+
User = auth.get_user_model()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CompletionBatchView(APIView):
|
|
44
|
+
"""
|
|
45
|
+
Handles API requests to submit batch completions.
|
|
46
|
+
"""
|
|
47
|
+
authentication_classes = (
|
|
48
|
+
JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser,
|
|
49
|
+
)
|
|
50
|
+
permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,)
|
|
51
|
+
REQUIRED_KEYS = ['username', 'course_key', 'blocks']
|
|
52
|
+
|
|
53
|
+
def _validate_and_parse(self, batch_object):
|
|
54
|
+
"""
|
|
55
|
+
Performs validation on the batch object to make sure it is in the proper format.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
* batch_object: The data provided to a POST. The expected format is the following:
|
|
59
|
+
```
|
|
60
|
+
{
|
|
61
|
+
"username": "username",
|
|
62
|
+
"course_key": "context-key",
|
|
63
|
+
"blocks": {
|
|
64
|
+
"block_key1": 0.0,
|
|
65
|
+
"block_key2": 1.0,
|
|
66
|
+
"block_key3": 1.0,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Return Value:
|
|
72
|
+
* tuple: (User, LearningContextKey, List of tuples (UsageKey, completion_float)
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
|
|
76
|
+
django.core.exceptions.ValidationError:
|
|
77
|
+
If any aspect of validation fails a ValidationError is raised.
|
|
78
|
+
|
|
79
|
+
ObjectDoesNotExist:
|
|
80
|
+
If a database object cannot be found an ObjectDoesNotExist is raised.
|
|
81
|
+
"""
|
|
82
|
+
if not waffle.ENABLE_COMPLETION_TRACKING_SWITCH.is_enabled():
|
|
83
|
+
raise ValidationError(
|
|
84
|
+
_("BlockCompletion.objects.submit_batch_completion should not be called when the feature is disabled.")
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for key in self.REQUIRED_KEYS:
|
|
88
|
+
if key not in batch_object:
|
|
89
|
+
raise ValidationError(_("Key '{key}' not found.").format(key=key))
|
|
90
|
+
|
|
91
|
+
username = batch_object['username']
|
|
92
|
+
user = User.objects.get(username=username)
|
|
93
|
+
|
|
94
|
+
context_key_obj = self._validate_and_parse_context_key(batch_object['course_key'])
|
|
95
|
+
|
|
96
|
+
if context_key_obj.is_course and not CourseEnrollment.is_enrolled(user, context_key_obj):
|
|
97
|
+
raise ValidationError(_('User is not enrolled in course.'))
|
|
98
|
+
|
|
99
|
+
blocks = batch_object['blocks']
|
|
100
|
+
block_objs = []
|
|
101
|
+
for block_key in blocks:
|
|
102
|
+
block_key_obj = self._validate_and_parse_block_key(block_key, context_key_obj)
|
|
103
|
+
completion = float(blocks[block_key])
|
|
104
|
+
block_objs.append((block_key_obj, completion))
|
|
105
|
+
|
|
106
|
+
return user, block_objs
|
|
107
|
+
|
|
108
|
+
def _validate_and_parse_context_key(self, context_key):
|
|
109
|
+
"""
|
|
110
|
+
Returns a validated parsed LearningContextKey deserialized from the given context_key.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
return LearningContextKey.from_string(context_key)
|
|
114
|
+
except InvalidKeyError as error:
|
|
115
|
+
raise ValidationError(_("Invalid learning context key: {}").format(context_key)) from error
|
|
116
|
+
|
|
117
|
+
def _validate_and_parse_block_key(self, block_key, context_key_obj):
|
|
118
|
+
"""
|
|
119
|
+
Returns a validated, parsed UsageKey deserialized from the given block_key.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
block_key_obj = UsageKey.from_string(block_key)
|
|
123
|
+
except InvalidKeyError as error:
|
|
124
|
+
raise ValidationError(_("Invalid block key: {}").format(block_key)) from error
|
|
125
|
+
|
|
126
|
+
if block_key_obj.context_key.is_course and block_key_obj.context_key.run is None:
|
|
127
|
+
# block_key_obj is from an old mongo course and its context_key is missing run info:
|
|
128
|
+
block_key_obj = block_key_obj.replace(course_key=context_key_obj)
|
|
129
|
+
|
|
130
|
+
if block_key_obj.context_key != context_key_obj:
|
|
131
|
+
raise ValidationError(
|
|
132
|
+
_("Block with key: '{key}' is not in context {context}").format(key=block_key, context=context_key_obj)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return block_key_obj
|
|
136
|
+
|
|
137
|
+
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
|
138
|
+
"""
|
|
139
|
+
Inserts a batch of completions.
|
|
140
|
+
|
|
141
|
+
REST Endpoint Format:
|
|
142
|
+
```
|
|
143
|
+
{
|
|
144
|
+
"username": "username",
|
|
145
|
+
"course_key": "course-key",
|
|
146
|
+
"blocks": {
|
|
147
|
+
"block_key1": 0.0,
|
|
148
|
+
"block_key2": 1.0,
|
|
149
|
+
"block_key3": 1.0,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Returns**
|
|
155
|
+
|
|
156
|
+
A Response object, with an appropriate status code.
|
|
157
|
+
|
|
158
|
+
If successful, status code is 200.
|
|
159
|
+
```
|
|
160
|
+
{
|
|
161
|
+
"detail" : _("ok")
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Otherwise, a 400 or 404 may be returned, and the "detail" content will explain the error.
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
batch_object = request.data or {}
|
|
169
|
+
try:
|
|
170
|
+
user, blocks = self._validate_and_parse(batch_object)
|
|
171
|
+
BlockCompletion.objects.submit_batch_completion(user, blocks)
|
|
172
|
+
except ValidationError as exc:
|
|
173
|
+
return Response({
|
|
174
|
+
"detail": _(' ').join(str(msg) for msg in exc.messages),
|
|
175
|
+
}, status=status.HTTP_400_BAD_REQUEST)
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
return Response({
|
|
178
|
+
"detail": str(exc),
|
|
179
|
+
}, status=status.HTTP_400_BAD_REQUEST)
|
|
180
|
+
except ObjectDoesNotExist as exc:
|
|
181
|
+
return Response({
|
|
182
|
+
"detail": str(exc),
|
|
183
|
+
}, status=status.HTTP_404_NOT_FOUND)
|
|
184
|
+
except DatabaseError as exc:
|
|
185
|
+
return Response({
|
|
186
|
+
"detail": str(exc),
|
|
187
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
188
|
+
|
|
189
|
+
return Response({"detail": _("ok")}, status=status.HTTP_200_OK)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SubsectionCompletionView(APIView):
|
|
193
|
+
"""
|
|
194
|
+
Handles API endpoints for the milestones experiments.
|
|
195
|
+
TODO: EDUCATOR-2358 Remove this class after the
|
|
196
|
+
milestones experiment is no longer running.
|
|
197
|
+
"""
|
|
198
|
+
authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser,)
|
|
199
|
+
permission_classes = (permissions.IsAuthenticated, IsUserInUrl)
|
|
200
|
+
|
|
201
|
+
def get(self, request, username, course_key, subsection_id):
|
|
202
|
+
"""
|
|
203
|
+
Returns completion for a (user, subsection, course).
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def get_completion(course_completions, all_blocks, block_id):
|
|
207
|
+
"""
|
|
208
|
+
Recursively get the aggregate completion for a subsection,
|
|
209
|
+
given the subsection block and a list of all blocks.
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
course_completions: a dictionary of completion values by block IDs
|
|
213
|
+
all_blocks: a dictionary of the block structure for a subsection
|
|
214
|
+
block_id: an ID of a block for which to get completion
|
|
215
|
+
"""
|
|
216
|
+
block = all_blocks.get(block_id)
|
|
217
|
+
child_ids = block.get('children', [])
|
|
218
|
+
if not child_ids:
|
|
219
|
+
return course_completions.get(block.serializer.instance, 0)
|
|
220
|
+
|
|
221
|
+
completion = 0
|
|
222
|
+
total_children = 0
|
|
223
|
+
for child_id in child_ids:
|
|
224
|
+
completion += get_completion(course_completions, all_blocks, child_id)
|
|
225
|
+
total_children += 1
|
|
226
|
+
|
|
227
|
+
return int(completion == total_children)
|
|
228
|
+
|
|
229
|
+
user_id = User.objects.get(username=username).id
|
|
230
|
+
block_types_filter = [
|
|
231
|
+
'course',
|
|
232
|
+
'chapter',
|
|
233
|
+
'sequential',
|
|
234
|
+
'vertical',
|
|
235
|
+
'html',
|
|
236
|
+
'problem',
|
|
237
|
+
'video',
|
|
238
|
+
'discussion',
|
|
239
|
+
'drag-and-drop-v2'
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
blocks = get_blocks(
|
|
243
|
+
request,
|
|
244
|
+
UsageKey.from_string(subsection_id),
|
|
245
|
+
nav_depth=2,
|
|
246
|
+
requested_fields=[
|
|
247
|
+
'children'
|
|
248
|
+
],
|
|
249
|
+
block_types_filter=block_types_filter
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
course_key = LearningContextKey.from_string(course_key)
|
|
253
|
+
context_completions = BlockCompletion.get_learning_context_completions(user_id, course_key)
|
|
254
|
+
aggregated_completion = get_completion(context_completions, blocks['blocks'], blocks['root'])
|
|
255
|
+
|
|
256
|
+
return Response({"completion": aggregated_completion}, status=status.HTTP_200_OK)
|
completion/apps.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App Configuration for Completion
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from django.apps import AppConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CompletionAppConfig(AppConfig):
|
|
10
|
+
"""
|
|
11
|
+
App Configuration for Completion
|
|
12
|
+
"""
|
|
13
|
+
name = 'completion'
|
|
14
|
+
verbose_name = 'Completion'
|
|
15
|
+
|
|
16
|
+
plugin_app = {
|
|
17
|
+
'url_config': {
|
|
18
|
+
'lms.djangoapp': {
|
|
19
|
+
'namespace': 'completion',
|
|
20
|
+
'regex': r'^api/completion/',
|
|
21
|
+
'relative_path': 'api.urls',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
'settings_config': {
|
|
25
|
+
'lms.djangoapp': {
|
|
26
|
+
'common': {
|
|
27
|
+
'relative_path': 'settings.common',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'signals_config': {
|
|
32
|
+
'lms.djangoapp': {
|
|
33
|
+
'relative_path': 'handlers',
|
|
34
|
+
'receivers': [
|
|
35
|
+
{
|
|
36
|
+
'receiver_func_name': 'scorable_block_completion',
|
|
37
|
+
'signal_path': 'lms.djangoapps.grades.signals.signals.PROBLEM_WEIGHTED_SCORE_CHANGED',
|
|
38
|
+
'dispatch_uid': 'completion.handlers.scorable_block_completion',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
completion/exceptions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains custom exceptions used by the completion repository.
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnavailableCompletionData(Exception):
|
|
9
|
+
"""
|
|
10
|
+
UnavailableCompletionData should be raised when a method is unable to gather
|
|
11
|
+
completion data from BlockCompletion.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, context_key):
|
|
15
|
+
Exception.__init__(
|
|
16
|
+
self, f"The learner does not have completion data within learning context {context_key}")
|
completion/handlers.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signal handlers to trigger completion updates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from django.contrib import auth
|
|
8
|
+
from opaque_keys import InvalidKeyError
|
|
9
|
+
from opaque_keys.edx.keys import LearningContextKey, UsageKey
|
|
10
|
+
from xblock.completable import XBlockCompletionMode
|
|
11
|
+
from xblock.core import XBlock
|
|
12
|
+
|
|
13
|
+
from . import waffle
|
|
14
|
+
from .models import BlockCompletion
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
User = auth.get_user_model()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scorable_block_completion(sender, **kwargs): # pylint: disable=unused-argument
|
|
21
|
+
"""
|
|
22
|
+
When a problem is scored, submit a new BlockCompletion for that block.
|
|
23
|
+
"""
|
|
24
|
+
if not waffle.ENABLE_COMPLETION_TRACKING_SWITCH.is_enabled():
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
block_key = UsageKey.from_string(kwargs['usage_id'])
|
|
28
|
+
except InvalidKeyError:
|
|
29
|
+
log.exception("Unable to parse XBlock usage_id for completion: %s", kwargs['usage_id'])
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if block_key.context_key.is_course and block_key.context_key.run is None:
|
|
33
|
+
# In the case of old mongo courses, the context_key cannot be derived
|
|
34
|
+
# from the block key alone since it will be missing run info:
|
|
35
|
+
course_key_with_run = LearningContextKey.from_string(kwargs['course_id'])
|
|
36
|
+
block_key = block_key.replace(course_key=course_key_with_run)
|
|
37
|
+
|
|
38
|
+
block_cls = XBlock.load_class(block_key.block_type)
|
|
39
|
+
if XBlockCompletionMode.get_mode(block_cls) != XBlockCompletionMode.COMPLETABLE:
|
|
40
|
+
return
|
|
41
|
+
if getattr(block_cls, 'has_custom_completion', False):
|
|
42
|
+
return
|
|
43
|
+
user = User.objects.get(id=kwargs['user_id'])
|
|
44
|
+
if kwargs.get('score_deleted'):
|
|
45
|
+
completion = 0.0
|
|
46
|
+
else:
|
|
47
|
+
completion = 1.0
|
|
48
|
+
if not kwargs.get('grader_response'):
|
|
49
|
+
BlockCompletion.objects.submit_completion(
|
|
50
|
+
user=user,
|
|
51
|
+
block_key=block_key,
|
|
52
|
+
completion=completion,
|
|
53
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
|
|
2
|
+
|
|
3
|
+
import django.utils.timezone
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
from django.db.models import BigAutoField
|
|
7
|
+
|
|
8
|
+
import model_utils.fields
|
|
9
|
+
|
|
10
|
+
from completion.models import validate_percent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Migration(migrations.Migration):
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='BlockCompletion',
|
|
22
|
+
fields=[
|
|
23
|
+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
|
24
|
+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
|
25
|
+
('id', BigAutoField(serialize=False, primary_key=True)),
|
|
26
|
+
('course_key', CourseKeyField(max_length=255)),
|
|
27
|
+
('block_key', UsageKeyField(max_length=255)),
|
|
28
|
+
('block_type', models.CharField(max_length=64)),
|
|
29
|
+
('completion', models.FloatField(validators=[validate_percent])),
|
|
30
|
+
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
|
31
|
+
],
|
|
32
|
+
),
|
|
33
|
+
migrations.AlterUniqueTogether(
|
|
34
|
+
name='blockcompletion',
|
|
35
|
+
unique_together={('course_key', 'block_key', 'user')},
|
|
36
|
+
),
|
|
37
|
+
migrations.AlterIndexTogether(
|
|
38
|
+
name='blockcompletion',
|
|
39
|
+
index_together={('course_key', 'block_type', 'user'), ('user', 'course_key', 'modified')},
|
|
40
|
+
),
|
|
41
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Generated by Django 5.1.8 on 2025-04-10 08:24
|
|
2
|
+
|
|
3
|
+
import completion.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
|
+
from django.conf import settings
|
|
9
|
+
from django.db import migrations, models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Migration(migrations.Migration):
|
|
13
|
+
|
|
14
|
+
replaces = [
|
|
15
|
+
("completion", "0001_initial"),
|
|
16
|
+
("completion", "0002_auto_20180125_1510"),
|
|
17
|
+
("completion", "0003_learning_context"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
initial = True
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
operations = [
|
|
27
|
+
migrations.CreateModel(
|
|
28
|
+
name="BlockCompletion",
|
|
29
|
+
fields=[
|
|
30
|
+
(
|
|
31
|
+
"created",
|
|
32
|
+
model_utils.fields.AutoCreatedField(
|
|
33
|
+
default=django.utils.timezone.now,
|
|
34
|
+
editable=False,
|
|
35
|
+
verbose_name="created",
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
"modified",
|
|
40
|
+
model_utils.fields.AutoLastModifiedField(
|
|
41
|
+
default=django.utils.timezone.now,
|
|
42
|
+
editable=False,
|
|
43
|
+
verbose_name="modified",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
|
47
|
+
(
|
|
48
|
+
"context_key",
|
|
49
|
+
opaque_keys.edx.django.models.LearningContextKeyField(
|
|
50
|
+
db_column="course_key", max_length=255
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
"block_key",
|
|
55
|
+
opaque_keys.edx.django.models.UsageKeyField(max_length=255),
|
|
56
|
+
),
|
|
57
|
+
("block_type", models.CharField(max_length=64)),
|
|
58
|
+
(
|
|
59
|
+
"completion",
|
|
60
|
+
models.FloatField(validators=[completion.models.validate_percent]),
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
"user",
|
|
64
|
+
models.ForeignKey(
|
|
65
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
66
|
+
to=settings.AUTH_USER_MODEL,
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
options={
|
|
71
|
+
"unique_together": {("context_key", "block_key", "user")},
|
|
72
|
+
"index_together": {
|
|
73
|
+
("context_key", "block_type", "user"),
|
|
74
|
+
("user", "context_key", "modified"),
|
|
75
|
+
},
|
|
76
|
+
"get_latest_by": "modified",
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
('completion', '0001_initial'),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AlterModelOptions(
|
|
12
|
+
name='blockcompletion',
|
|
13
|
+
options={'get_latest_by': 'modified'},
|
|
14
|
+
),
|
|
15
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changes completion to track blocks for any learning context,
|
|
2
|
+
# and not just for courses.
|
|
3
|
+
#
|
|
4
|
+
# This migration does not produce any database-level changes. You can verify
|
|
5
|
+
# this with: ./manage.py lms sqlmigrate completion 0003
|
|
6
|
+
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.db import migrations
|
|
9
|
+
import opaque_keys.edx.django.models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Migration(migrations.Migration):
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
16
|
+
('completion', '0002_auto_20180125_1510'),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.AlterField(
|
|
21
|
+
model_name='blockcompletion',
|
|
22
|
+
name='course_key',
|
|
23
|
+
field=opaque_keys.edx.django.models.LearningContextKeyField(db_column='course_key', max_length=255),
|
|
24
|
+
preserve_default=False,
|
|
25
|
+
),
|
|
26
|
+
migrations.RenameField(
|
|
27
|
+
model_name='blockcompletion',
|
|
28
|
+
old_name='course_key',
|
|
29
|
+
new_name='context_key',
|
|
30
|
+
),
|
|
31
|
+
migrations.AlterUniqueTogether(
|
|
32
|
+
name='blockcompletion',
|
|
33
|
+
unique_together={('context_key', 'block_key', 'user')},
|
|
34
|
+
),
|
|
35
|
+
migrations.AlterIndexTogether(
|
|
36
|
+
name='blockcompletion',
|
|
37
|
+
index_together={('user', 'context_key', 'modified'), ('context_key', 'block_type', 'user')},
|
|
38
|
+
),
|
|
39
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-04-03 05:37
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("completion", "0003_learning_context"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RenameIndex(
|
|
14
|
+
model_name="blockcompletion",
|
|
15
|
+
new_name="completion__user_id_861fe5_idx",
|
|
16
|
+
old_fields=("user", "context_key", "modified"),
|
|
17
|
+
),
|
|
18
|
+
migrations.RenameIndex(
|
|
19
|
+
model_name="blockcompletion",
|
|
20
|
+
new_name="completion__course__c6e6ad_idx",
|
|
21
|
+
old_fields=("context_key", "block_type", "user"),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
File without changes
|