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
24ee5a68
Commit
24ee5a68
authored
Nov 04, 2015
by
Eric Fischer
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #10490 from edx/release
Release 4 November 2015 to master
parents
e46b4396
ae7b44bb
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
255 additions
and
170 deletions
+255
-170
common/djangoapps/student/models.py
+2
-2
common/djangoapps/student/tests/test_refunds.py
+3
-3
common/djangoapps/student/tests/test_views.py
+19
-1
common/djangoapps/student/tests/tests.py
+41
-8
common/djangoapps/student/views.py
+8
-5
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
+2
-1
lms/djangoapps/certificates/admin.py
+10
-0
lms/djangoapps/courseware/module_render.py
+1
-1
lms/templates/dashboard.html
+2
-1
lms/templates/dashboard/_dashboard_course_listing.html
+2
-2
openedx/core/djangoapps/course_groups/management/commands/post_cohort_membership_fix.py
+81
-0
openedx/core/djangoapps/course_groups/management/commands/remove_users_from_multiple_cohorts.py
+0
-56
openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py
+84
-0
openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py
+0
-82
openedx/core/djangoapps/course_groups/migrations/rerun_0006.sh
+0
-8
No files found.
common/djangoapps/student/models.py
View file @
24ee5a68
...
@@ -1377,7 +1377,7 @@ class CourseEnrollment(models.Model):
...
@@ -1377,7 +1377,7 @@ class CourseEnrollment(models.Model):
# If it is after the refundable cutoff date they should not be refunded.
# If it is after the refundable cutoff date they should not be refunded.
refund_cutoff_date
=
self
.
refund_cutoff_date
()
refund_cutoff_date
=
self
.
refund_cutoff_date
()
if
refund_cutoff_date
and
datetime
.
now
()
>
refund_cutoff_date
:
if
refund_cutoff_date
and
datetime
.
now
(
UTC
)
>
refund_cutoff_date
:
return
False
return
False
course_mode
=
CourseMode
.
mode_for_course
(
self
.
course_id
,
'verified'
)
course_mode
=
CourseMode
.
mode_for_course
(
self
.
course_id
,
'verified'
)
...
@@ -1400,7 +1400,7 @@ class CourseEnrollment(models.Model):
...
@@ -1400,7 +1400,7 @@ class CourseEnrollment(models.Model):
self
.
course_overview
.
start
.
replace
(
tzinfo
=
None
)
self
.
course_overview
.
start
.
replace
(
tzinfo
=
None
)
)
)
return
refund_window_start_date
+
EnrollmentRefundConfiguration
.
current
()
.
refund_window
return
refund_window_start_date
.
replace
(
tzinfo
=
UTC
)
+
EnrollmentRefundConfiguration
.
current
()
.
refund_window
@property
@property
def
username
(
self
):
def
username
(
self
):
...
...
common/djangoapps/student/tests/test_refunds.py
View file @
24ee5a68
...
@@ -113,10 +113,10 @@ class RefundableTest(SharedModuleStoreTestCase):
...
@@ -113,10 +113,10 @@ class RefundableTest(SharedModuleStoreTestCase):
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
with
patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
as
cutoff_date
:
with
patch
(
'student.models.CourseEnrollment.refund_cutoff_date'
)
as
cutoff_date
:
cutoff_date
.
return_value
=
datetime
.
now
(
)
-
timedelta
(
days
=
1
)
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
minutes
=
5
)
self
.
assertFalse
(
self
.
enrollment
.
refundable
())
self
.
assertFalse
(
self
.
enrollment
.
refundable
())
cutoff_date
.
return_value
=
datetime
.
now
(
)
+
timedelta
(
days
=
1
)
cutoff_date
.
return_value
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
5
)
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
self
.
assertTrue
(
self
.
enrollment
.
refundable
())
@ddt.data
(
@ddt.data
(
...
@@ -132,7 +132,7 @@ class RefundableTest(SharedModuleStoreTestCase):
...
@@ -132,7 +132,7 @@ class RefundableTest(SharedModuleStoreTestCase):
"""
"""
Assert that the later date is used with the configurable refund period in calculating the returned cutoff date.
Assert that the later date is used with the configurable refund period in calculating the returned cutoff date.
"""
"""
now
=
datetime
.
now
()
.
replace
(
microsecond
=
0
)
now
=
datetime
.
now
(
pytz
.
UTC
)
.
replace
(
microsecond
=
0
)
order_date
=
now
+
order_date_delta
order_date
=
now
+
order_date_delta
course_start
=
now
+
course_start_delta
course_start
=
now
+
course_start_delta
expected_date
=
now
+
expected_date_delta
expected_date
=
now
+
expected_date_delta
...
...
common/djangoapps/student/tests/test_views.py
View file @
24ee5a68
...
@@ -11,6 +11,7 @@ from django.conf import settings
...
@@ -11,6 +11,7 @@ from django.conf import settings
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
student.helpers
import
DISABLE_UNENROLL_CERT_STATES
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
...
@@ -38,7 +39,10 @@ class TestStudentDashboardUnenrollments(ModuleStoreTestCase):
...
@@ -38,7 +39,10 @@ class TestStudentDashboardUnenrollments(ModuleStoreTestCase):
def
mock_cert
(
self
,
_user
,
_course_overview
,
_course_mode
):
# pylint: disable=unused-argument
def
mock_cert
(
self
,
_user
,
_course_overview
,
_course_mode
):
# pylint: disable=unused-argument
""" Return a preset certificate status. """
""" Return a preset certificate status. """
if
self
.
cert_status
is
not
None
:
if
self
.
cert_status
is
not
None
:
return
{
'status'
:
self
.
cert_status
}
return
{
'status'
:
self
.
cert_status
,
'can_unenroll'
:
self
.
cert_status
not
in
DISABLE_UNENROLL_CERT_STATES
}
else
:
else
:
return
{}
return
{}
...
@@ -85,3 +89,17 @@ class TestStudentDashboardUnenrollments(ModuleStoreTestCase):
...
@@ -85,3 +89,17 @@ class TestStudentDashboardUnenrollments(ModuleStoreTestCase):
course_enrollment
.
assert_called_with
(
self
.
user
,
self
.
course
.
id
)
course_enrollment
.
assert_called_with
(
self
.
user
,
self
.
course
.
id
)
else
:
else
:
course_enrollment
.
assert_not_called
()
course_enrollment
.
assert_not_called
()
def
test_no_cert_status
(
self
):
""" Assert that the dashboard loads when cert_status is None."""
with
patch
(
'student.views.cert_info'
,
return_value
=
None
):
response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_cant_unenroll_status
(
self
):
""" Assert that the dashboard loads when cert_status does not allow for unenrollment"""
with
patch
(
'certificates.models.certificate_status_for_student'
,
return_value
=
{
'status'
:
'ready'
}):
response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
common/djangoapps/student/tests/tests.py
View file @
24ee5a68
...
@@ -79,6 +79,7 @@ class CourseEndingTest(TestCase):
...
@@ -79,6 +79,7 @@ class CourseEndingTest(TestCase):
'show_disabled_download_button'
:
False
,
'show_disabled_download_button'
:
False
,
'show_download_url'
:
False
,
'show_download_url'
:
False
,
'show_survey_button'
:
False
,
'show_survey_button'
:
False
,
'can_unenroll'
:
True
,
}
}
)
)
...
@@ -91,7 +92,8 @@ class CourseEndingTest(TestCase):
...
@@ -91,7 +92,8 @@ class CourseEndingTest(TestCase):
'show_download_url'
:
False
,
'show_download_url'
:
False
,
'show_survey_button'
:
False
,
'show_survey_button'
:
False
,
'mode'
:
None
,
'mode'
:
None
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
True
,
}
}
)
)
...
@@ -106,7 +108,8 @@ class CourseEndingTest(TestCase):
...
@@ -106,7 +108,8 @@ class CourseEndingTest(TestCase):
'survey_url'
:
survey_url
,
'survey_url'
:
survey_url
,
'grade'
:
'67'
,
'grade'
:
'67'
,
'mode'
:
'honor'
,
'mode'
:
'honor'
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
False
,
}
}
)
)
...
@@ -121,7 +124,8 @@ class CourseEndingTest(TestCase):
...
@@ -121,7 +124,8 @@ class CourseEndingTest(TestCase):
'survey_url'
:
survey_url
,
'survey_url'
:
survey_url
,
'grade'
:
'67'
,
'grade'
:
'67'
,
'mode'
:
'verified'
,
'mode'
:
'verified'
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
False
,
}
}
)
)
...
@@ -143,7 +147,8 @@ class CourseEndingTest(TestCase):
...
@@ -143,7 +147,8 @@ class CourseEndingTest(TestCase):
'survey_url'
:
survey_url
,
'survey_url'
:
survey_url
,
'grade'
:
'67'
,
'grade'
:
'67'
,
'mode'
:
'honor'
,
'mode'
:
'honor'
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
False
,
}
}
)
)
...
@@ -162,7 +167,8 @@ class CourseEndingTest(TestCase):
...
@@ -162,7 +167,8 @@ class CourseEndingTest(TestCase):
'survey_url'
:
survey_url
,
'survey_url'
:
survey_url
,
'grade'
:
'67'
,
'grade'
:
'67'
,
'mode'
:
'honor'
,
'mode'
:
'honor'
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
True
,
}
}
)
)
...
@@ -181,21 +187,22 @@ class CourseEndingTest(TestCase):
...
@@ -181,21 +187,22 @@ class CourseEndingTest(TestCase):
'show_survey_button'
:
False
,
'show_survey_button'
:
False
,
'grade'
:
'67'
,
'grade'
:
'67'
,
'mode'
:
'honor'
,
'mode'
:
'honor'
,
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
True
,
}
}
)
)
# test when the display is unavailable or notpassing, we get the correct results out
# test when the display is unavailable or notpassing, we get the correct results out
course2
.
certificates_display_behavior
=
'early_no_info'
course2
.
certificates_display_behavior
=
'early_no_info'
cert_status
=
{
'status'
:
'unavailable'
}
cert_status
=
{
'status'
:
'unavailable'
}
self
.
assert
IsNone
(
_cert_info
(
user
,
course2
,
cert_status
,
course_mode
)
)
self
.
assert
Equal
(
_cert_info
(
user
,
course2
,
cert_status
,
course_mode
),
{}
)
cert_status
=
{
cert_status
=
{
'status'
:
'notpassing'
,
'grade'
:
'67'
,
'status'
:
'notpassing'
,
'grade'
:
'67'
,
'download_url'
:
download_url
,
'download_url'
:
download_url
,
'mode'
:
'honor'
'mode'
:
'honor'
}
}
self
.
assert
IsNone
(
_cert_info
(
user
,
course2
,
cert_status
,
course_mode
)
)
self
.
assert
Equal
(
_cert_info
(
user
,
course2
,
cert_status
,
course_mode
),
{}
)
@ddt.ddt
@ddt.ddt
...
@@ -1033,6 +1040,32 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
...
@@ -1033,6 +1040,32 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
else
:
else
:
self
.
assertIn
(
'xseries-border-btn'
,
response
.
content
)
self
.
assertIn
(
'xseries-border-btn'
,
response
.
content
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'DISABLE_START_DATES'
:
False
})
@ddt.data
((
-
2
,
-
1
),
(
-
1
,
1
),
(
1
,
2
))
@ddt.unpack
def
test_start_end_offsets
(
self
,
start_days_offset
,
end_days_offset
):
"""Test that the xseries upsell messaging displays whether the course
has not yet started, is in session, or has already ended.
"""
self
.
course_1
.
start
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
start_days_offset
)
self
.
course_1
.
end
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
end_days_offset
)
self
.
update_course
(
self
.
course_1
,
self
.
user
.
id
)
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_1
.
id
,
mode
=
'verified'
)
self
.
client
.
login
(
username
=
"jack"
,
password
=
"test"
)
self
.
create_config
(
enabled
=
True
,
enable_student_dashboard
=
True
)
with
patch
(
'student.views.get_course_programs_for_dashboard'
,
return_value
=
self
.
_create_program_data
([(
self
.
course_1
.
id
,
'active'
)])
)
as
mock_get_programs
:
response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
# ensure that our course id was included in the API call regardless of start/end dates
__
,
course_ids
=
mock_get_programs
.
call_args
[
0
]
self
.
assertEqual
(
list
(
course_ids
),
[
self
.
course_1
.
id
])
# count total courses appearing on student dashboard
self
.
_assert_responses
(
response
,
1
)
@ddt.data
(
@ddt.data
(
(
'unpublished'
,
'unpublished'
,
'unpublished'
,
0
),
(
'unpublished'
,
'unpublished'
,
'unpublished'
,
0
),
(
'active'
,
'unpublished'
,
'unpublished'
,
1
),
(
'active'
,
'unpublished'
,
'unpublished'
,
1
),
...
...
common/djangoapps/student/views.py
View file @
24ee5a68
...
@@ -202,6 +202,7 @@ def cert_info(user, course_overview, course_mode):
...
@@ -202,6 +202,7 @@ def cert_info(user, course_overview, course_mode):
'show_survey_button': bool
'show_survey_button': bool
'survey_url': url, only if show_survey_button is True
'survey_url': url, only if show_survey_button is True
'grade': if status is not 'processing'
'grade': if status is not 'processing'
'can_unenroll': if status allows for unenrollment
"""
"""
if
not
course_overview
.
may_certify
():
if
not
course_overview
.
may_certify
():
return
{}
return
{}
...
@@ -302,6 +303,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
...
@@ -302,6 +303,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
'show_disabled_download_button'
:
False
,
'show_disabled_download_button'
:
False
,
'show_download_url'
:
False
,
'show_download_url'
:
False
,
'show_survey_button'
:
False
,
'show_survey_button'
:
False
,
'can_unenroll'
:
True
}
}
if
cert_status
is
None
:
if
cert_status
is
None
:
...
@@ -310,7 +312,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
...
@@ -310,7 +312,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
is_hidden_status
=
cert_status
[
'status'
]
in
(
'unavailable'
,
'processing'
,
'generating'
,
'notpassing'
)
is_hidden_status
=
cert_status
[
'status'
]
in
(
'unavailable'
,
'processing'
,
'generating'
,
'notpassing'
)
if
course_overview
.
certificates_display_behavior
==
'early_no_info'
and
is_hidden_status
:
if
course_overview
.
certificates_display_behavior
==
'early_no_info'
and
is_hidden_status
:
return
None
return
{}
status
=
template_state
.
get
(
cert_status
[
'status'
],
default_status
)
status
=
template_state
.
get
(
cert_status
[
'status'
],
default_status
)
...
@@ -319,7 +321,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
...
@@ -319,7 +321,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
'show_download_url'
:
status
==
'ready'
,
'show_download_url'
:
status
==
'ready'
,
'show_disabled_download_button'
:
status
==
'generating'
,
'show_disabled_download_button'
:
status
==
'generating'
,
'mode'
:
cert_status
.
get
(
'mode'
,
None
),
'mode'
:
cert_status
.
get
(
'mode'
,
None
),
'linked_in_url'
:
None
'linked_in_url'
:
None
,
'can_unenroll'
:
status
not
in
DISABLE_UNENROLL_CERT_STATES
,
}
}
if
(
status
in
(
'generating'
,
'ready'
,
'notpassing'
,
'restricted'
)
and
if
(
status
in
(
'generating'
,
'ready'
,
'notpassing'
,
'restricted'
)
and
...
@@ -581,7 +584,7 @@ def dashboard(request):
...
@@ -581,7 +584,7 @@ def dashboard(request):
# program-related information on the dashboard view.
# program-related information on the dashboard view.
course_programs
=
{}
course_programs
=
{}
if
is_student_dashboard_programs_enabled
():
if
is_student_dashboard_programs_enabled
():
course_programs
=
_get_course_programs
(
user
,
show_courseware_links_for
)
course_programs
=
_get_course_programs
(
user
,
[
enrollment
.
course_id
for
enrollment
in
course_enrollments
]
)
# Construct a dictionary of course mode information
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
# used to render the course list. We re-use the course modes dict
...
@@ -1030,8 +1033,8 @@ def change_enrollment(request, check_access=True):
...
@@ -1030,8 +1033,8 @@ def change_enrollment(request, check_access=True):
if
not
enrollment
:
if
not
enrollment
:
return
HttpResponseBadRequest
(
_
(
"You are not enrolled in this course"
))
return
HttpResponseBadRequest
(
_
(
"You are not enrolled in this course"
))
certi
cif
ate_info
=
cert_info
(
user
,
enrollment
.
course_overview
,
enrollment
.
mode
)
certi
fic
ate_info
=
cert_info
(
user
,
enrollment
.
course_overview
,
enrollment
.
mode
)
if
certi
cif
ate_info
.
get
(
'status'
)
in
DISABLE_UNENROLL_CERT_STATES
:
if
certi
fic
ate_info
.
get
(
'status'
)
in
DISABLE_UNENROLL_CERT_STATES
:
return
HttpResponseBadRequest
(
_
(
"Your certificate prevents you from unenrolling from this course"
))
return
HttpResponseBadRequest
(
_
(
"Your certificate prevents you from unenrolling from this course"
))
CourseEnrollment
.
unenroll
(
user
,
course_id
)
CourseEnrollment
.
unenroll
(
user
,
course_id
)
...
...
common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
View file @
24ee5a68
...
@@ -245,7 +245,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
...
@@ -245,7 +245,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Stop the timed exam.
# Stop the timed exam.
self
.
courseware_page
.
stop_timed_exam
()
self
.
courseware_page
.
stop_timed_exam
()
@flaky
# TODO fix this SOL-118
3
@flaky
# TODO fix this SOL-118
2
def
test_can_add_remove_allowance
(
self
):
def
test_can_add_remove_allowance
(
self
):
"""
"""
Make sure that allowances can be added and removed.
Make sure that allowances can be added and removed.
...
@@ -263,6 +263,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
...
@@ -263,6 +263,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Then I can add Allowance to that exam for a student
# Then I can add Allowance to that exam for a student
self
.
assertTrue
(
allowance_section
.
is_add_allowance_button_visible
)
self
.
assertTrue
(
allowance_section
.
is_add_allowance_button_visible
)
@flaky
# TODO fix this SOL-1182
def
test_can_reset_attempts
(
self
):
def
test_can_reset_attempts
(
self
):
"""
"""
Make sure that Exam attempts are visible and can be reset.
Make sure that Exam attempts are visible and can be reset.
...
...
lms/djangoapps/certificates/admin.py
View file @
24ee5a68
...
@@ -11,6 +11,7 @@ from certificates.models import (
...
@@ -11,6 +11,7 @@ from certificates.models import (
BadgeImageConfiguration
,
BadgeImageConfiguration
,
CertificateTemplate
,
CertificateTemplate
,
CertificateTemplateAsset
,
CertificateTemplateAsset
,
GeneratedCertificate
,
)
)
...
@@ -46,8 +47,17 @@ class CertificateTemplateAssetAdmin(admin.ModelAdmin):
...
@@ -46,8 +47,17 @@ class CertificateTemplateAssetAdmin(admin.ModelAdmin):
list_display
=
(
'description'
,
'__unicode__'
)
list_display
=
(
'description'
,
'__unicode__'
)
class
GeneratedCertificateAdmin
(
admin
.
ModelAdmin
):
"""
Django admin customizations for GeneratedCertificate model
"""
search_fields
=
(
'course_id'
,
'user__username'
)
list_display
=
(
'id'
,
'course_id'
,
'mode'
,
'user'
)
admin
.
site
.
register
(
CertificateGenerationConfiguration
)
admin
.
site
.
register
(
CertificateGenerationConfiguration
)
admin
.
site
.
register
(
CertificateHtmlViewConfiguration
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
CertificateHtmlViewConfiguration
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
BadgeImageConfiguration
)
admin
.
site
.
register
(
BadgeImageConfiguration
)
admin
.
site
.
register
(
CertificateTemplate
,
CertificateTemplateAdmin
)
admin
.
site
.
register
(
CertificateTemplate
,
CertificateTemplateAdmin
)
admin
.
site
.
register
(
CertificateTemplateAsset
,
CertificateTemplateAssetAdmin
)
admin
.
site
.
register
(
CertificateTemplateAsset
,
CertificateTemplateAssetAdmin
)
admin
.
site
.
register
(
GeneratedCertificate
,
GeneratedCertificateAdmin
)
lms/djangoapps/courseware/module_render.py
View file @
24ee5a68
...
@@ -1037,7 +1037,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
...
@@ -1037,7 +1037,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
# New Relic. The suffix is necessary for XModule handlers because the
# New Relic. The suffix is necessary for XModule handlers because the
# "handler" in those cases is always just "xmodule_handler".
# "handler" in those cases is always just "xmodule_handler".
nr_tx_name
=
"{}.{}"
.
format
(
instance
.
__class__
.
__name__
,
handler
)
nr_tx_name
=
"{}.{}"
.
format
(
instance
.
__class__
.
__name__
,
handler
)
nr_tx_name
+=
"/{}"
.
format
(
suffix
)
if
suffix
else
""
nr_tx_name
+=
"/{}"
.
format
(
suffix
)
if
(
suffix
and
handler
==
"xmodule_handler"
)
else
""
newrelic
.
agent
.
set_transaction_name
(
nr_tx_name
,
group
=
"Python/XBlock/Handler"
)
newrelic
.
agent
.
set_transaction_name
(
nr_tx_name
,
group
=
"Python/XBlock/Handler"
)
tracking_context_name
=
'module_callback_handler'
tracking_context_name
=
'module_callback_handler'
...
...
lms/templates/dashboard.html
View file @
24ee5a68
...
@@ -87,6 +87,7 @@ import json
...
@@ -87,6 +87,7 @@ import json
% for dashboard_index, enrollment in enumerate(course_enrollments):
% for dashboard_index, enrollment in enumerate(course_enrollments):
<
%
show_courseware_link =
(enrollment.course_id
in
show_courseware_links_for
)
%
>
<
%
show_courseware_link =
(enrollment.course_id
in
show_courseware_links_for
)
%
>
<
%
cert_status =
cert_statuses.get(enrollment.course_id)
%
>
<
%
cert_status =
cert_statuses.get(enrollment.course_id)
%
>
<
%
can_unenroll =
(not
cert_status
)
or
cert_status
.
get
('
can_unenroll
')
%
>
<
%
credit_status =
credit_statuses.get(enrollment.course_id)
%
>
<
%
credit_status =
credit_statuses.get(enrollment.course_id)
%
>
<
%
show_email_settings =
(enrollment.course_id
in
show_email_settings_for
)
%
>
<
%
show_email_settings =
(enrollment.course_id
in
show_email_settings_for
)
%
>
<
%
course_mode_info =
all_course_modes.get(enrollment.course_id)
%
>
<
%
course_mode_info =
all_course_modes.get(enrollment.course_id)
%
>
...
@@ -96,7 +97,7 @@ import json
...
@@ -96,7 +97,7 @@ import json
<
%
course_verification_status =
verification_status_by_course.get(enrollment.course_id,
{})
%
>
<
%
course_verification_status =
verification_status_by_course.get(enrollment.course_id,
{})
%
>
<
%
course_requirements =
courses_requirements_not_met.get(enrollment.course_id)
%
>
<
%
course_requirements =
courses_requirements_not_met.get(enrollment.course_id)
%
>
<
%
course_program_info =
course_programs.get(unicode(enrollment.course_id))
%
>
<
%
course_program_info =
course_programs.get(unicode(enrollment.course_id))
%
>
<
%
include
file =
'dashboard/_dashboard_course_listing.html'
args=
"course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info"
/>
<
%
include
file =
'dashboard/_dashboard_course_listing.html'
args=
"course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, c
an_unenroll=can_unenroll, c
redit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info"
/>
% endfor
% endfor
</ul>
</ul>
...
...
lms/templates/dashboard/_dashboard_course_listing.html
View file @
24ee5a68
<
%
page
args=
"course_overview, enrollment, show_courseware_link, cert_status, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, course_program_info"
/>
<
%
page
args=
"course_overview, enrollment, show_courseware_link, cert_status, c
an_unenroll, c
redit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, course_program_info"
/>
<
%!
<
%!
import
urllib
import
urllib
...
@@ -178,7 +178,7 @@ from student.helpers import (
...
@@ -178,7 +178,7 @@ from student.helpers import (
</a>
</a>
<div
class=
"actions-dropdown"
id=
"actions-dropdown-${dashboard_index}"
aria-label=
"${_('Additional Actions Menu')}"
>
<div
class=
"actions-dropdown"
id=
"actions-dropdown-${dashboard_index}"
aria-label=
"${_('Additional Actions Menu')}"
>
<ul
class=
"actions-dropdown-list"
id=
"actions-dropdown-list-${dashboard_index}"
aria-label=
"${_('Available Actions')}"
role=
"menu"
>
<ul
class=
"actions-dropdown-list"
id=
"actions-dropdown-list-${dashboard_index}"
aria-label=
"${_('Available Actions')}"
role=
"menu"
>
% if c
ert_status.get('status') not in DISABLE_UNENROLL_CERT_STATES
:
% if c
an_unenroll
:
<li
class=
"actions-item"
id=
"actions-item-unenroll-${dashboard_index}"
>
<li
class=
"actions-item"
id=
"actions-item-unenroll-${dashboard_index}"
>
% if is_paid_course and show_refund_option:
% if is_paid_course and show_refund_option:
% if not is_course_blocked:
% if not is_course_blocked:
...
...
openedx/core/djangoapps/course_groups/management/commands/post_cohort_membership_fix.py
0 → 100644
View file @
24ee5a68
"""
Intended to fix any inconsistencies that may arise during the rollout of the CohortMembership model.
Illustration: https://gist.github.com/efischer19/d62f8ee42b7fbfbc6c9a
"""
from
django.core.management.base
import
BaseCommand
from
django.db
import
IntegrityError
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroup
,
CohortMembership
class
Command
(
BaseCommand
):
"""
Repair any inconsistencies between CourseUserGroup and CohortMembership. To be run after migration 0006.
"""
help
=
'''
Repairs any potential inconsistencies made in the window between running migrations 0005 and 0006, and deploying
the code changes to enforce use of CohortMembership that go with said migrations.
|commit|: optional argument. If not provided, will dry-run and list number of operations that would be made.
'''
def
handle
(
self
,
*
args
,
**
options
):
"""
Execute the command. Since this is designed to fix any issues cause by running pre-CohortMembership code
with the database already migrated to post-CohortMembership state, we will use the pre-CohortMembership
table CourseUserGroup as the canonical source of truth. This way, changes made in the window are persisted.
"""
commit
=
False
if
len
(
args
)
==
1
:
commit
=
args
[
0
]
==
'commit'
memberships_to_delete
=
0
memberships_to_add
=
0
# Begin by removing any data in CohortMemberships that does not match CourseUserGroups data
for
membership
in
CohortMembership
.
objects
.
all
():
try
:
CourseUserGroup
.
objects
.
get
(
group_type
=
CourseUserGroup
.
COHORT
,
users__id
=
membership
.
user
.
id
,
course_id
=
membership
.
course_id
,
id
=
membership
.
course_user_group
.
id
)
except
CourseUserGroup
.
DoesNotExist
:
memberships_to_delete
+=
1
if
commit
:
membership
.
delete
()
# Now we can add any CourseUserGroup data that is missing a backing CohortMembership
for
course_group
in
CourseUserGroup
.
objects
.
filter
(
group_type
=
CourseUserGroup
.
COHORT
):
for
user
in
course_group
.
users
.
all
():
try
:
CohortMembership
.
objects
.
get
(
user
=
user
,
course_id
=
course_group
.
course_id
,
course_user_group_id
=
course_group
.
id
)
except
CohortMembership
.
DoesNotExist
:
memberships_to_add
+=
1
if
commit
:
membership
=
CohortMembership
(
course_user_group
=
course_group
,
user
=
user
,
course_id
=
course_group
.
course_id
)
try
:
membership
.
save
()
except
IntegrityError
:
# If the user is in multiple cohorts, we arbitrarily choose between them
# In this case, allow the pre-existing entry to be "correct"
course_group
.
users
.
remove
(
user
)
user
.
course_groups
.
remove
(
course_group
)
print
'{} CohortMemberships did not match the CourseUserGroup table and will be deleted'
.
format
(
memberships_to_delete
)
print
'{} CourseUserGroup users do not have a CohortMembership; one will be added if it is valid'
.
format
(
memberships_to_add
)
if
commit
:
print
'Changes have been made and saved.'
else
:
print
'Dry run, changes have not been saved. Run again with "commit" argument to save changes'
openedx/core/djangoapps/course_groups/management/commands/remove_users_from_multiple_cohorts.py
deleted
100644 → 0
View file @
e46b4396
"""
Script for removing users with multiple cohorts of a course from cohorts
to ensure user's uniqueness for a course cohorts
"""
from
django.contrib.auth.models
import
User
from
django.core.management.base
import
BaseCommand
from
django.db.models
import
Count
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroup
class
Command
(
BaseCommand
):
"""
Remove users with multiple cohorts of a course from all cohorts
"""
help
=
'Remove all users from multiple cohorts (except one) of each course'
def
handle
(
self
,
*
args
,
**
options
):
"""
Execute the command
"""
# Get entries of cohorts which have same user added multiple times for a single course
multiple_objects_cohorts
=
CourseUserGroup
.
objects
.
filter
(
group_type
=
CourseUserGroup
.
COHORT
)
.
\
values_list
(
'users'
,
'course_id'
)
.
annotate
(
user_count
=
Count
(
'users'
))
.
filter
(
user_count__gt
=
1
)
.
\
order_by
(
'users'
)
multiple_objects_cohorts_count
=
multiple_objects_cohorts
.
count
()
multiple_course_cohorts_users
=
set
(
multiple_objects_cohorts
.
values_list
(
'users'
,
flat
=
True
))
users_failed_to_cleanup
=
[]
for
user
in
User
.
objects
.
filter
(
id__in
=
multiple_course_cohorts_users
):
print
u"Removing user with id '{0}' from cohort groups"
.
format
(
user
.
id
)
try
:
# remove user from only cohorts
user
.
course_groups
.
remove
(
*
user
.
course_groups
.
filter
(
group_type
=
CourseUserGroup
.
COHORT
))
except
AttributeError
as
err
:
users_failed_to_cleanup
.
append
(
user
.
email
)
print
u"Failed to remove user with id {0} from cohort groups, error: {1}"
.
format
(
user
.
id
,
err
)
print
"="
*
80
print
u"="
*
30
+
u"> Cohorts summary"
print
(
u"Total number of CourseUserGroup of type '{0}' with multiple users: {1}"
.
format
(
CourseUserGroup
.
COHORT
,
multiple_objects_cohorts_count
)
)
print
(
u"Total number of unique users with multiple course cohorts: {0}"
.
format
(
len
(
multiple_course_cohorts_users
)
)
)
print
(
u"Users which failed on cohorts cleanup [{0}]: [{1}]"
.
format
(
len
(
users_failed_to_cleanup
),
(
', '
.
join
(
users_failed_to_cleanup
))
)
)
print
"="
*
80
openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py
0 → 100644
View file @
24ee5a68
"""
Test for the post-migration fix commands that are included with this djangoapp
"""
from
django.core.management
import
call_command
from
django.test.client
import
RequestFactory
from
openedx.core.djangoapps.course_groups.views
import
cohort_handler
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort_by_name
from
openedx.core.djangoapps.course_groups.tests.helpers
import
config_course_cohorts
from
openedx.core.djangoapps.course_groups.models
import
CohortMembership
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
class
TestPostMigrationFix
(
ModuleStoreTestCase
):
"""
Base class for testing post-migration fix commands
"""
def
setUp
(
self
):
"""
setup course, user and request for tests
"""
super
(
TestPostMigrationFix
,
self
)
.
setUp
()
self
.
course1
=
CourseFactory
.
create
()
self
.
course2
=
CourseFactory
.
create
()
self
.
user1
=
UserFactory
(
is_staff
=
True
)
self
.
user2
=
UserFactory
(
is_staff
=
True
)
self
.
request
=
RequestFactory
()
.
get
(
"dummy_url"
)
self
.
request
.
user
=
self
.
user1
def
test_post_cohortmembership_fix
(
self
):
"""
Test that changes made *after* migration, but *before* turning on new code are handled properly
"""
# First, we're going to simulate some problem states that can arise during this window
config_course_cohorts
(
self
.
course1
,
is_cohorted
=
True
,
auto_cohorts
=
[
"Course1AutoGroup1"
,
"Course1AutoGroup2"
])
# Get the cohorts from the courses, which will cause auto cohorts to be created
cohort_handler
(
self
.
request
,
unicode
(
self
.
course1
.
id
))
course_1_auto_cohort_1
=
get_cohort_by_name
(
self
.
course1
.
id
,
"Course1AutoGroup1"
)
course_1_auto_cohort_2
=
get_cohort_by_name
(
self
.
course1
.
id
,
"Course1AutoGroup2"
)
# When migrations were first run, the users were assigned to CohortMemberships correctly
membership1
=
CohortMembership
(
course_id
=
course_1_auto_cohort_1
.
course_id
,
user
=
self
.
user1
,
course_user_group
=
course_1_auto_cohort_1
)
membership1
.
save
()
membership2
=
CohortMembership
(
course_id
=
course_1_auto_cohort_1
.
course_id
,
user
=
self
.
user2
,
course_user_group
=
course_1_auto_cohort_1
)
membership2
.
save
()
# But before CohortMembership code was turned on, some changes were made:
course_1_auto_cohort_2
.
users
.
add
(
self
.
user1
)
# user1 is now in 2 cohorts in the same course!
course_1_auto_cohort_2
.
users
.
add
(
self
.
user2
)
course_1_auto_cohort_1
.
users
.
remove
(
self
.
user2
)
# and user2 was moved, but no one told CohortMembership!
# run the post-CohortMembership command, dry-run
call_command
(
'post_cohort_membership_fix'
)
# Verify nothing was changed in dry-run mode.
self
.
assertEqual
(
self
.
user1
.
course_groups
.
count
(),
2
)
# CourseUserGroup has 2 entries for user1
self
.
assertEqual
(
CohortMembership
.
objects
.
get
(
user
=
self
.
user2
)
.
course_user_group
.
name
,
'Course1AutoGroup1'
)
user2_cohorts
=
list
(
self
.
user2
.
course_groups
.
values_list
(
'name'
,
flat
=
True
))
self
.
assertEqual
(
user2_cohorts
,
[
'Course1AutoGroup2'
])
# CourseUserGroup and CohortMembership disagree
# run the post-CohortMembership command, and commit it
call_command
(
'post_cohort_membership_fix'
,
'commit'
)
# verify that both databases agree about the (corrected) state of the memberships
self
.
assertEqual
(
self
.
user1
.
course_groups
.
count
(),
1
)
self
.
assertEqual
(
CohortMembership
.
objects
.
filter
(
user
=
self
.
user1
)
.
count
(),
1
)
self
.
assertEqual
(
self
.
user2
.
course_groups
.
count
(),
1
)
self
.
assertEqual
(
CohortMembership
.
objects
.
filter
(
user
=
self
.
user2
)
.
count
(),
1
)
self
.
assertEqual
(
CohortMembership
.
objects
.
get
(
user
=
self
.
user2
)
.
course_user_group
.
name
,
'Course1AutoGroup2'
)
user2_cohorts
=
list
(
self
.
user2
.
course_groups
.
values_list
(
'name'
,
flat
=
True
))
self
.
assertEqual
(
user2_cohorts
,
[
'Course1AutoGroup2'
])
openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py
deleted
100644 → 0
View file @
e46b4396
"""
Tests for cleanup of users which are added in multiple cohorts of a course
"""
from
django.core.exceptions
import
MultipleObjectsReturned
from
django.core.management
import
call_command
from
django.test.client
import
RequestFactory
from
openedx.core.djangoapps.course_groups.views
import
cohort_handler
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort
,
get_cohort_by_name
from
openedx.core.djangoapps.course_groups.tests.helpers
import
config_course_cohorts
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
class
TestMultipleCohortUsers
(
ModuleStoreTestCase
):
"""
Base class for testing users with multiple cohorts
"""
def
setUp
(
self
):
"""
setup course, user and request for tests
"""
super
(
TestMultipleCohortUsers
,
self
)
.
setUp
()
self
.
course1
=
CourseFactory
.
create
()
self
.
course2
=
CourseFactory
.
create
()
self
.
user1
=
UserFactory
(
is_staff
=
True
)
self
.
user2
=
UserFactory
(
is_staff
=
True
)
self
.
request
=
RequestFactory
()
.
get
(
"dummy_url"
)
self
.
request
.
user
=
self
.
user1
def
test_users_with_multiple_cohorts_cleanup
(
self
):
"""
Test that user which have been added in multiple cohorts of a course,
can get cohorts without error after running cohorts cleanup command
"""
# set two auto_cohort_groups for both courses
config_course_cohorts
(
self
.
course1
,
is_cohorted
=
True
,
auto_cohorts
=
[
"Course1AutoGroup1"
,
"Course1AutoGroup2"
]
)
config_course_cohorts
(
self
.
course2
,
is_cohorted
=
True
,
auto_cohorts
=
[
"Course2AutoGroup1"
,
"Course2AutoGroup2"
]
)
# get the cohorts from the courses, which will cause auto cohorts to be created
cohort_handler
(
self
.
request
,
unicode
(
self
.
course1
.
id
))
cohort_handler
(
self
.
request
,
unicode
(
self
.
course2
.
id
))
course_1_auto_cohort_1
=
get_cohort_by_name
(
self
.
course1
.
id
,
"Course1AutoGroup1"
)
course_1_auto_cohort_2
=
get_cohort_by_name
(
self
.
course1
.
id
,
"Course1AutoGroup2"
)
course_2_auto_cohort_1
=
get_cohort_by_name
(
self
.
course2
.
id
,
"Course2AutoGroup1"
)
# forcefully add user1 in two auto cohorts
course_1_auto_cohort_1
.
users
.
add
(
self
.
user1
)
course_1_auto_cohort_2
.
users
.
add
(
self
.
user1
)
# forcefully add user2 in auto cohorts of both courses
course_1_auto_cohort_1
.
users
.
add
(
self
.
user2
)
course_2_auto_cohort_1
.
users
.
add
(
self
.
user2
)
# now check that when user1 goes on discussion page and tries to get
# cohorts 'MultipleObjectsReturned' exception is returned
with
self
.
assertRaises
(
MultipleObjectsReturned
):
get_cohort
(
self
.
user1
,
self
.
course1
.
id
)
# also check that user 2 can go on discussion page of both courses
# without any exception
get_cohort
(
self
.
user2
,
self
.
course1
.
id
)
get_cohort
(
self
.
user2
,
self
.
course2
.
id
)
# call command to remove users added in multiple cohorts of a course
# are removed from all cohort groups
call_command
(
'remove_users_from_multiple_cohorts'
)
# check that only user1 (with multiple cohorts) is removed from cohorts
# and user2 is still in auto cohorts of both course after running
# 'remove_users_from_multiple_cohorts' management command
self
.
assertEqual
(
self
.
user1
.
course_groups
.
count
(),
0
)
self
.
assertEqual
(
self
.
user2
.
course_groups
.
count
(),
2
)
user2_cohorts
=
list
(
self
.
user2
.
course_groups
.
values_list
(
'name'
,
flat
=
True
))
self
.
assertEqual
(
user2_cohorts
,
[
'Course1AutoGroup1'
,
'Course2AutoGroup1'
])
# now check that user1 can get cohorts in which he is added
response
=
cohort_handler
(
self
.
request
,
unicode
(
self
.
course1
.
id
))
self
.
assertEqual
(
response
.
status_code
,
200
)
openedx/core/djangoapps/course_groups/migrations/rerun_0006.sh
deleted
100644 → 0
View file @
e46b4396
#!/bin/bash
if
[
$#
-eq
0
]
;
then
echo
"
$0
: usage: rerun_0006.sh <arguments>. At minimum, '--settings=<environment>' is expected."
exit
1
fi
./manage.py lms migrate course_groups 0005
--fake
"
$@
"
./manage.py lms migrate course_groups 0006
"
$@
"
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