Commit 9519087d by Victor Shnayder

Merge pull request #1271 from MITx/feature/victor/beta-testers

Feature/victor/beta testers
parents 39655bff b188c8e7
......@@ -7,8 +7,11 @@ TIME_FORMAT = "%Y-%m-%dT%H:%M"
def parse_time(time_str):
"""
Takes a time string in TIME_FORMAT, returns
it as a time_struct. Raises ValueError if the string is not in the right format.
Takes a time string in TIME_FORMAT
Returns it as a time_struct.
Raises ValueError if the string is not in the right format.
"""
return time.strptime(time_str, TIME_FORMAT)
......
......@@ -414,7 +414,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
'xqa_key',
# TODO: This is used by the XMLModuleStore to provide for locations for
# static files, and will need to be removed when that code is removed
'data_dir'
'data_dir',
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta'
)
# cdodge: this is a list of metadata names which are 'system' metadata
......@@ -497,13 +501,27 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
@property
def start(self):
"""
If self.metadata contains start, return it. Else return None.
If self.metadata contains a valid start time, return it as a time struct.
Else return None.
"""
if 'start' not in self.metadata:
return None
return self._try_parse_time('start')
@property
def days_early_for_beta(self):
"""
If self.metadata contains start, return the number, as a float. Else return None.
"""
if 'days_early_for_beta' not in self.metadata:
return None
try:
return float(self.metadata['days_early_for_beta'])
except ValueError:
return None
@property
def own_metadata(self):
"""
Return the metadata that is not inherited, but was defined on this module.
......@@ -715,7 +733,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
Returns a time_struct, or None if metadata key is not present or is invalid.
"""
if key in self.metadata:
try:
......
......@@ -257,6 +257,7 @@ Supported fields at the course level:
* "tabs" -- have custom tabs in the courseware. See below for details on config.
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* "show_calculator" (value "Yes" if desired)
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
* TODO: there are others
### Grading policy file contents
......
......@@ -4,13 +4,13 @@ like DISABLE_START_DATES"""
import logging
import time
from datetime import datetime, timedelta
from django.conf import settings
from xmodule.course_module import CourseDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed
......@@ -73,7 +73,7 @@ def has_access(user, obj, action):
raise TypeError("Unknown object type in has_access(): '{0}'"
.format(type(obj)))
def get_access_group_name(obj,action):
def get_access_group_name(obj, action):
'''
Returns group name for user group which has "action" access to the given object.
......@@ -226,9 +226,10 @@ def _has_access_descriptor(user, descriptor, action):
# Check start date
if descriptor.start is not None:
now = time.gmtime()
if now > descriptor.start:
effective_start = _adjust_start_date_for_beta_testers(user, descriptor)
if now > effective_start:
# after start date, everyone can see it
debug("Allow: now > start date")
debug("Allow: now > effective start date")
return True
# otherwise, need staff access
return _has_staff_access_to_descriptor(user, descriptor)
......@@ -328,6 +329,15 @@ def _course_staff_group_name(location):
"""
return 'staff_%s' % Location(location).course
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
def _course_instructor_group_name(location):
"""
......@@ -348,6 +358,51 @@ def _has_global_staff_access(user):
return False
def _adjust_start_date_for_beta_testers(user, descriptor):
"""
If user is in a beta test group, adjust the start date by the appropriate number of
days.
Arguments:
user: A django user. May be anonymous.
descriptor: the XModuleDescriptor the user is trying to get access to, with a
non-None start date.
Returns:
A time, in the same format as returned by time.gmtime(). Either the same as
start, or earlier for beta testers.
NOTE: number of days to adjust should be cached to avoid looking it up thousands of
times per query.
NOTE: For now, this function assumes that the descriptor's location is in the course
the user is looking at. Once we have proper usages and definitions per the XBlock
design, this should use the course the usage is in.
NOTE: If testing manually, make sure MITX_FEATURES['DISABLE_START_DATES'] = False
in envs/dev.py!
"""
if descriptor.days_early_for_beta is None:
# bail early if no beta testing is set up
return descriptor.start
user_groups = [g.name for g in user.groups.all()]
beta_group = course_beta_test_group_name(descriptor.location)
if beta_group in user_groups:
debug("Adjust start time: user in group %s", beta_group)
# time_structs don't support subtraction, so convert to datetimes,
# subtract, convert back.
# (fun fact: datetime(*a_time_struct[:6]) is the beautiful syntax for
# converting time_structs into datetimes)
start_as_datetime = datetime(*descriptor.start[:6])
delta = timedelta(descriptor.days_early_for_beta)
effective = start_as_datetime - delta
# ...and back to time_struct
return effective.timetuple()
return descriptor.start
def _has_instructor_access_to_location(user, location):
return _has_access_to_location(user, location, 'instructor')
......
......@@ -17,7 +17,8 @@ import xmodule.modulestore.django
# Need access to internal func to put users in the right group
from courseware import grades
from courseware.access import _course_staff_group_name
from courseware.access import (has_access, _course_staff_group_name,
course_beta_test_group_name)
from courseware.models import StudentModuleCache
from student.models import Registration
......@@ -238,7 +239,7 @@ class PageLoader(ActivateLoginTestCase):
n = 0
num_bad = 0
all_ok = True
for descriptor in module_store.modules[course_id].itervalues():
for descriptor in module_store.modules[course_id].itervalues():
n += 1
print "Checking ", descriptor.location.url()
#print descriptor.__class__, descriptor.location
......@@ -259,11 +260,11 @@ class PageLoader(ActivateLoginTestCase):
# check content to make sure there were no rendering failures
content = resp.content
if content.find("this module is temporarily unavailable")>=0:
msg = "ERROR unavailable module "
msg = "ERROR unavailable module "
all_ok = False
num_bad += 1
elif isinstance(descriptor, ErrorDescriptor):
msg = "ERROR error descriptor loaded: "
msg = "ERROR error descriptor loaded: "
msg = msg + descriptor.definition['data']['error_msg']
all_ok = False
num_bad += 1
......@@ -286,7 +287,7 @@ class TestCoursesLoadTestCase(PageLoader):
# xmodule.modulestore.django.modulestore().collection.drop()
# store = xmodule.modulestore.django.modulestore()
# is there a way to empty the store?
def test_toy_course_loads(self):
self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
......@@ -453,6 +454,9 @@ class TestViewAuth(PageLoader):
"""Check that enrollment periods work"""
self.run_wrapped(self._do_test_enrollment_period)
def test_beta_period(self):
"""Check that beta-test access works"""
self.run_wrapped(self._do_test_beta_period)
def _do_test_dark_launch(self):
"""Actually do the test, relying on settings to be right."""
......@@ -618,6 +622,38 @@ class TestViewAuth(PageLoader):
self.unenroll(self.toy)
self.assertTrue(self.try_enroll(self.toy))
def _do_test_beta_period(self):
"""Actually test beta periods, relying on settings to be right."""
# trust, but verify :)
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
# Make courses start in the future
tomorrow = time.time() + 24 * 3600
nextday = tomorrow + 24 * 3600
yesterday = time.time() - 24 * 3600
# toy course's hasn't started
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
self.assertFalse(self.toy.has_started())
# but should be accessible for beta testers
self.toy.metadata['days_early_for_beta'] = '2'
# student user shouldn't see it
student_user = user(self.student)
self.assertFalse(has_access(student_user, self.toy, 'load'))
# now add the student to the beta test group
group_name = course_beta_test_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(student_user)
# now the student should see it
self.assertTrue(has_access(student_user, self.toy, 'load'))
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCourseGrader(PageLoader):
"""Check that a course gets graded properly"""
......
......@@ -179,7 +179,7 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename))>=0)
self.assertFalse(has_forum_access(username, course.id, rolename))
def test_add_and_readd_forum_admin_users(self):
def test_add_and_read_forum_admin_users(self):
course = self.toy
self.initialize_roles(course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
......
......@@ -36,6 +36,9 @@ table.stat_table td {
a.selectedmode { background-color: yellow; }
textarea {
height: 200px;
}
</style>
<script language="JavaScript" type="text/javascript">
......@@ -58,8 +61,8 @@ function goto( mode)
%endif
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a>
]
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> |
<a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a> ]
</h2>
<div style="text-align:right"><span id="djangopid">${djangopid}</span>
......@@ -168,7 +171,8 @@ function goto( mode)
<p>
<input type="submit" name="action" value="List course staff members">
<p>
<input type="text" name="staffuser"> <input type="submit" name="action" value="Remove course staff">
<input type="text" name="staffuser">
<input type="submit" name="action" value="Remove course staff">
<input type="submit" name="action" value="Add course staff">
<hr width="40%" style="align:left">
%endif
......@@ -250,7 +254,7 @@ function goto( mode)
%endif
<p>Add students: enter emails, separated by returns or commas;</p>
<p>Add students: enter emails, separated by new lines or commas;</p>
<textarea rows="6" cols="70" name="enroll_multiple"></textarea>
<input type="submit" name="action" value="Enroll multiple students">
......@@ -258,6 +262,24 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Manage Groups'):
%if instructor_access:
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List beta testers">
<p>
Enter usernames or emails for students who should be beta-testers, one per line, or separated by commas. They will get to
see course materials early, as configured via the <tt>days_early_for_beta</tt> option in the course policy.
</p>
<p>
<textarea cols="50" row="30" name="betausers"></textarea>
<input type="submit" name="action" value="Remove beta testers">
<input type="submit" name="action" value="Add beta testers">
</p>
<hr width="40%" style="align:left">
%endif
%endif
</form>
##-----------------------------------------------------------------------------
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment