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
0a657bef
Commit
0a657bef
authored
Jun 20, 2013
by
Miles Steele
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add forum list management
parent
bc9cce57
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
191 additions
and
36 deletions
+191
-36
lms/djangoapps/instructor/access.py
+24
-0
lms/djangoapps/instructor/views/api.py
+91
-8
lms/djangoapps/instructor/views/instructor_dashboard.py
+2
-0
lms/static/coffee/src/instructor_dashboard/membership.coffee
+34
-18
lms/static/sass/course/instructor/_instructor_2.scss
+9
-10
lms/templates/courseware/instructor_dashboard_2/membership.html
+27
-0
lms/urls.py
+4
-0
No files found.
lms/djangoapps/instructor/access.py
View file @
0a657bef
...
@@ -11,6 +11,10 @@ TODO sync instructor and staff flags
...
@@ -11,6 +11,10 @@ TODO sync instructor and staff flags
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
courseware.access
import
get_access_group_name
from
courseware.access
import
get_access_group_name
from
django_comment_common.models
import
(
Role
,
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_MODERATOR
,
FORUM_ROLE_COMMUNITY_TA
)
def
list_with_level
(
course
,
level
):
def
list_with_level
(
course
,
level
):
...
@@ -55,3 +59,23 @@ def _change_access(course, user, level, mode):
...
@@ -55,3 +59,23 @@ def _change_access(course, user, level, mode):
user
.
groups
.
remove
(
group
)
user
.
groups
.
remove
(
group
)
else
:
else
:
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
def
update_forum_role_membership
(
course_id
,
user
,
rolename
,
mode
):
"""
Change forum access of user.
rolename is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
mode is one of ['alow', 'revoke']
"""
role
=
Role
.
objects
.
get
(
course_id
=
course_id
,
name
=
rolename
)
if
mode
==
'allow'
:
role
.
users
.
add
(
user
)
elif
mode
==
'revoke'
:
role
.
users
.
remove
(
user
)
print
"
\n
"
*
5
print
role
.
users
.
all
()
else
:
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
lms/djangoapps/instructor/views/api.py
View file @
0a657bef
...
@@ -15,11 +15,15 @@ from django.http import HttpResponse, HttpResponseBadRequest
...
@@ -15,11 +15,15 @@ from django.http import HttpResponse, HttpResponseBadRequest
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_with_access
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
django_comment_common.models
import
(
Role
,
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_MODERATOR
,
FORUM_ROLE_COMMUNITY_TA
)
from
courseware.models
import
StudentModule
from
courseware.models
import
StudentModule
import
instructor.enrollment
as
enrollment
import
instructor.enrollment
as
enrollment
from
instructor.enrollment
import
split_input_list
,
enroll_emails
,
unenroll_emails
from
instructor.enrollment
import
split_input_list
,
enroll_emails
,
unenroll_emails
from
instructor.access
import
allow_access
,
revoke_access
,
list_with_level
import
instructor.access
as
access
import
analytics.basic
import
analytics.basic
import
analytics.distributions
import
analytics.distributions
import
analytics.csvs
import
analytics.csvs
...
@@ -57,21 +61,21 @@ def access_allow_revoke(request, course_id):
...
@@ -57,21 +61,21 @@ def access_allow_revoke(request, course_id):
Query parameters:
Query parameters:
email is the target users email
email is the target users email
level
is one of ['instructor', 'staff']
rolename
is one of ['instructor', 'staff']
mode is one of ['allow', 'revoke']
mode is one of ['allow', 'revoke']
"""
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'instructor'
,
depth
=
None
)
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'instructor'
,
depth
=
None
)
email
=
request
.
GET
.
get
(
'email'
)
email
=
request
.
GET
.
get
(
'email'
)
level
=
request
.
GET
.
get
(
'level
'
)
rolename
=
request
.
GET
.
get
(
'rolename
'
)
mode
=
request
.
GET
.
get
(
'mode'
)
mode
=
request
.
GET
.
get
(
'mode'
)
user
=
User
.
objects
.
get
(
email
=
email
)
user
=
User
.
objects
.
get
(
email
=
email
)
if
mode
==
'allow'
:
if
mode
==
'allow'
:
a
llow_access
(
course
,
user
,
level
)
a
ccess
.
allow_access
(
course
,
user
,
rolename
)
elif
mode
==
'revoke'
:
elif
mode
==
'revoke'
:
revoke_access
(
course
,
user
,
level
)
access
.
revoke_access
(
course
,
user
,
rolename
)
else
:
else
:
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
raise
ValueError
(
"unrecognized mode '{}'"
.
format
(
mode
))
...
@@ -88,10 +92,17 @@ def list_instructors_staff(request, course_id):
...
@@ -88,10 +92,17 @@ def list_instructors_staff(request, course_id):
"""
"""
List instructors and staff.
List instructors and staff.
Requires staff access.
Requires staff access.
rolename is one of ['instructor', 'staff']
"""
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
def
extract_user
(
user
):
rolename
=
request
.
GET
.
get
(
'rolename'
,
''
)
if
not
rolename
in
[
'instructor'
,
'staff'
]:
return
HttpResponseBadRequest
()
def
extract_user_info
(
user
):
return
{
return
{
'username'
:
user
.
username
,
'username'
:
user
.
username
,
'email'
:
user
.
email
,
'email'
:
user
.
email
,
...
@@ -101,8 +112,7 @@ def list_instructors_staff(request, course_id):
...
@@ -101,8 +112,7 @@ def list_instructors_staff(request, course_id):
response_payload
=
{
response_payload
=
{
'course_id'
:
course_id
,
'course_id'
:
course_id
,
'instructor'
:
map
(
extract_user
,
list_with_level
(
course
,
'instructor'
)),
rolename
:
map
(
extract_user_info
,
access
.
list_with_level
(
course
,
rolename
)),
'staff'
:
map
(
extract_user
,
list_with_level
(
course
,
'staff'
)),
}
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
return
response
...
@@ -301,3 +311,76 @@ def reset_student_attempts(request, course_id):
...
@@ -301,3 +311,76 @@ def reset_student_attempts(request, course_id):
}
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
list_forum_members
(
request
,
course_id
):
"""
Resets a students attempts counter. Optionally deletes student state for a problem.
Limited to staff access.
Takes query parameter rolename
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
rolename
=
request
.
GET
.
get
(
'rolename'
,
''
)
if
not
rolename
in
[
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_MODERATOR
,
FORUM_ROLE_COMMUNITY_TA
]:
return
HttpResponseBadRequest
()
try
:
role
=
Role
.
objects
.
get
(
name
=
rolename
,
course_id
=
course_id
)
users
=
role
.
users
.
all
()
.
order_by
(
'username'
)
except
Role
.
DoesNotExist
:
users
=
[]
def
extract_user_info
(
user
):
return
{
'username'
:
user
.
username
,
'email'
:
user
.
email
,
'first_name'
:
user
.
first_name
,
'last_name'
:
user
.
last_name
,
}
response_payload
=
{
'course_id'
:
course_id
,
rolename
:
map
(
extract_user_info
,
users
),
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
def
update_forum_role_membership
(
request
,
course_id
):
"""
Modify forum role access.
Query parameters:
email is the target users email
rolename is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
mode is one of ['allow', 'revoke']
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'instructor'
,
depth
=
None
)
email
=
request
.
GET
.
get
(
'email'
,
''
)
rolename
=
request
.
GET
.
get
(
'rolename'
,
''
)
mode
=
request
.
GET
.
get
(
'mode'
,
''
)
if
not
rolename
in
[
access
.
FORUM_ROLE_ADMINISTRATOR
,
access
.
FORUM_ROLE_MODERATOR
,
access
.
FORUM_ROLE_COMMUNITY_TA
]:
return
HttpResponseBadRequest
()
try
:
user
=
User
.
objects
.
get
(
email
=
email
)
access
.
update_forum_role_membership
(
course_id
,
user
,
rolename
,
mode
)
except
User
.
DoesNotExist
,
Role
.
DoesNotExist
:
return
HttpResponseBadRequest
()
response_payload
=
{
'course_id'
:
course_id
,
'mode'
:
mode
,
'DONE'
:
'YES'
,
}
response
=
HttpResponse
(
json
.
dumps
(
response_payload
),
content_type
=
"application/json"
)
return
response
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
0a657bef
...
@@ -109,6 +109,8 @@ def _section_membership(course_id):
...
@@ -109,6 +109,8 @@ def _section_membership(course_id):
'unenroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'unenroll_button_url'
:
reverse
(
'enroll_unenroll'
,
kwargs
=
{
'course_id'
:
course_id
}),
'list_instructors_staff_url'
:
reverse
(
'list_instructors_staff'
,
kwargs
=
{
'course_id'
:
course_id
}),
'list_instructors_staff_url'
:
reverse
(
'list_instructors_staff'
,
kwargs
=
{
'course_id'
:
course_id
}),
'access_allow_revoke_url'
:
reverse
(
'access_allow_revoke'
,
kwargs
=
{
'course_id'
:
course_id
}),
'access_allow_revoke_url'
:
reverse
(
'access_allow_revoke'
,
kwargs
=
{
'course_id'
:
course_id
}),
'list_forum_members_url'
:
reverse
(
'list_forum_members'
,
kwargs
=
{
'course_id'
:
course_id
}),
'update_forum_role_membership_url'
:
reverse
(
'update_forum_role_membership'
,
kwargs
=
{
'course_id'
:
course_id
}),
}
}
return
section_data
return
section_data
...
...
lms/static/coffee/src/instructor_dashboard/membership.coffee
View file @
0a657bef
...
@@ -94,26 +94,26 @@ class BatchEnrollment
...
@@ -94,26 +94,26 @@ class BatchEnrollment
# manages a list of instructors or staff and the control of their access.
# manages a list of instructors or staff and the control of their access.
class
AuthorityList
class
AuthList
# level is in ['instructor', 'staff']
# rolename is in ['instructor', 'staff'] for instructor_staff endpoints
constructor
:
(
@
$container
,
@
level
)
->
# rolename is the name of Role for forums for the forum endpoints
log
'setting up instructor dashboard subsection - authlist management for #{@level}'
constructor
:
(
@
$container
,
@
rolename
)
->
log
"setting up instructor dashboard subsection - authlist management for
#{
@
rolename
}
"
@
$display_table
=
@
$container
.
find
(
'.auth-list-table'
)
@
$display_table
=
@
$container
.
find
(
'.auth-list-table'
)
$add_section
=
@
$container
.
find
(
'.auth-list-add'
)
@
$add_section
=
@
$container
.
find
(
'.auth-list-add'
)
$allow_field
=
$add_section
.
find
(
"input[name='email']"
)
$allow_field
=
@
$add_section
.
find
(
"input[name='email']"
)
$allow_button
=
$add_section
.
find
(
"input[name='allow']"
)
$allow_button
=
@
$add_section
.
find
(
"input[name='allow']"
)
@
list_endpoint
=
@
$display_table
.
data
'endpoint'
@
access_change_endpoint
=
$add_section
.
data
'endpoint'
$allow_button
.
click
=>
$allow_button
.
click
=>
@
access_change
(
$allow_field
.
val
(),
@
level
,
'allow'
,
@
reload_auth_list
)
@
access_change
(
$allow_field
.
val
(),
@
rolename
,
'allow'
,
@
reload_auth_list
)
$allow_field
.
val
''
$allow_field
.
val
''
@
reload_auth_list
()
@
reload_auth_list
()
reload_auth_list
:
=>
reload_auth_list
:
=>
$
.
getJSON
@
list_endpoint
,
(
data
)
=>
list_endpoint
=
@
$display_table
.
data
'endpoint'
$
.
getJSON
list_endpoint
,
{
rolename
:
@
rolename
},
(
data
)
=>
log
data
log
data
@
$display_table
.
empty
()
@
$display_table
.
empty
()
...
@@ -138,7 +138,7 @@ class AuthorityList
...
@@ -138,7 +138,7 @@ class AuthorityList
"<span class='revoke-link'>Revoke Access</span>"
"<span class='revoke-link'>Revoke Access</span>"
]
]
table_data
=
data
[
@
level
]
table_data
=
data
[
@
rolename
]
log
'table_data'
,
table_data
log
'table_data'
,
table_data
$table_placeholder
=
$
'<div/>'
,
class
:
'slickgrid'
$table_placeholder
=
$
'<div/>'
,
class
:
'slickgrid'
...
@@ -150,11 +150,11 @@ class AuthorityList
...
@@ -150,11 +150,11 @@ class AuthorityList
grid
.
onClick
.
subscribe
(
e
,
args
)
=>
grid
.
onClick
.
subscribe
(
e
,
args
)
=>
item
=
args
.
grid
.
getDataItem
(
args
.
row
)
item
=
args
.
grid
.
getDataItem
(
args
.
row
)
if
args
.
cell
is
2
if
args
.
cell
is
2
@
access_change
(
item
.
email
,
@
level
,
'revoke'
,
@
reload_auth_list
)
@
access_change
(
item
.
email
,
@
rolename
,
'revoke'
,
@
reload_auth_list
)
access_change
:
(
email
,
level
,
mode
,
cb
)
->
access_change
:
(
email
,
rolename
,
mode
,
cb
)
->
url
=
@
access_change_endpoint
access_change_endpoint
=
@
$add_section
.
data
'endpoint'
$
.
getJSON
@
access_change_endpoint
,
{
email
:
email
,
level
:
@
level
,
mode
:
mode
},
(
data
)
->
$
.
getJSON
access_change_endpoint
,
{
email
:
email
,
rolename
:
@
rolename
,
mode
:
mode
},
(
data
)
->
log
data
log
data
cb
?
()
cb
?
()
...
@@ -166,15 +166,31 @@ class Membership
...
@@ -166,15 +166,31 @@ class Membership
# isolate sections from each other's errors.
# isolate sections from each other's errors.
plantTimeout
0
,
=>
@
batchenrollment
=
new
BatchEnrollment
@
$section
.
find
'.batch-enrollment'
plantTimeout
0
,
=>
@
batchenrollment
=
new
BatchEnrollment
@
$section
.
find
'.batch-enrollment'
plantTimeout
0
,
=>
@
stafflist
=
new
AuthorityList
(
@
$section
.
find
'.auth-list-container.auth-list-staff'
),
'staff'
plantTimeout
0
,
=>
@
stafflist
=
new
AuthList
(
@
$section
.
find
'.auth-list-container.auth-list-staff'
),
'staff'
plantTimeout
0
,
=>
@
instructorlist
=
new
AuthorityList
(
@
$section
.
find
'.auth-list-container.auth-list-instructor'
),
'instructor'
plantTimeout
0
,
=>
@
instructorlist
=
new
AuthList
(
@
$section
.
find
'.auth-list-container.auth-list-instructor'
),
'instructor'
# TODO names like 'Administrator' should come from server through template.
plantTimeout
0
,
=>
@
forum_admin_list
=
new
AuthList
(
@
$section
.
find
'.auth-list-container.auth-list-forum-admin'
),
'Administrator'
plantTimeout
0
,
=>
@
forum_mod_list
=
new
AuthList
(
@
$section
.
find
'.auth-list-container.auth-list-forum-moderator'
),
'Moderator'
plantTimeout
0
,
=>
@
forum_comta_list
=
new
AuthList
(
@
$section
.
find
'.auth-list-container.auth-list-forum-community-ta'
),
'Community TA'
onClickTitle
:
->
onClickTitle
:
->
@
stafflist
.
$display_table
.
empty
()
@
stafflist
.
$display_table
.
empty
()
@
stafflist
.
reload_auth_list
()
@
stafflist
.
reload_auth_list
()
@
instructorlist
.
$display_table
.
empty
()
@
instructorlist
.
$display_table
.
empty
()
@
instructorlist
.
reload_auth_list
()
@
instructorlist
.
reload_auth_list
()
@
forum_admin_list
.
$display_table
.
empty
()
@
forum_admin_list
.
reload_auth_list
()
@
forum_mod_list
.
$display_table
.
empty
()
@
forum_mod_list
.
reload_auth_list
()
@
forum_comta_list
.
$display_table
.
empty
()
@
forum_comta_list
.
reload_auth_list
()
# exports
# exports
_
.
defaults
window
,
InstructorDashboard
:
{}
_
.
defaults
window
,
InstructorDashboard
:
{}
...
...
lms/static/sass/course/instructor/_instructor_2.scss
View file @
0a657bef
...
@@ -92,21 +92,20 @@
...
@@ -92,21 +92,20 @@
.vert-right
{
.vert-right
{
float
:
right
;
float
:
right
;
width
:
45%
;
width
:
45%
;
}
.auth-list-container
{
.auth-list-container
{
margin-bottom
:
1
.5em
;
margin-bottom
:
1
.5em
;
.auth-list-table
{
.slickgrid
{
height
:
250px
;
}
}
.auth-list-add
{
.auth-list-table
{
margin-top
:
0
.5em
;
.slickgrid
{
height
:
250px
;
}
}
}
}
.auth-list-add
{
margin-top
:
0
.5em
;
}
}
}
.batch-enrollment
{
.batch-enrollment
{
...
...
lms/templates/courseware/instructor_dashboard_2/membership.html
View file @
0a657bef
...
@@ -8,6 +8,33 @@
...
@@ -8,6 +8,33 @@
<input
type=
"button"
name=
"enroll"
value=
"Enroll"
data-endpoint=
"${ section_data['enroll_button_url'] }"
>
<input
type=
"button"
name=
"enroll"
value=
"Enroll"
data-endpoint=
"${ section_data['enroll_button_url'] }"
>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
<input
type=
"button"
name=
"unenroll"
value=
"Unenroll"
data-endpoint=
"${ section_data['unenroll_button_url'] }"
>
<div
class=
"task-response"
></div>
<div
class=
"task-response"
></div>
<div
class=
"auth-list-container auth-list-forum-admin"
>
<h2>
Instructor Management
</h2>
<div
class=
"auth-list-table"
data-endpoint=
"${ section_data['list_forum_members_url'] }"
></div>
<div
class=
"auth-list-add"
data-endpoint=
"${ section_data['update_forum_role_membership_url'] }"
>
<input
type=
"text"
name=
"email"
placeholder=
"Enter Email"
spellcheck=
"false"
>
<input
type=
"button"
name=
"allow"
value=
"Grant Forum Admin"
>
</div>
</div>
<div
class=
"auth-list-container auth-list-forum-moderator"
>
<h2>
Instructor Management
</h2>
<div
class=
"auth-list-table"
data-endpoint=
"${ section_data['list_forum_members_url'] }"
></div>
<div
class=
"auth-list-add"
data-endpoint=
"${ section_data['update_forum_role_membership_url'] }"
>
<input
type=
"text"
name=
"email"
placeholder=
"Enter Email"
spellcheck=
"false"
>
<input
type=
"button"
name=
"allow"
value=
"Grant Forum Moderator"
>
</div>
</div>
<div
class=
"auth-list-container auth-list-forum-community-ta"
>
<h2>
Instructor Management
</h2>
<div
class=
"auth-list-table"
data-endpoint=
"${ section_data['list_forum_members_url'] }"
></div>
<div
class=
"auth-list-add"
data-endpoint=
"${ section_data['update_forum_role_membership_url'] }"
>
<input
type=
"text"
name=
"email"
placeholder=
"Enter Email"
spellcheck=
"false"
>
<input
type=
"button"
name=
"allow"
value=
"Grant Community TA"
>
</div>
</div>
</div>
</div>
<div
class=
"vert-right instructor-staff-management"
>
<div
class=
"vert-right instructor-staff-management"
>
...
...
lms/urls.py
View file @
0a657bef
...
@@ -271,6 +271,10 @@ if settings.COURSEWARE_ENABLED:
...
@@ -271,6 +271,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.api.get_student_progress_url'
,
name
=
"get_student_progress_url"
),
'instructor.views.api.get_student_progress_url'
,
name
=
"get_student_progress_url"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$'
,
'instructor.views.api.reset_student_attempts'
,
name
=
"reset_student_attempts"
),
'instructor.views.api.reset_student_attempts'
,
name
=
"reset_student_attempts"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_forum_members$'
,
'instructor.views.api.list_forum_members'
,
name
=
"list_forum_members"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/update_forum_role_membership$'
,
'instructor.views.api.update_forum_role_membership'
,
name
=
"update_forum_role_membership"
),
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$'
,
url
(
r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$'
,
'instructor.views.legacy.gradebook'
,
name
=
'gradebook'
),
'instructor.views.legacy.gradebook'
,
name
=
'gradebook'
),
...
...
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