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
76235d90
Commit
76235d90
authored
Aug 13, 2012
by
kimth
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into kimth/generic-coderesponse
parents
ac51cbc3
0847b46c
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
356 additions
and
222 deletions
+356
-222
common/djangoapps/static_replace.py
+2
-2
common/djangoapps/student/models.py
+23
-23
common/djangoapps/student/tests.py
+26
-9
common/djangoapps/student/views.py
+43
-12
common/lib/xmodule/xmodule/course_module.py
+22
-5
common/lib/xmodule/xmodule/css/capa/display.scss
+14
-1
common/lib/xmodule/xmodule/css/video/display.scss
+8
-38
common/lib/xmodule/xmodule/x_module.py
+2
-2
lms/djangoapps/courseware/grades.py
+3
-7
lms/djangoapps/courseware/tests/tests.py
+110
-19
lms/static/sass/course.scss
+1
-0
lms/static/sass/course/_info.scss
+11
-9
lms/static/sass/course/base/_base.scss
+1
-0
lms/static/sass/course/base/_extends.scss
+6
-20
lms/static/sass/course/courseware/_courseware.scss
+3
-3
lms/static/sass/course/courseware/_sidebar.scss
+2
-1
lms/static/sass/course/discussion/_answers.scss
+1
-2
lms/static/sass/course/discussion/_forms.scss
+1
-1
lms/static/sass/course/wiki/_sidebar.scss
+8
-8
lms/static/sass/multicourse/_dashboard.scss
+9
-14
lms/static/sass/shared/_forms.scss
+44
-42
lms/static/sass/shared/_tooltips.scss
+12
-0
lms/templates/dashboard.html
+1
-1
lms/templates/simplewiki/simplewiki_base.html
+3
-3
No files found.
common/djangoapps/static_replace.py
View file @
76235d90
...
...
@@ -17,8 +17,8 @@ def try_staticfiles_lookup(path):
except
Exception
as
err
:
log
.
warning
(
"staticfiles_storage couldn't find path {}: {}"
.
format
(
path
,
str
(
err
)))
# Just return
a dead link--
don't kill everything.
url
=
"file_not_found"
# Just return
the original path;
don't kill everything.
url
=
path
return
url
...
...
common/djangoapps/student/models.py
View file @
76235d90
...
...
@@ -257,8 +257,11 @@ def add_user_to_default_group(user, group):
########################## REPLICATION SIGNALS #################################
@receiver
(
post_save
,
sender
=
User
)
def
replicate_user_save
(
sender
,
**
kwargs
):
user_obj
=
kwargs
[
'instance'
]
return
replicate_model
(
User
.
save
,
user_obj
,
user_obj
.
id
)
user_obj
=
kwargs
[
'instance'
]
if
not
should_replicate
(
user_obj
):
return
for
course_db_name
in
db_names_to_replicate_to
(
user_obj
.
id
):
replicate_user
(
user_obj
,
course_db_name
)
@receiver
(
post_save
,
sender
=
CourseEnrollment
)
def
replicate_enrollment_save
(
sender
,
**
kwargs
):
...
...
@@ -287,8 +290,8 @@ def replicate_enrollment_save(sender, **kwargs):
@receiver
(
post_delete
,
sender
=
CourseEnrollment
)
def
replicate_enrollment_delete
(
sender
,
**
kwargs
):
enrollment_obj
=
kwargs
[
'instance'
]
return
replicate_model
(
CourseEnrollment
.
delete
,
enrollment_obj
,
enrollment_obj
.
user_id
)
enrollment_obj
=
kwargs
[
'instance'
]
return
replicate_model
(
CourseEnrollment
.
delete
,
enrollment_obj
,
enrollment_obj
.
user_id
)
@receiver
(
post_save
,
sender
=
UserProfile
)
def
replicate_userprofile_save
(
sender
,
**
kwargs
):
...
...
@@ -311,23 +314,20 @@ def replicate_user(portal_user, course_db_name):
overridden.
"""
try
:
# If the user exists in the Course DB, update the appropriate fields and
# save it back out to the Course DB.
course_user
=
User
.
objects
.
using
(
course_db_name
)
.
get
(
id
=
portal_user
.
id
)
for
field
in
USER_FIELDS_TO_COPY
:
setattr
(
course_user
,
field
,
getattr
(
portal_user
,
field
))
mark_handled
(
course_user
)
log
.
debug
(
"User {0} found in Course DB, replicating fields to {1}"
.
format
(
course_user
,
course_db_name
))
course_user
.
save
(
using
=
course_db_name
)
# Just being explicit.
except
User
.
DoesNotExist
:
# Otherwise, just make a straight copy to the Course DB.
mark_handled
(
portal_user
)
log
.
debug
(
"User {0} not found in Course DB, creating copy in {1}"
.
format
(
portal_user
,
course_db_name
))
portal_user
.
save
(
using
=
course_db_name
)
course_user
=
User
()
for
field
in
USER_FIELDS_TO_COPY
:
setattr
(
course_user
,
field
,
getattr
(
portal_user
,
field
))
mark_handled
(
course_user
)
course_user
.
save
(
using
=
course_db_name
)
unmark
(
course_user
)
def
replicate_model
(
model_method
,
instance
,
user_id
):
"""
...
...
@@ -337,13 +337,14 @@ def replicate_model(model_method, instance, user_id):
if
not
should_replicate
(
instance
):
return
mark_handled
(
instance
)
course_db_names
=
db_names_to_replicate_to
(
user_id
)
log
.
debug
(
"Replicating {0} for user {1} to DBs: {2}"
.
format
(
model_method
,
user_id
,
course_db_names
))
mark_handled
(
instance
)
for
db_name
in
course_db_names
:
model_method
(
instance
,
using
=
db_name
)
unmark
(
instance
)
######### Replication Helpers #########
...
...
@@ -371,7 +372,7 @@ def db_names_to_replicate_to(user_id):
def
marked_handled
(
instance
):
"""Have we marked this instance as being handled to avoid infinite loops
caused by saving models in post_save hooks for the same models?"""
return
hasattr
(
instance
,
'_do_not_copy_to_course_db'
)
return
hasattr
(
instance
,
'_do_not_copy_to_course_db'
)
and
instance
.
_do_not_copy_to_course_db
def
mark_handled
(
instance
):
"""You have to mark your instance with this function or else we'll go into
...
...
@@ -384,6 +385,11 @@ def mark_handled(instance):
"""
instance
.
_do_not_copy_to_course_db
=
True
def
unmark
(
instance
):
"""If we don't unmark a model after we do replication, then consecutive
save() calls won't be properly replicated."""
instance
.
_do_not_copy_to_course_db
=
False
def
should_replicate
(
instance
):
"""Should this instance be replicated? We need to be a Portal server and
the instance has to not have been marked_handled."""
...
...
@@ -398,9 +404,3 @@ def should_replicate(instance):
return
False
return
True
common/djangoapps/student/tests.py
View file @
76235d90
...
...
@@ -4,6 +4,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import
logging
from
datetime
import
datetime
from
django.test
import
TestCase
...
...
@@ -13,6 +14,8 @@ from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FI
COURSE_1
=
'edX/toy/2012_Fall'
COURSE_2
=
'edx/full/6.002_Spring_2012'
log
=
logging
.
getLogger
(
__name__
)
class
ReplicationTest
(
TestCase
):
multi_db
=
True
...
...
@@ -47,23 +50,18 @@ class ReplicationTest(TestCase):
field
,
portal_user
,
course_user
))
if
hasattr
(
portal_user
,
'seen_response_count'
):
# Since it's the first copy over of User data, we should have all of it
self
.
assertEqual
(
portal_user
.
seen_response_count
,
course_user
.
seen_response_count
)
# But if we replicate again, the user already exists in the Course DB,
# so it shouldn't update the seen_response_count (which is Askbot
# controlled).
# This hasattr lameness is here because we don't want this test to be
# triggered when we're being run by CMS tests (Askbot doesn't exist
# there, so the test will fail).
#
# seen_response_count isn't a field we care about, so it shouldn't have
# been copied over.
if
hasattr
(
portal_user
,
'seen_response_count'
):
portal_user
.
seen_response_count
=
20
replicate_user
(
portal_user
,
COURSE_1
)
course_user
=
User
.
objects
.
using
(
COURSE_1
)
.
get
(
id
=
portal_user
.
id
)
self
.
assertEqual
(
portal_user
.
seen_response_count
,
20
)
self
.
assertEqual
(
course_user
.
seen_response_count
,
1
0
)
self
.
assertEqual
(
course_user
.
seen_response_count
,
0
)
# Another replication should work for an email change however, since
# it's a field we care about.
...
...
@@ -123,6 +121,25 @@ class ReplicationTest(TestCase):
UserProfile
.
objects
.
using
(
COURSE_2
)
.
get
,
id
=
portal_user_profile
.
id
)
log
.
debug
(
"Make sure our seen_response_count is not replicated."
)
if
hasattr
(
portal_user
,
'seen_response_count'
):
portal_user
.
seen_response_count
=
200
course_user
=
User
.
objects
.
using
(
COURSE_1
)
.
get
(
id
=
portal_user
.
id
)
self
.
assertEqual
(
portal_user
.
seen_response_count
,
200
)
self
.
assertEqual
(
course_user
.
seen_response_count
,
0
)
portal_user
.
save
()
course_user
=
User
.
objects
.
using
(
COURSE_1
)
.
get
(
id
=
portal_user
.
id
)
self
.
assertEqual
(
portal_user
.
seen_response_count
,
200
)
self
.
assertEqual
(
course_user
.
seen_response_count
,
0
)
portal_user
.
email
=
'jim@edx.org'
portal_user
.
save
()
course_user
=
User
.
objects
.
using
(
COURSE_1
)
.
get
(
id
=
portal_user
.
id
)
self
.
assertEqual
(
portal_user
.
email
,
'jim@edx.org'
)
self
.
assertEqual
(
course_user
.
email
,
'jim@edx.org'
)
def
test_enrollment_for_user_info_after_enrollment
(
self
):
"""Test the effect of modifying User data after you've enrolled."""
...
...
common/djangoapps/student/views.py
View file @
76235d90
import
datetime
import
feedparser
import
itertools
import
json
import
logging
import
random
import
string
import
sys
import
uuid
import
feedparser
import
time
import
urllib
import
itertools
import
uuid
from
django.conf
import
settings
from
django.contrib.auth
import
logout
,
authenticate
,
login
...
...
@@ -26,17 +27,19 @@ from bs4 import BeautifulSoup
from
django.core.cache
import
cache
from
django_future.csrf
import
ensure_csrf_cookie
from
student.models
import
Registration
,
UserProfile
,
PendingNameChange
,
PendingEmailChange
,
CourseEnrollment
from
student.models
import
(
Registration
,
UserProfile
,
PendingNameChange
,
PendingEmailChange
,
CourseEnrollment
)
from
util.cache
import
cache_if_anonymous
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
models
import
Registration
,
UserProfile
,
PendingNameChange
,
PendingEmailChange
,
CourseEnrollment
from
datetime
import
date
from
collections
import
namedtuple
from
courseware.courses
import
course_staff_group_name
,
has_staff_access_to_course
,
get_courses_by_university
from
courseware.courses
import
(
course_staff_group_name
,
has_staff_access_to_course
,
get_courses_by_university
)
log
=
logging
.
getLogger
(
"mitx.student"
)
Article
=
namedtuple
(
'Article'
,
'title url author image deck publication publish_date'
)
...
...
@@ -47,7 +50,8 @@ def csrf_token(context):
csrf_token
=
context
.
get
(
'csrf_token'
,
''
)
if
csrf_token
==
'NOTPROVIDED'
:
return
''
return
u'<div style="display:none"><input type="hidden" name="csrfmiddlewaretoken" value="
%
s" /></div>'
%
(
csrf_token
)
return
(
u'<div style="display:none"><input type="hidden"'
' name="csrfmiddlewaretoken" value="
%
s" /></div>'
%
(
csrf_token
))
@ensure_csrf_cookie
...
...
@@ -162,6 +166,24 @@ def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work."""
return
HttpResponse
(
json
.
dumps
(
change_enrollment
(
request
)))
def
enrollment_allowed
(
user
,
course
):
"""If the course has an enrollment period, check whether we are in it.
Also respects the DARK_LAUNCH setting"""
now
=
time
.
gmtime
()
start
=
course
.
enrollment_start
end
=
course
.
enrollment_end
if
(
start
is
None
or
now
>
start
)
and
(
end
is
None
or
now
<
end
):
# in enrollment period.
return
True
if
settings
.
MITX_FEATURES
[
'DARK_LAUNCH'
]:
if
has_staff_access_to_course
(
user
,
course
):
# if dark launch, staff can enroll outside enrollment window
return
True
return
False
def
change_enrollment
(
request
):
if
request
.
method
!=
"POST"
:
raise
Http404
...
...
@@ -174,7 +196,8 @@ def change_enrollment(request):
course_id
=
request
.
POST
.
get
(
"course_id"
,
None
)
if
course_id
==
None
:
return
HttpResponse
(
json
.
dumps
({
'success'
:
False
,
'error'
:
'There was an error receiving the course id.'
}))
return
HttpResponse
(
json
.
dumps
({
'success'
:
False
,
'error'
:
'There was an error receiving the course id.'
}))
if
action
==
"enroll"
:
# Make sure the course exists
...
...
@@ -187,12 +210,20 @@ def change_enrollment(request):
return
{
'success'
:
False
,
'error'
:
'The course requested does not exist.'
}
if
settings
.
MITX_FEATURES
.
get
(
'ACCESS_REQUIRE_STAFF_FOR_COURSE'
):
# require that user be in the staff_* group (or be an overall admin) to be able to enroll
# eg staff_6.002x or staff_6.00x
# require that user be in the staff_* group (or be an
# overall admin) to be able to enroll eg staff_6.002x or
# staff_6.00x
if
not
has_staff_access_to_course
(
user
,
course
):
staff_group
=
course_staff_group_name
(
course
)
log
.
debug
(
'user
%
s denied enrollment to
%
s ; not in
%
s'
%
(
user
,
course
.
location
.
url
(),
staff_group
))
return
{
'success'
:
False
,
'error'
:
'
%
s membership required to access course.'
%
staff_group
}
log
.
debug
(
'user
%
s denied enrollment to
%
s ; not in
%
s'
%
(
user
,
course
.
location
.
url
(),
staff_group
))
return
{
'success'
:
False
,
'error'
:
'
%
s membership required to access course.'
%
staff_group
}
if
not
enrollment_allowed
(
user
,
course
):
return
{
'success'
:
False
,
'error'
:
'enrollment in {} not allowed at this time'
.
format
(
course
.
display_name
)}
enrollment
,
created
=
CourseEnrollment
.
objects
.
get_or_create
(
user
=
user
,
course_id
=
course
.
id
)
return
{
'success'
:
True
}
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
76235d90
...
...
@@ -21,18 +21,35 @@ class CourseDescriptor(SequenceDescriptor):
try
:
self
.
start
=
time
.
strptime
(
self
.
metadata
[
"start"
],
"
%
Y-
%
m-
%
dT
%
H:
%
M"
)
except
KeyError
:
self
.
start
=
time
.
gmtime
(
0
)
#The epoch
msg
=
"Course loaded without a start date. id =
%
s"
%
self
.
id
log
.
critical
(
msg
)
except
ValueError
as
e
:
self
.
start
=
time
.
gmtime
(
0
)
#The epoch
msg
=
"Course loaded with a bad start date.
%
s '
%
s'"
%
(
self
.
id
,
e
)
log
.
critical
(
msg
)
# Don't call the tracker from the exception handler.
if
msg
is
not
None
:
self
.
start
=
time
.
gmtime
(
0
)
# The epoch
log
.
critical
(
msg
)
system
.
error_tracker
(
msg
)
def
try_parse_time
(
key
):
"""
Parse an optional metadata key: if present, must be valid.
Return None if not present.
"""
if
key
in
self
.
metadata
:
try
:
return
time
.
strptime
(
self
.
metadata
[
key
],
"
%
Y-
%
m-
%
dT
%
H:
%
M"
)
except
ValueError
as
e
:
msg
=
"Course
%
s loaded with a bad metadata key
%
s '
%
s'"
%
(
self
.
id
,
self
.
metadata
[
key
],
e
)
log
.
warning
(
msg
)
return
None
self
.
enrollment_start
=
try_parse_time
(
"enrollment_start"
)
self
.
enrollment_end
=
try_parse_time
(
"enrollment_end"
)
def
has_started
(
self
):
return
time
.
gmtime
()
>
self
.
start
...
...
@@ -100,7 +117,7 @@ class CourseDescriptor(SequenceDescriptor):
for
s
in
c
.
get_children
():
if
s
.
metadata
.
get
(
'graded'
,
False
):
xmoduledescriptors
=
list
(
yield_descriptor_descendents
(
s
))
# The xmoduledescriptors included here are only the ones that have scores.
section_description
=
{
'section_descriptor'
:
s
,
'xmoduledescriptors'
:
filter
(
lambda
child
:
child
.
has_score
,
xmoduledescriptors
)
}
...
...
common/lib/xmodule/xmodule/css/capa/display.scss
View file @
76235d90
...
...
@@ -89,6 +89,19 @@ div {
}
}
&
.processing
{
p
.status
{
@include
inline-block
();
background
:
url('../images/spinner.gif')
center
center
no-repeat
;
height
:
20px
;
width
:
20px
;
}
input
{
border-color
:
#aaa
;
}
}
&
.incorrect
,
&
.ui-icon-close
{
p
.status
{
@include
inline-block
();
...
...
@@ -146,7 +159,7 @@ div {
width
:
14px
;
}
&
.processing
,
&
.ui-icon-
check
{
&
.processing
,
&
.ui-icon-
processing
{
@include
inline-block
();
background
:
url('../images/spinner.gif')
center
center
no-repeat
;
height
:
20px
;
...
...
common/lib/xmodule/xmodule/css/video/display.scss
View file @
76235d90
...
...
@@ -14,7 +14,7 @@ div.video {
section
.video-player
{
height
:
0
;
overflow
:
hidden
;
//
overflow: hidden;
padding-bottom
:
56
.25%
;
position
:
relative
;
...
...
@@ -45,12 +45,13 @@ div.video {
div
.slider
{
@extend
.clearfix
;
background
:
#c2c2c2
;
border
:
none
;
border-bottom
:
1px
solid
#000
;
border
:
1px
solid
#000
;
@include
border-radius
(
0
);
border-top
:
1px
solid
#000
;
@include
box-shadow
(
inset
0
1px
0
#eee
,
0
1px
0
#555
);
height
:
7px
;
margin-left
:
-1px
;
margin-right
:
-1px
;
@include
transition
(
height
2
.0s
ease-in-out
);
div
.ui-widget-header
{
...
...
@@ -58,43 +59,12 @@ div.video {
@include
box-shadow
(
inset
0
1px
0
#999
);
}
.ui-tooltip.qtip
.ui-tooltip-content
{
background
:
$mit-red
;
border
:
1px
solid
darken
(
$mit-red
,
20%
);
@include
border-radius
(
2px
);
@include
box-shadow
(
inset
0
1px
0
lighten
(
$mit-red
,
10%
));
color
:
#fff
;
font
:
bold
12px
$body-font-family
;
margin-bottom
:
6px
;
margin-right
:
0
;
overflow
:
visible
;
padding
:
4px
;
text-align
:
center
;
text-shadow
:
0
-1px
0
darken
(
$mit-red
,
10%
);
-webkit-font-smoothing
:
antialiased
;
&
:
:
after
{
background
:
$mit-red
;
border-bottom
:
1px
solid
darken
(
$mit-red
,
20%
);
border-right
:
1px
solid
darken
(
$mit-red
,
20%
);
bottom
:
-5px
;
content
:
" "
;
display
:
block
;
height
:
7px
;
left
:
50%
;
margin-left
:
-3px
;
position
:
absolute
;
@include
transform
(
rotate
(
45deg
));
width
:
7px
;
}
}
a
.ui-slider-handle
{
background
:
$
mit-red
url(../images/slider-handle.png)
center
center
no-repeat
;
background
:
$
pink
url(../images/slider-handle.png)
center
center
no-repeat
;
@include
background-size
(
50%
);
border
:
1px
solid
darken
(
$
mit-red
,
20%
);
border
:
1px
solid
darken
(
$
pink
,
20%
);
@include
border-radius
(
15px
);
@include
box-shadow
(
inset
0
1px
0
lighten
(
$
mit-red
,
10%
));
@include
box-shadow
(
inset
0
1px
0
lighten
(
$
pink
,
10%
));
cursor
:
pointer
;
height
:
15px
;
margin-left
:
-7px
;
...
...
@@ -103,7 +73,7 @@ div.video {
width
:
15px
;
&
:focus
,
&
:hover
{
background-color
:
lighten
(
$
mit-red
,
10%
);
background-color
:
lighten
(
$
pink
,
10%
);
outline
:
none
;
}
}
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
76235d90
...
...
@@ -227,7 +227,7 @@ class XModule(HTMLSnippet):
def
get_display_items
(
self
):
'''
Returns a list of descendent module instances that will display
immediately inside this module
immediately inside this module
.
'''
items
=
[]
for
child
in
self
.
get_children
():
...
...
@@ -238,7 +238,7 @@ class XModule(HTMLSnippet):
def
displayable_items
(
self
):
'''
Returns list of displayable modules contained by this module. If this
module is visible, should return [self]
module is visible, should return [self]
.
'''
return
[
self
]
...
...
lms/djangoapps/courseware/grades.py
View file @
76235d90
...
...
@@ -145,15 +145,11 @@ def progress_summary(student, course, grader, student_module_cache):
instance_modules for the student
"""
chapters
=
[]
for
c
in
course
.
get_children
():
# Don't include chapters that aren't displayable (e.g. due to error)
if
c
not
in
c
.
displayable_items
():
continue
# Don't include chapters that aren't displayable (e.g. due to error)
for
c
in
course
.
get_display_items
():
sections
=
[]
for
s
in
c
.
get_
children
():
for
s
in
c
.
get_
display_items
():
# Same for sections
if
s
not
in
s
.
displayable_items
():
continue
graded
=
s
.
metadata
.
get
(
'graded'
,
False
)
scores
=
[]
for
module
in
yield_module_descendents
(
s
):
...
...
lms/djangoapps/courseware/tests/tests.py
View file @
76235d90
...
...
@@ -58,8 +58,22 @@ def mongo_store_config(data_dir):
}
}
def
xml_store_config
(
data_dir
):
return
{
'default'
:
{
'ENGINE'
:
'xmodule.modulestore.xml.XMLModuleStore'
,
'OPTIONS'
:
{
'data_dir'
:
data_dir
,
'default_class'
:
'xmodule.hidden_module.HiddenDescriptor'
,
'eager'
:
True
,
}
}
}
TEST_DATA_DIR
=
settings
.
COMMON_TEST_DATA_ROOT
TEST_DATA_MODULESTORE
=
mongo_store_config
(
TEST_DATA_DIR
)
TEST_DATA_MONGO_MODULESTORE
=
mongo_store_config
(
TEST_DATA_DIR
)
TEST_DATA_XML_MODULESTORE
=
xml_store_config
(
TEST_DATA_DIR
)
REAL_DATA_DIR
=
settings
.
GITHUB_REPO_ROOT
REAL_DATA_MODULESTORE
=
mongo_store_config
(
REAL_DATA_DIR
)
...
...
@@ -149,8 +163,27 @@ class ActivateLoginTestCase(TestCase):
class
PageLoader
(
ActivateLoginTestCase
):
''' Base class that adds a function to load all pages in a modulestore '''
def
_enroll
(
self
,
course
):
"""Post to the enrollment view, and return the parsed json response"""
resp
=
self
.
client
.
post
(
'/change_enrollment'
,
{
'enrollment_action'
:
'enroll'
,
'course_id'
:
course
.
id
,
})
return
parse_json
(
resp
)
def
try_enroll
(
self
,
course
):
"""Try to enroll. Return bool success instead of asserting it."""
data
=
self
.
_enroll
(
course
)
print
'Enrollment in {} result: {}'
.
format
(
course
.
location
.
url
(),
data
)
return
data
[
'success'
]
def
enroll
(
self
,
course
):
"""Enroll the currently logged-in user, and check that it worked."""
data
=
self
.
_enroll
(
course
)
self
.
assertTrue
(
data
[
'success'
])
def
unenroll
(
self
,
course
):
"""Unenroll the currently logged-in user, and check that it worked."""
resp
=
self
.
client
.
post
(
'/change_enrollment'
,
{
'enrollment_action'
:
'enroll'
,
'course_id'
:
course
.
id
,
...
...
@@ -159,6 +192,7 @@ class PageLoader(ActivateLoginTestCase):
self
.
assertTrue
(
data
[
'success'
])
def
check_pages_load
(
self
,
course_name
,
data_dir
,
modstore
):
"""Make all locations in course load"""
print
"Checking course {0} in {1}"
.
format
(
course_name
,
data_dir
)
import_from_xml
(
modstore
,
data_dir
,
[
course_name
])
...
...
@@ -191,7 +225,7 @@ class PageLoader(ActivateLoginTestCase):
self
.
assertTrue
(
all_ok
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MODULESTORE
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MO
NGO_MO
DULESTORE
)
class
TestCoursesLoadTestCase
(
PageLoader
):
'''Check that all pages in test courses load properly'''
...
...
@@ -207,7 +241,7 @@ class TestCoursesLoadTestCase(PageLoader):
self
.
check_pages_load
(
'full'
,
TEST_DATA_DIR
,
modulestore
())
@override_settings
(
MODULESTORE
=
TEST_DATA_MODULESTORE
)
@override_settings
(
MODULESTORE
=
TEST_DATA_
XML_
MODULESTORE
)
class
TestViewAuth
(
PageLoader
):
"""Check that view authentication works properly"""
...
...
@@ -215,15 +249,15 @@ class TestViewAuth(PageLoader):
# can't do imports there without manually hacking settings.
def
setUp
(
self
):
print
"sys.path: {}"
.
format
(
sys
.
path
)
xmodule
.
modulestore
.
django
.
_MODULESTORES
=
{}
modulestore
()
.
collection
.
drop
()
import_from_xml
(
modulestore
(),
TEST_DATA_DIR
,
[
'toy'
])
import_from_xml
(
modulestore
(),
TEST_DATA_DIR
,
[
'full'
])
courses
=
modulestore
()
.
get_courses
()
# get the two courses sorted out
courses
.
sort
(
key
=
lambda
c
:
c
.
location
.
course
)
[
self
.
full
,
self
.
toy
]
=
courses
def
find_course
(
name
):
"""Assumes the course is present"""
return
[
c
for
c
in
courses
if
c
.
location
.
course
==
name
][
0
]
self
.
full
=
find_course
(
"full"
)
self
.
toy
=
find_course
(
"toy"
)
# Create two accounts
self
.
student
=
'view@test.com'
...
...
@@ -304,26 +338,35 @@ class TestViewAuth(PageLoader):
self
.
check_for_get_code
(
200
,
url
)
def
test_dark_launch
(
self
):
"""Make sure that when dark launch is on, students can't access course
pages, but instructors can"""
# test.py turns off start dates, enable them and set them correctly.
# Because settings is global, be careful not to mess it up for other tests
# (Can't use override_settings because we're only changing part of the
# MITX_FEATURES dict)
def
run_wrapped
(
self
,
test
):
"""
test.py turns off start dates. Enable them and DARK_LAUNCH.
Because settings is global, be careful not to mess it up for other tests
(Can't use override_settings because we're only changing part of the
MITX_FEATURES dict)
"""
oldDSD
=
settings
.
MITX_FEATURES
[
'DISABLE_START_DATES'
]
oldDL
=
settings
.
MITX_FEATURES
[
'DARK_LAUNCH'
]
try
:
settings
.
MITX_FEATURES
[
'DISABLE_START_DATES'
]
=
False
settings
.
MITX_FEATURES
[
'DARK_LAUNCH'
]
=
True
self
.
_do_test_dark_launch
()
test
()
finally
:
settings
.
MITX_FEATURES
[
'DISABLE_START_DATES'
]
=
oldDSD
settings
.
MITX_FEATURES
[
'DARK_LAUNCH'
]
=
oldDL
def
test_dark_launch
(
self
):
"""Make sure that when dark launch is on, students can't access course
pages, but instructors can"""
self
.
run_wrapped
(
self
.
_do_test_dark_launch
)
def
test_enrollment_period
(
self
):
"""Check that enrollment periods work"""
self
.
run_wrapped
(
self
.
_do_test_enrollment_period
)
def
_do_test_dark_launch
(
self
):
"""Actually do the test, relying on settings to be right."""
...
...
@@ -338,6 +381,7 @@ class TestViewAuth(PageLoader):
self
.
assertTrue
(
settings
.
MITX_FEATURES
[
'DARK_LAUNCH'
])
def
reverse_urls
(
names
,
course
):
"""Reverse a list of course urls"""
return
[
reverse
(
name
,
kwargs
=
{
'course_id'
:
course
.
id
})
for
name
in
names
]
def
dark_student_urls
(
course
):
...
...
@@ -424,6 +468,53 @@ class TestViewAuth(PageLoader):
check_staff
(
self
.
toy
)
check_staff
(
self
.
full
)
def
_do_test_enrollment_period
(
self
):
"""Actually do the test, relying on settings to be right."""
# Make courses start in the future
tomorrow
=
time
.
time
()
+
24
*
3600
nextday
=
tomorrow
+
24
*
3600
yesterday
=
time
.
time
()
-
24
*
3600
print
"changing"
# toy course's enrollment period hasn't started
self
.
toy
.
enrollment_start
=
time
.
gmtime
(
tomorrow
)
self
.
toy
.
enrollment_end
=
time
.
gmtime
(
nextday
)
# full course's has
self
.
full
.
enrollment_start
=
time
.
gmtime
(
yesterday
)
self
.
full
.
enrollment_end
=
time
.
gmtime
(
tomorrow
)
print
"login"
# First, try with an enrolled student
print
'=== Testing student access....'
self
.
login
(
self
.
student
,
self
.
password
)
self
.
assertFalse
(
self
.
try_enroll
(
self
.
toy
))
self
.
assertTrue
(
self
.
try_enroll
(
self
.
full
))
print
'=== Testing course instructor access....'
# Make the instructor staff in the toy course
group_name
=
course_staff_group_name
(
self
.
toy
)
g
=
Group
.
objects
.
create
(
name
=
group_name
)
g
.
user_set
.
add
(
user
(
self
.
instructor
))
print
"logout/login"
self
.
logout
()
self
.
login
(
self
.
instructor
,
self
.
password
)
print
"Instructor should be able to enroll in toy course"
self
.
assertTrue
(
self
.
try_enroll
(
self
.
toy
))
print
'=== Testing staff access....'
# now make the instructor global staff, but not in the instructor group
g
.
user_set
.
remove
(
user
(
self
.
instructor
))
u
=
user
(
self
.
instructor
)
u
.
is_staff
=
True
u
.
save
()
# unenroll and try again
self
.
unenroll
(
self
.
toy
)
self
.
assertTrue
(
self
.
try_enroll
(
self
.
toy
))
@override_settings
(
MODULESTORE
=
REAL_DATA_MODULESTORE
)
class
RealCoursesLoadTestCase
(
PageLoader
):
...
...
lms/static/sass/course.scss
View file @
76235d90
...
...
@@ -7,6 +7,7 @@
@import
'base/base'
;
@import
'base/extends'
;
@import
'base/animations'
;
@import
'shared/tooltips'
;
// Course base / layout styles
@import
'course/layout/courseware_subnav'
;
...
...
lms/static/sass/course/_info.scss
View file @
76235d90
...
...
@@ -15,15 +15,15 @@ div.info-wrapper {
>
ol
{
list-style
:
none
;
padding-left
:
0
;
margin-bottom
:
lh
();
padding-left
:
0
;
>
li
{
@extend
.clearfix
;
border-bottom
:
1px
solid
lighten
(
$border-color
,
10%
);
list-style-type
:
disk
;
margin-bottom
:
lh
();
padding-bottom
:
lh
(
.5
);
list-style-type
:
disk
;
&
:first-child
{
margin
:
0
(
-
(
lh
(
.5
)))
lh
();
...
...
@@ -41,10 +41,10 @@ div.info-wrapper {
h2
{
float
:
left
;
margin
:
0
flex-gutter
()
0
0
;
width
:
flex-grid
(
2
,
9
);
font-size
:
$body-font-size
;
font-weight
:
bold
;
margin
:
0
flex-gutter
()
0
0
;
width
:
flex-grid
(
2
,
9
);
}
section
.update-description
{
...
...
@@ -68,15 +68,15 @@ div.info-wrapper {
section
.handouts
{
@extend
.sidebar
;
border-left
:
1px
solid
#d3d3d3
;
border-left
:
1px
solid
$border-color
;
@include
border-radius
(
0
4px
4px
0
);
@include
box-shadow
(
none
);
border-right
:
0
;
@include
box-shadow
(
none
);
h1
{
@extend
.bottom-border
;
padding
:
lh
(
.5
)
lh
(
.5
);
margin-bottom
:
0
;
padding
:
lh
(
.5
)
lh
(
.5
);
}
ol
{
...
...
@@ -90,8 +90,9 @@ div.info-wrapper {
&
.expandable
,
&
.collapsable
{
h4
{
font-weight
:
normal
;
color
:
$blue
;
font-size
:
1em
;
font-weight
:
normal
;
padding
:
lh
(
.25
)
0
lh
(
.25
)
lh
(
1
.5
);
}
}
...
...
@@ -145,7 +146,8 @@ div.info-wrapper {
filter
:
alpha
(
opacity
=
60
);
+
h4
{
background-color
:
#e3e3e3
;
@extend
a
:hover
;
text-decoration
:
underline
;
}
}
...
...
lms/static/sass/course/base/_base.scss
View file @
76235d90
...
...
@@ -3,6 +3,7 @@ body {
}
body
,
h1
,
h2
,
h3
,
h4
,
h5
,
h6
,
p
,
p
a
:link
,
p
a
:visited
,
a
{
text-align
:
left
;
font-family
:
$sans-serif
;
}
...
...
lms/static/sass/course/base/_extends.scss
View file @
76235d90
...
...
@@ -25,24 +25,12 @@ h1.top-header {
}
}
.action-link
{
a
{
color
:
$mit-red
;
&
:hover
{
color
:
darken
(
$mit-red
,
20%
);
text-decoration
:
none
;
}
}
}
.content
{
@include
box-sizing
(
border-box
);
display
:
table-cell
;
padding
:
lh
();
vertical-align
:
top
;
width
:
flex-grid
(
9
)
+
flex-gutter
();
overflow
:
hidden
;
@media
print
{
@include
box-shadow
(
none
);
...
...
@@ -164,7 +152,6 @@ h1.top-header {
.topbar
{
@extend
.clearfix
;
border-bottom
:
1px
solid
$border-color
;
font-size
:
14px
;
@media
print
{
display
:
none
;
...
...
@@ -193,17 +180,17 @@ h1.top-header {
h2
{
display
:
block
;
width
:
700px
;
float
:
left
;
font-size
:
0
.9em
;
font-weight
:
600
;
line-height
:
40px
;
letter-spacing
:
0
;
text-transform
:
none
;
line-height
:
40px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-shadow
:
0
1px
0
#fff
;
text-transform
:
none
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
;
overflow
:
hidden
;
width
:
700px
;
.provider
{
font
:
inherit
;
...
...
@@ -211,4 +198,4 @@ h1.top-header {
color
:
#6d6d6d
;
}
}
}
\ No newline at end of file
}
lms/static/sass/course/courseware/_courseware.scss
View file @
76235d90
...
...
@@ -146,13 +146,13 @@ div.course-wrapper {
@include
border-radius
(
0
);
a
.ui-slider-handle
{
@include
box-shadow
(
inset
0
1px
0
lighten
(
$
mit-red
,
10%
));
@include
box-shadow
(
inset
0
1px
0
lighten
(
$
pink
,
10%
));
background
:
$mit-red
url(../images/slider-bars.png)
center
center
no-repeat
;
border
:
1px
solid
darken
(
$
mit-red
,
20%
);
border
:
1px
solid
darken
(
$
pink
,
20%
);
cursor
:
pointer
;
&
:hover
,
&
:focus
{
background-color
:
lighten
(
$
mit-red
,
10%
);
background-color
:
lighten
(
$
pink
,
10%
);
outline
:
none
;
}
}
...
...
lms/static/sass/course/courseware/_sidebar.scss
View file @
76235d90
...
...
@@ -13,7 +13,7 @@ section.course-index {
div
#accordion
{
h3
{
@include
border-radius
(
0
);
border-top
:
1px
solid
#e3e3e3
;
border-top
:
1px
solid
lighten
(
$border-color
,
10%
)
;
font-size
:
em
(
16
,
18
);
margin
:
0
;
overflow
:
hidden
;
...
...
@@ -34,6 +34,7 @@ section.course-index {
}
&
.ui-accordion-header
{
border-bottom
:
none
;
color
:
#000
;
a
{
...
...
lms/static/sass/course/discussion/_answers.scss
View file @
76235d90
...
...
@@ -17,7 +17,6 @@ div.answer-controls {
margin-left
:
flex-gutter
();
nav
{
@extend
.action-link
;
float
:
right
;
margin-top
:
34px
;
...
...
@@ -144,7 +143,7 @@ div.answer-actions {
text-decoration
:
none
;
&
.question-delete
{
//
color: $mit-red;
color
:
$mit-red
;
}
}
}
...
...
lms/static/sass/course/discussion/_forms.scss
View file @
76235d90
...
...
@@ -92,7 +92,7 @@ form.answer-form {
margin-left
:
2
.5%
;
padding-left
:
1
.5%
;
border-left
:
1px
dashed
#ddd
;
color
:
$mit-red
;
;
color
:
$mit-red
;
}
ul
,
ol
,
pre
{
...
...
lms/static/sass/course/wiki/_sidebar.scss
View file @
76235d90
...
...
@@ -14,14 +14,6 @@ div#wiki_panel {
}
}
form
{
input
[
type
=
"submit"
]
{
@extend
.light-button
;
text-transform
:
none
;
text-shadow
:
none
;
}
}
div
#wiki_create_form
{
@extend
.clearfix
;
padding
:
lh
(
.5
)
lh
()
lh
(
.5
)
0
;
...
...
@@ -53,4 +45,12 @@ div#wiki_panel {
}
}
}
input
#wiki_search_input_submit
{
padding
:
4px
8px
;
}
input
#wiki_search_input
{
margin-right
:
10px
;
}
}
lms/static/sass/multicourse/_dashboard.scss
View file @
76235d90
...
...
@@ -203,28 +203,23 @@
display
:
block
;
left
:
0px
;
position
:
absolute
;
z-index
:
50
;
top
:
0px
;
@include
transition
(
all
,
0
.15s
,
linear
);
right
:
0px
;
}
.arrow
{
border-top
:
8px
solid
;
border-left
:
8px
solid
;
border-color
:
rgba
(
0
,
0
,
0
,
0
.7
);
@include
box-shadow
(
inset
0
1px
0
0
rgba
(
255
,
255
,
255
,
0
.8
)
,
-1px
0
1px
0
rgba
(
255
,
255
,
255
,
0
.8
));
content
:
""
;
display
:
block
;
height
:
55px
;
left
:
50%
;
margin-left
:
-10px
;
margin-top
:
-30px
;
opacity
:
0
;
position
:
absolute
;
top
:
50%
;
@include
transform
(
rotate
(
-45deg
));
z-index
:
100
;
width
:
100%
;
font-size
:
70px
;
line-height
:
110px
;
text-align
:
center
;
text-decoration
:
none
;
color
:
rgba
(
0
,
0
,
0
,
.7
);
opacity
:
0
;
@include
transition
(
all
,
0
.15s
,
linear
);
width
:
55px
;
}
&
:hover
{
...
...
lms/static/sass/shared/_forms.scss
View file @
76235d90
form
{
font-size
:
1em
;
}
label
{
color
:
$base-font-color
;
font
:
italic
300
1rem
/
1
.6rem
$serif
;
margin-bottom
:
5px
;
text-shadow
:
0
1px
rgba
(
255
,
255
,
255
,
0
.4
);
-webkit-font-smoothing
:
antialiased
;
}
textarea
,
input
[
type
=
"text"
],
input
[
type
=
"email"
],
input
[
type
=
"password"
]
{
background
:
rgb
(
250
,
250
,
250
);
border
:
1px
solid
rgb
(
200
,
200
,
200
);
@include
border-radius
(
3px
);
@include
box-shadow
(
0
1px
0
0
rgba
(
255
,
255
,
255
,
0
.6
)
,
inset
0
0
3px
0
rgba
(
0
,
0
,
0
,
0
.1
));
@include
box-sizing
(
border-box
);
font
:
italic
300
1rem
/
1
.6rem
$serif
;
height
:
35px
;
padding
:
5px
12px
;
vertical-align
:
top
;
-webkit-font-smoothing
:
antialiased
;
label
{
color
:
$base-font-color
;
font
:
italic
300
1rem
/
1
.6rem
$serif
;
margin-bottom
:
5px
;
text-shadow
:
0
1px
rgba
(
255
,
255
,
255
,
0
.4
);
-webkit-font-smoothing
:
antialiased
;
}
&
:last-child
{
margin-right
:
0px
;
}
textarea
,
input
[
type
=
"text"
],
input
[
type
=
"email"
],
input
[
type
=
"password"
]
{
background
:
rgb
(
250
,
250
,
250
);
border
:
1px
solid
rgb
(
200
,
200
,
200
);
@include
border-radius
(
3px
);
@include
box-shadow
(
0
1px
0
0
rgba
(
255
,
255
,
255
,
0
.6
)
,
inset
0
0
3px
0
rgba
(
0
,
0
,
0
,
0
.1
));
@include
box-sizing
(
border-box
);
font
:
italic
300
1rem
/
1
.6rem
$serif
;
height
:
35px
;
padding
:
5px
12px
;
vertical-align
:
top
;
-webkit-font-smoothing
:
antialiased
;
&
:focus
{
border-color
:
lighten
(
$blue
,
20%
);
@include
box-shadow
(
0
0
6px
0
rgba
(
$blue
,
0
.4
)
,
inset
0
0
4px
0
rgba
(
0
,
0
,
0
,
0
.15
));
outline
:
none
;
}
&
:last-child
{
margin-right
:
0px
;
}
textarea
{
height
:
60px
;
&
:focus
{
border-color
:
lighten
(
$blue
,
20%
);
@include
box-shadow
(
0
0
6px
0
rgba
(
$blue
,
0
.4
)
,
inset
0
0
4px
0
rgba
(
0
,
0
,
0
,
0
.15
));
outline
:
none
;
}
}
input
[
type
=
"submit"
]
{
@include
button
(
shiny
,
$blue
);
@include
border-radius
(
3px
);
font
:
normal
1
.2rem
/
1
.6rem
$sans-serif
;
height
:
35px
;
letter-spacing
:
1px
;
text-transform
:
uppercase
;
vertical-align
:
top
;
-webkit-font-smoothing
:
antialiased
;
}
textarea
{
height
:
60px
;
}
input
[
type
=
"submit"
],
input
[
type
=
"button"
],
.button
{
@include
border-radius
(
3px
);
@include
button
(
shiny
,
$blue
);
font
:
normal
1
.2rem
/
1
.6rem
$sans-serif
;
letter-spacing
:
1px
;
padding
:
4px
20px
;
text-transform
:
uppercase
;
vertical-align
:
top
;
-webkit-font-smoothing
:
antialiased
;
}
lms/static/sass/shared/_tooltips.scss
0 → 100644
View file @
76235d90
.ui-tooltip.qtip
.ui-tooltip-content
{
background
:
rgba
(
$pink
,
.8
);
border
:
0
;
color
:
#fff
;
font
:
bold
12px
$body-font-family
;
margin-bottom
:
6px
;
margin-right
:
0
;
overflow
:
visible
;
padding
:
4px
;
text-align
:
center
;
-webkit-font-smoothing
:
antialiased
;
}
lms/templates/dashboard.html
View file @
76235d90
...
...
@@ -73,7 +73,7 @@
%
>
<a
href=
"${course_target}"
class=
"cover"
style=
"background-image: url('${course_image_url(course)}')"
>
<div
class=
"shade"
></div>
<div
class=
"arrow"
></div>
<div
class=
"arrow"
>
❯
</div>
</a>
<section
class=
"info"
>
<hgroup>
...
...
lms/templates/simplewiki/simplewiki_base.html
View file @
76235d90
...
...
@@ -110,7 +110,7 @@
</div>
<ul>
<li>
<input
type=
"submit"
class=
"button"
value=
"Create"
style=
"display: inline-block; margin-right: 2px; font-weight: bold;"
/>
<input
type=
"submit"
class=
"button"
value=
"Create"
/>
</li>
</ul>
</form>
...
...
@@ -120,8 +120,8 @@
<li
class=
"search"
>
<form
method=
"GET"
action=
'${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'
>
<label
class=
"wiki_box_title"
>
Search
</label>
<input
type=
"text"
placeholder=
"Search"
name=
"value"
id=
"wiki_search_input"
style=
"width: 71%"
value=
"${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"
/>
<input
type=
"submit"
id=
"wiki_search_input_submit"
value=
"Go!"
style=
"width: 20%"
/>
<input
type=
"text"
placeholder=
"Search"
name=
"value"
id=
"wiki_search_input"
value=
"${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"
/>
<input
type=
"submit"
id=
"wiki_search_input_submit"
value=
"Go!"
/>
</form>
</li>
</ul>
...
...
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