Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
2b404622
Commit
2b404622
authored
Jul 22, 2013
by
Adam Palay
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
convert all datetime.now() to datetime.now(UTC)
parent
b2b3a504
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
71 additions
and
53 deletions
+71
-53
common/djangoapps/heartbeat/views.py
+3
-1
common/djangoapps/student/models.py
+30
-28
common/djangoapps/student/views.py
+2
-2
common/lib/capa/capa/tests/test_responsetypes.py
+2
-2
common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+7
-3
i18n/tests/test_extract.py
+15
-10
i18n/tests/test_generate.py
+8
-3
lms/djangoapps/certificates/management/commands/ungenerated_certs.py
+4
-4
No files found.
common/djangoapps/heartbeat/views.py
View file @
2b404622
import
json
import
json
from
datetime
import
datetime
from
datetime
import
datetime
from
pytz
import
UTC
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
dogapi
import
dog_stats_api
from
dogapi
import
dog_stats_api
@dog_stats_api.timed
(
'edxapp.heartbeat'
)
@dog_stats_api.timed
(
'edxapp.heartbeat'
)
def
heartbeat
(
request
):
def
heartbeat
(
request
):
"""
"""
Simple view that a loadbalancer can check to verify that the app is up
Simple view that a loadbalancer can check to verify that the app is up
"""
"""
output
=
{
output
=
{
'date'
:
datetime
.
now
()
.
isoformat
(),
'date'
:
datetime
.
now
(
UTC
)
.
isoformat
(),
'courses'
:
[
course
.
location
.
url
()
for
course
in
modulestore
()
.
get_courses
()],
'courses'
:
[
course
.
location
.
url
()
for
course
in
modulestore
()
.
get_courses
()],
}
}
return
HttpResponse
(
json
.
dumps
(
output
,
indent
=
4
))
return
HttpResponse
(
json
.
dumps
(
output
,
indent
=
4
))
common/djangoapps/student/models.py
View file @
2b404622
...
@@ -69,30 +69,33 @@ class UserProfile(models.Model):
...
@@ -69,30 +69,33 @@ class UserProfile(models.Model):
location
=
models
.
CharField
(
blank
=
True
,
max_length
=
255
,
db_index
=
True
)
location
=
models
.
CharField
(
blank
=
True
,
max_length
=
255
,
db_index
=
True
)
# Optional demographic data we started capturing from Fall 2012
# Optional demographic data we started capturing from Fall 2012
this_year
=
datetime
.
now
()
.
year
this_year
=
datetime
.
now
(
UTC
)
.
year
VALID_YEARS
=
range
(
this_year
,
this_year
-
120
,
-
1
)
VALID_YEARS
=
range
(
this_year
,
this_year
-
120
,
-
1
)
year_of_birth
=
models
.
IntegerField
(
blank
=
True
,
null
=
True
,
db_index
=
True
)
year_of_birth
=
models
.
IntegerField
(
blank
=
True
,
null
=
True
,
db_index
=
True
)
GENDER_CHOICES
=
((
'm'
,
'Male'
),
(
'f'
,
'Female'
),
(
'o'
,
'Other'
))
GENDER_CHOICES
=
((
'm'
,
'Male'
),
(
'f'
,
'Female'
),
(
'o'
,
'Other'
))
gender
=
models
.
CharField
(
blank
=
True
,
null
=
True
,
max_length
=
6
,
db_index
=
True
,
gender
=
models
.
CharField
(
choices
=
GENDER_CHOICES
)
blank
=
True
,
null
=
True
,
max_length
=
6
,
db_index
=
True
,
choices
=
GENDER_CHOICES
)
# [03/21/2013] removed these, but leaving comment since there'll still be
# [03/21/2013] removed these, but leaving comment since there'll still be
# p_se and p_oth in the existing data in db.
# p_se and p_oth in the existing data in db.
# ('p_se', 'Doctorate in science or engineering'),
# ('p_se', 'Doctorate in science or engineering'),
# ('p_oth', 'Doctorate in another field'),
# ('p_oth', 'Doctorate in another field'),
LEVEL_OF_EDUCATION_CHOICES
=
((
'p'
,
'Doctorate'
),
LEVEL_OF_EDUCATION_CHOICES
=
(
(
'm'
,
"Master's or professional degree"
),
(
'p'
,
'Doctorate'
),
(
'b'
,
"Bachelor's degree"
),
(
'm'
,
"Master's or professional degree"
),
(
'a'
,
"Associate's degree"
),
(
'b'
,
"Bachelor's degree"
),
(
'hs'
,
"Secondary/high school"
),
(
'a'
,
"Associate's degree"
),
(
'jhs'
,
"Junior secondary/junior high/middle school"
),
(
'hs'
,
"Secondary/high school"
),
(
'el'
,
"Elementary/primary school"
),
(
'jhs'
,
"Junior secondary/junior high/middle school"
),
(
'none'
,
"None"
),
(
'el'
,
"Elementary/primary school"
),
(
'other'
,
"Other"
))
(
'none'
,
"None"
),
(
'other'
,
"Other"
)
)
level_of_education
=
models
.
CharField
(
level_of_education
=
models
.
CharField
(
blank
=
True
,
null
=
True
,
max_length
=
6
,
db_index
=
True
,
blank
=
True
,
null
=
True
,
max_length
=
6
,
db_index
=
True
,
choices
=
LEVEL_OF_EDUCATION_CHOICES
choices
=
LEVEL_OF_EDUCATION_CHOICES
)
)
mailing_address
=
models
.
TextField
(
blank
=
True
,
null
=
True
)
mailing_address
=
models
.
TextField
(
blank
=
True
,
null
=
True
)
goals
=
models
.
TextField
(
blank
=
True
,
null
=
True
)
goals
=
models
.
TextField
(
blank
=
True
,
null
=
True
)
allow_certificate
=
models
.
BooleanField
(
default
=
1
)
allow_certificate
=
models
.
BooleanField
(
default
=
1
)
...
@@ -307,18 +310,18 @@ class TestCenterUserForm(ModelForm):
...
@@ -307,18 +310,18 @@ class TestCenterUserForm(ModelForm):
ACCOMMODATION_REJECTED_CODE
=
'NONE'
ACCOMMODATION_REJECTED_CODE
=
'NONE'
ACCOMMODATION_CODES
=
(
ACCOMMODATION_CODES
=
(
(
ACCOMMODATION_REJECTED_CODE
,
'No Accommodation Granted'
),
(
ACCOMMODATION_REJECTED_CODE
,
'No Accommodation Granted'
),
(
'EQPMNT'
,
'Equipment'
),
(
'EQPMNT'
,
'Equipment'
),
(
'ET12ET'
,
'Extra Time - 1/2 Exam Time'
),
(
'ET12ET'
,
'Extra Time - 1/2 Exam Time'
),
(
'ET30MN'
,
'Extra Time - 30 Minutes'
),
(
'ET30MN'
,
'Extra Time - 30 Minutes'
),
(
'ETDBTM'
,
'Extra Time - Double Time'
),
(
'ETDBTM'
,
'Extra Time - Double Time'
),
(
'SEPRMM'
,
'Separate Room'
),
(
'SEPRMM'
,
'Separate Room'
),
(
'SRREAD'
,
'Separate Room and Reader'
),
(
'SRREAD'
,
'Separate Room and Reader'
),
(
'SRRERC'
,
'Separate Room and Reader/Recorder'
),
(
'SRRERC'
,
'Separate Room and Reader/Recorder'
),
(
'SRRECR'
,
'Separate Room and Recorder'
),
(
'SRRECR'
,
'Separate Room and Recorder'
),
(
'SRSEAN'
,
'Separate Room and Service Animal'
),
(
'SRSEAN'
,
'Separate Room and Service Animal'
),
(
'SRSGNR'
,
'Separate Room and Sign Language Interpreter'
),
(
'SRSGNR'
,
'Separate Room and Sign Language Interpreter'
),
)
)
ACCOMMODATION_CODE_DICT
=
{
code
:
name
for
(
code
,
name
)
in
ACCOMMODATION_CODES
}
ACCOMMODATION_CODE_DICT
=
{
code
:
name
for
(
code
,
name
)
in
ACCOMMODATION_CODES
}
...
@@ -572,7 +575,6 @@ class TestCenterRegistrationForm(ModelForm):
...
@@ -572,7 +575,6 @@ class TestCenterRegistrationForm(ModelForm):
return
code
return
code
def
get_testcenter_registration
(
user
,
course_id
,
exam_series_code
):
def
get_testcenter_registration
(
user
,
course_id
,
exam_series_code
):
try
:
try
:
tcu
=
TestCenterUser
.
objects
.
get
(
user
=
user
)
tcu
=
TestCenterUser
.
objects
.
get
(
user
=
user
)
...
...
common/djangoapps/student/views.py
View file @
2b404622
...
@@ -1100,7 +1100,7 @@ def confirm_email_change(request, key):
...
@@ -1100,7 +1100,7 @@ def confirm_email_change(request, key):
meta
=
up
.
get_meta
()
meta
=
up
.
get_meta
()
if
'old_emails'
not
in
meta
:
if
'old_emails'
not
in
meta
:
meta
[
'old_emails'
]
=
[]
meta
[
'old_emails'
]
=
[]
meta
[
'old_emails'
]
.
append
([
user
.
email
,
datetime
.
datetime
.
now
()
.
isoformat
()])
meta
[
'old_emails'
]
.
append
([
user
.
email
,
datetime
.
datetime
.
now
(
UTC
)
.
isoformat
()])
up
.
set_meta
(
meta
)
up
.
set_meta
(
meta
)
up
.
save
()
up
.
save
()
# Send it to the old email...
# Send it to the old email...
...
@@ -1198,7 +1198,7 @@ def accept_name_change_by_id(id):
...
@@ -1198,7 +1198,7 @@ def accept_name_change_by_id(id):
meta
=
up
.
get_meta
()
meta
=
up
.
get_meta
()
if
'old_names'
not
in
meta
:
if
'old_names'
not
in
meta
:
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
.
append
([
up
.
name
,
pnc
.
rationale
,
datetime
.
datetime
.
now
()
.
isoformat
()])
meta
[
'old_names'
]
.
append
([
up
.
name
,
pnc
.
rationale
,
datetime
.
datetime
.
now
(
UTC
)
.
isoformat
()])
up
.
set_meta
(
meta
)
up
.
set_meta
(
meta
)
up
.
name
=
pnc
.
new_name
up
.
name
=
pnc
.
new_name
...
...
common/lib/capa/capa/tests/test_responsetypes.py
View file @
2b404622
...
@@ -705,7 +705,7 @@ class CodeResponseTest(ResponseTest):
...
@@ -705,7 +705,7 @@ class CodeResponseTest(ResponseTest):
# Now we queue the LCP
# Now we queue the LCP
cmap
=
CorrectMap
()
cmap
=
CorrectMap
()
for
i
,
answer_id
in
enumerate
(
answer_ids
):
for
i
,
answer_id
in
enumerate
(
answer_ids
):
queuestate
=
CodeResponseTest
.
make_queuestate
(
i
,
datetime
.
now
())
queuestate
=
CodeResponseTest
.
make_queuestate
(
i
,
datetime
.
now
(
UTC
))
cmap
.
update
(
CorrectMap
(
answer_id
=
answer_ids
[
i
],
queuestate
=
queuestate
))
cmap
.
update
(
CorrectMap
(
answer_id
=
answer_ids
[
i
],
queuestate
=
queuestate
))
self
.
problem
.
correct_map
.
update
(
cmap
)
self
.
problem
.
correct_map
.
update
(
cmap
)
...
@@ -721,7 +721,7 @@ class CodeResponseTest(ResponseTest):
...
@@ -721,7 +721,7 @@ class CodeResponseTest(ResponseTest):
old_cmap
=
CorrectMap
()
old_cmap
=
CorrectMap
()
for
i
,
answer_id
in
enumerate
(
answer_ids
):
for
i
,
answer_id
in
enumerate
(
answer_ids
):
queuekey
=
1000
+
i
queuekey
=
1000
+
i
queuestate
=
CodeResponseTest
.
make_queuestate
(
queuekey
,
datetime
.
now
())
queuestate
=
CodeResponseTest
.
make_queuestate
(
queuekey
,
datetime
.
now
(
UTC
))
old_cmap
.
update
(
CorrectMap
(
answer_id
=
answer_ids
[
i
],
queuestate
=
queuestate
))
old_cmap
.
update
(
CorrectMap
(
answer_id
=
answer_ids
[
i
],
queuestate
=
queuestate
))
# Message format common to external graders
# Message format common to external graders
...
...
common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
View file @
2b404622
...
@@ -5,6 +5,7 @@ import re
...
@@ -5,6 +5,7 @@ import re
import
open_ended_image_submission
import
open_ended_image_submission
from
xmodule.progress
import
Progress
from
xmodule.progress
import
Progress
import
capa.xqueue_interface
as
xqueue_interface
from
capa.util
import
*
from
capa.util
import
*
from
.peer_grading_service
import
PeerGradingService
,
MockPeerGradingService
from
.peer_grading_service
import
PeerGradingService
,
MockPeerGradingService
import
controller_query_service
import
controller_query_service
...
@@ -334,12 +335,15 @@ class OpenEndedChild(object):
...
@@ -334,12 +335,15 @@ class OpenEndedChild(object):
log
.
exception
(
"Could not create image and check it."
)
log
.
exception
(
"Could not create image and check it."
)
if
image_ok
:
if
image_ok
:
image_key
=
image_data
.
name
+
datetime
.
now
()
.
strftime
(
"
%
Y
%
m
%
d
%
H
%
M
%
S"
)
image_key
=
image_data
.
name
+
datetime
.
now
(
UTC
)
.
strftime
(
xqueue_interface
.
dateformat
)
try
:
try
:
image_data
.
seek
(
0
)
image_data
.
seek
(
0
)
success
,
s3_public_url
=
open_ended_image_submission
.
upload_to_s3
(
image_data
,
image_key
,
success
,
s3_public_url
=
open_ended_image_submission
.
upload_to_s3
(
self
.
s3_interface
)
image_data
,
image_key
,
self
.
s3_interface
)
except
:
except
:
log
.
exception
(
"Could not upload image to S3."
)
log
.
exception
(
"Could not upload image to S3."
)
...
...
i18n/tests/test_extract.py
View file @
2b404622
import
os
,
polib
import
os
import
polib
from
unittest
import
TestCase
from
unittest
import
TestCase
from
nose.plugins.skip
import
SkipTest
from
nose.plugins.skip
import
SkipTest
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
from
pytz
import
UTC
import
extract
import
extract
from
config
import
CONFIGURATION
from
config
import
CONFIGURATION
...
@@ -9,6 +11,7 @@ from config import CONFIGURATION
...
@@ -9,6 +11,7 @@ from config import CONFIGURATION
# Make sure setup runs only once
# Make sure setup runs only once
SETUP_HAS_RUN
=
False
SETUP_HAS_RUN
=
False
class
TestExtract
(
TestCase
):
class
TestExtract
(
TestCase
):
"""
"""
Tests functionality of i18n/extract.py
Tests functionality of i18n/extract.py
...
@@ -19,20 +22,20 @@ class TestExtract(TestCase):
...
@@ -19,20 +22,20 @@ class TestExtract(TestCase):
# Skip this test because it takes too long (>1 minute)
# Skip this test because it takes too long (>1 minute)
# TODO: figure out how to declare a "long-running" test suite
# TODO: figure out how to declare a "long-running" test suite
# and add this test to it.
# and add this test to it.
raise
SkipTest
()
raise
SkipTest
()
global
SETUP_HAS_RUN
global
SETUP_HAS_RUN
# Subtract 1 second to help comparisons with file-modify time succeed,
# Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate
# since os.path.getmtime() is not millisecond-accurate
self
.
start_time
=
datetime
.
now
()
-
timedelta
(
seconds
=
1
)
self
.
start_time
=
datetime
.
now
(
UTC
)
-
timedelta
(
seconds
=
1
)
super
(
TestExtract
,
self
)
.
setUp
()
super
(
TestExtract
,
self
)
.
setUp
()
if
not
SETUP_HAS_RUN
:
if
not
SETUP_HAS_RUN
:
# Run extraction script. Warning, this takes 1 minute or more
# Run extraction script. Warning, this takes 1 minute or more
extract
.
main
()
extract
.
main
()
SETUP_HAS_RUN
=
True
SETUP_HAS_RUN
=
True
def
get_files
(
self
):
def
get_files
(
self
):
"""
"""
This is a generator.
This is a generator.
Returns the fully expanded filenames for all extracted files
Returns the fully expanded filenames for all extracted files
...
@@ -65,19 +68,21 @@ class TestExtract(TestCase):
...
@@ -65,19 +68,21 @@ class TestExtract(TestCase):
entry2
.
msgid
=
"This is not a keystring"
entry2
.
msgid
=
"This is not a keystring"
self
.
assertTrue
(
extract
.
is_key_string
(
entry1
.
msgid
))
self
.
assertTrue
(
extract
.
is_key_string
(
entry1
.
msgid
))
self
.
assertFalse
(
extract
.
is_key_string
(
entry2
.
msgid
))
self
.
assertFalse
(
extract
.
is_key_string
(
entry2
.
msgid
))
def
test_headers
(
self
):
def
test_headers
(
self
):
"""Verify all headers have been modified"""
"""Verify all headers have been modified"""
for
path
in
self
.
get_files
():
for
path
in
self
.
get_files
():
po
=
polib
.
pofile
(
path
)
po
=
polib
.
pofile
(
path
)
header
=
po
.
header
header
=
po
.
header
self
.
assertEqual
(
header
.
find
(
'edX translation file'
),
0
,
self
.
assertEqual
(
msg
=
'Missing header in
%
s:
\n
"
%
s"'
%
\
header
.
find
(
'edX translation file'
),
(
os
.
path
.
basename
(
path
),
header
))
0
,
msg
=
'Missing header in
%
s:
\n
"
%
s"'
%
(
os
.
path
.
basename
(
path
),
header
)
)
def
test_metadata
(
self
):
def
test_metadata
(
self
):
"""Verify all metadata has been modified"""
"""Verify all metadata has been modified"""
for
path
in
self
.
get_files
():
for
path
in
self
.
get_files
():
po
=
polib
.
pofile
(
path
)
po
=
polib
.
pofile
(
path
)
metadata
=
po
.
metadata
metadata
=
po
.
metadata
value
=
metadata
[
'Report-Msgid-Bugs-To'
]
value
=
metadata
[
'Report-Msgid-Bugs-To'
]
...
...
i18n/tests/test_generate.py
View file @
2b404622
import
os
,
string
,
random
,
re
import
os
import
string
import
random
import
re
from
polib
import
pofile
from
polib
import
pofile
from
unittest
import
TestCase
from
unittest
import
TestCase
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
from
pytz
import
UTC
import
generate
import
generate
from
config
import
CONFIGURATION
from
config
import
CONFIGURATION
class
TestGenerate
(
TestCase
):
class
TestGenerate
(
TestCase
):
"""
"""
Tests functionality of i18n/generate.py
Tests functionality of i18n/generate.py
...
@@ -15,7 +20,7 @@ class TestGenerate(TestCase):
...
@@ -15,7 +20,7 @@ class TestGenerate(TestCase):
def
setUp
(
self
):
def
setUp
(
self
):
# Subtract 1 second to help comparisons with file-modify time succeed,
# Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate
# since os.path.getmtime() is not millisecond-accurate
self
.
start_time
=
datetime
.
now
()
-
timedelta
(
seconds
=
1
)
self
.
start_time
=
datetime
.
now
(
UTC
)
-
timedelta
(
seconds
=
1
)
def
test_merge
(
self
):
def
test_merge
(
self
):
"""
"""
...
@@ -49,7 +54,7 @@ class TestGenerate(TestCase):
...
@@ -49,7 +54,7 @@ class TestGenerate(TestCase):
"""
"""
This is invoked by test_main to ensure that it runs after
This is invoked by test_main to ensure that it runs after
calling generate.main().
calling generate.main().
There should be exactly three merge comment headers
There should be exactly three merge comment headers
in our merged .po file. This counts them to be sure.
in our merged .po file. This counts them to be sure.
A merge comment looks like this:
A merge comment looks like this:
...
...
lms/djangoapps/certificates/management/commands/ungenerated_certs.py
View file @
2b404622
...
@@ -8,6 +8,7 @@ from xmodule.course_module import CourseDescriptor
...
@@ -8,6 +8,7 @@ from xmodule.course_module import CourseDescriptor
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
certificates.models
import
CertificateStatuses
from
certificates.models
import
CertificateStatuses
import
datetime
import
datetime
from
pytz
import
UTC
class
Command
(
BaseCommand
):
class
Command
(
BaseCommand
):
...
@@ -41,7 +42,6 @@ class Command(BaseCommand):
...
@@ -41,7 +42,6 @@ class Command(BaseCommand):
'whose entry in the certificate table matches STATUS. '
'whose entry in the certificate table matches STATUS. '
'STATUS can be generating, unavailable, deleted, error '
'STATUS can be generating, unavailable, deleted, error '
'or notpassing.'
),
'or notpassing.'
),
)
)
def
handle
(
self
,
*
args
,
**
options
):
def
handle
(
self
,
*
args
,
**
options
):
...
@@ -83,20 +83,20 @@ class Command(BaseCommand):
...
@@ -83,20 +83,20 @@ class Command(BaseCommand):
xq
=
XQueueCertInterface
()
xq
=
XQueueCertInterface
()
total
=
enrolled_students
.
count
()
total
=
enrolled_students
.
count
()
count
=
0
count
=
0
start
=
datetime
.
datetime
.
now
()
start
=
datetime
.
datetime
.
now
(
UTC
)
for
student
in
enrolled_students
:
for
student
in
enrolled_students
:
count
+=
1
count
+=
1
if
count
%
STATUS_INTERVAL
==
0
:
if
count
%
STATUS_INTERVAL
==
0
:
# Print a status update with an approximation of
# Print a status update with an approximation of
# how much time is left based on how long the last
# how much time is left based on how long the last
# interval took
# interval took
diff
=
datetime
.
datetime
.
now
()
-
start
diff
=
datetime
.
datetime
.
now
(
UTC
)
-
start
timeleft
=
diff
*
(
total
-
count
)
/
STATUS_INTERVAL
timeleft
=
diff
*
(
total
-
count
)
/
STATUS_INTERVAL
hours
,
remainder
=
divmod
(
timeleft
.
seconds
,
3600
)
hours
,
remainder
=
divmod
(
timeleft
.
seconds
,
3600
)
minutes
,
seconds
=
divmod
(
remainder
,
60
)
minutes
,
seconds
=
divmod
(
remainder
,
60
)
print
"{0}/{1} completed ~{2:02}:{3:02}m remaining"
.
format
(
print
"{0}/{1} completed ~{2:02}:{3:02}m remaining"
.
format
(
count
,
total
,
hours
,
minutes
)
count
,
total
,
hours
,
minutes
)
start
=
datetime
.
datetime
.
now
()
start
=
datetime
.
datetime
.
now
(
UTC
)
if
certificate_status_for_student
(
if
certificate_status_for_student
(
student
,
course_id
)[
'status'
]
in
valid_statuses
:
student
,
course_id
)[
'status'
]
in
valid_statuses
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment