-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
services.py
258 lines (217 loc) · 9.92 KB
/
services.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""
Implementation of abstraction layer for other parts of the system to make queries related to ID Verification.
"""
import logging
from datetime import timedelta
from itertools import chain
from urllib.parse import quote
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils.timezone import now
from django.utils.translation import gettext as _
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import User
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from .models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
from .utils import most_recent_verification
log = logging.getLogger(__name__)
class XBlockVerificationService:
"""
Learner verification XBlock service.
"""
def get_status(self, user_id):
"""
Returns the user's current photo verification status.
Args:
user_id: the user's id
Returns: one of the following strings
'none' - no such verification exists
'expired' - verification has expired
'approved' - verification has been approved
'pending' - verification process is still ongoing
'must_reverify' - verification has been denied and user must resubmit photos
"""
user = User.objects.get(id=user_id)
return IDVerificationService.user_status(user)
def reverify_url(self):
"""
Returns the URL for a user to verify themselves.
"""
return IDVerificationService.get_verify_location()
class IDVerificationService:
"""
Learner verification service interface for callers within edx-platform.
"""
@classmethod
def user_is_verified(cls, user):
"""
Return whether or not a user has satisfactorily proved their identity.
Depending on the policy, this can expire after some period of time, so
a user might have to renew periodically.
"""
expiration_datetime = cls.get_expiration_datetime(user, ['approved'])
if expiration_datetime:
return expiration_datetime >= now()
return False
@classmethod
def verifications_for_user(cls, user):
"""
Return a list of all verifications associated with the given user.
"""
verifications = []
for verification in chain(SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-created_at'),
SSOVerification.objects.filter(user=user).order_by('-created_at'),
ManualVerification.objects.filter(user=user).order_by('-created_at')):
verifications.append(verification)
return verifications
@classmethod
def get_verified_user_ids(cls, users):
"""
Given a list of users, returns an iterator of user ids that have non-expired verifications of any type.
"""
filter_kwargs = {
'user__in': users,
'status': 'approved',
'created_at__gt': now() - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
}
return chain(
SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
SSOVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
ManualVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True)
)
@classmethod
def get_expiration_datetime(cls, user, statuses):
"""
Check whether the user has a verification with one of the given
statuses and return the "expiration_datetime" of most recent verification that
matches one of the given statuses.
Arguments:
user (Object): User
statuses: List of verification statuses (e.g., ['approved'])
Returns:
expiration_datetime: expiration_datetime of most recent verification that
matches one of the given statuses.
"""
filter_kwargs = {
'user': user,
'status__in': statuses,
}
photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
manual_id_verifications = ManualVerification.objects.filter(**filter_kwargs)
attempt = most_recent_verification((photo_id_verifications, sso_id_verifications, manual_id_verifications))
return attempt and attempt.expiration_datetime
@classmethod
def user_has_valid_or_pending(cls, user):
"""
Check whether the user has an active or pending verification attempt
Returns:
bool: True or False according to existence of valid verifications
"""
expiration_datetime = cls.get_expiration_datetime(user, ['submitted', 'approved', 'must_retry'])
if expiration_datetime:
return expiration_datetime >= now()
return False
@classmethod
def user_status(cls, user):
"""
Returns the status of the user based on their past verification attempts, and any corresponding error messages.
If no such verification exists, returns 'none'
If verification has expired, returns 'expired'
If the verification has been approved, returns 'approved'
If the verification process is still ongoing, returns 'pending'
If the verification has been denied and the user must resubmit photos, returns 'must_reverify'
This checks most recent verification
"""
# should_display only refers to displaying the verification attempt status to a user
# once a verification attempt has been made, otherwise we will display a prompt to complete ID verification.
user_status = {
'status': 'none',
'error': '',
'should_display': True,
'status_date': '',
'verification_expiry': '',
}
attempt = None
verifications = cls.verifications_for_user(user)
if verifications:
attempt = verifications[0]
for verification in verifications:
if verification.expiration_datetime > now() and verification.status == 'approved':
# Always select the LATEST non-expired approved verification if there is such
if attempt.status != 'approved' or (
attempt.expiration_datetime < verification.expiration_datetime
):
attempt = verification
if not attempt:
return user_status
user_status['should_display'] = attempt.should_display_status_to_user()
if attempt.expiration_datetime < now() and attempt.status == 'approved':
if user_status['should_display']:
user_status['status'] = 'expired'
user_status['error'] = _("Your {platform_name} verification has expired.").format(
platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
)
else:
# If we have a verification attempt that never would have displayed to the user,
# and that attempt is expired, then we should treat it as if the user had never verified.
return user_status
# If someone is denied their original verification attempt, they can try to reverify.
elif attempt.status == 'denied':
user_status['status'] = 'must_reverify'
if hasattr(attempt, 'error_msg') and attempt.error_msg:
user_status['error'] = attempt.parsed_error_msg()
elif attempt.status == 'approved':
user_status['status'] = 'approved'
expiration_datetime = cls.get_expiration_datetime(user, ['approved'])
if is_verification_expiring_soon(expiration_datetime):
user_status['verification_expiry'] = attempt.expiration_datetime.date().strftime("%m/%d/%Y")
user_status['status_date'] = attempt.status_changed
elif attempt.status in ['submitted', 'approved', 'must_retry']:
# user_has_valid_or_pending does include 'approved', but if we are
# here, we know that the attempt is still pending
user_status['status'] = 'pending'
return user_status
@classmethod
def verification_status_for_user(cls, user, user_enrollment_mode, user_is_verified=None):
"""
Returns the verification status for use in grade report.
"""
if user_enrollment_mode not in CourseMode.VERIFIED_MODES:
return 'N/A'
if user_is_verified is None:
user_is_verified = cls.user_is_verified(user)
if not user_is_verified:
return 'Not ID Verified'
else:
return 'ID Verified'
@classmethod
def get_verify_location(cls, course_id=None):
"""
Returns a string:
Returns URL for IDV on Account Microfrontend
"""
location = f'{settings.ACCOUNT_MICROFRONTEND_URL}/id-verification'
if course_id:
location += f'?course_id={quote(str(course_id))}'
return location
@classmethod
def get_verification_details_by_id(cls, attempt_id):
"""
Returns a verification attempt object by attempt_id
If the verification object cannot be found, returns None
"""
verification = None
verification_models = [
SoftwareSecurePhotoVerification,
SSOVerification,
ManualVerification,
]
for ver_model in verification_models:
if not verification:
try:
verification = ver_model.objects.get(id=attempt_id)
except ObjectDoesNotExist:
pass
return verification