Commit 7d7513b6 by David Baumgold

Merge pull request #470 from edx/db/course-team-admin-grants

Grant admin rights to members of the course team
parents 6a7fa41e 0022469e
...@@ -18,6 +18,9 @@ the setting is not present, the API is disabled). ...@@ -18,6 +18,9 @@ the setting is not present, the API is disabled).
LMS: Added endpoints for AJAX requests to enable/disable notifications LMS: Added endpoints for AJAX requests to enable/disable notifications
(which are not yet implemented) and a one-click unsubscribe page. (which are not yet implemented) and a one-click unsubscribe page.
Studio: Allow instructors of a course to designate other staff as instructors;
this allows instructors to hand off management of a course to someone else.
Common: Add a manage.py that knows about edx-platform specific settings and projects Common: Add a manage.py that knows about edx-platform specific settings and projects
Common: Added *experimental* support for jsinput type. Common: Added *experimental* support for jsinput type.
......
...@@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name): ...@@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name):
user.save() user.save()
def is_user_in_course_group_role(user, location, role): def is_user_in_course_group_role(user, location, role, check_staff=True):
if user.is_active and user.is_authenticated: if user.is_active and user.is_authenticated:
# all "is_staff" flagged accounts belong to all groups # all "is_staff" flagged accounts belong to all groups
return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 if check_staff and user.is_staff:
return True
return user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
return False return False
......
...@@ -53,6 +53,14 @@ def i_have_opened_a_new_course(_step): ...@@ -53,6 +53,14 @@ def i_have_opened_a_new_course(_step):
open_new_course() open_new_course()
@step('(I select|s?he selects) the new course')
def select_new_course(_step, whom):
course_link_xpath = '//div[contains(@class, "courses")]//a[contains(@class, "class-link")]//span[contains(., "{name}")]/..'.format(
name="Robot Super Course")
element = world.browser.find_by_xpath(course_link_xpath)
element.click()
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name): def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower() css = 'a.action-%s' % name.lower()
...@@ -118,6 +126,8 @@ def create_studio_user( ...@@ -118,6 +126,8 @@ def create_studio_user(
registration.register(studio_user) registration.register(studio_user)
registration.activate() registration.activate()
return studio_user
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
......
...@@ -16,7 +16,11 @@ def create_component_instance(step, component_button_css, category, ...@@ -16,7 +16,11 @@ def create_component_instance(step, component_button_css, category,
if has_multiple_templates: if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css) click_component_from_menu(category, boilerplate, expected_css)
assert_equal(1, len(world.css_find(expected_css))) assert_equal(
1,
len(world.css_find(expected_css)),
"Component instance with css {css} was not created successfully".format(css=expected_css))
@world.absorb @world.absorb
def click_new_component_button(step, component_button_css): def click_new_component_button(step, component_button_css):
......
Feature: Course Team Feature: Course Team
As a course author, I want to be able to add others to my team As a course author, I want to be able to add others to my team
Scenario: Users can add other users Scenario: Admins can add other users
Given I have opened a new course in Studio Given I have opened a new course in Studio
And the user "alice" exists And the user "alice" exists
And I am viewing the course team settings And I am viewing the course team settings
...@@ -9,7 +9,7 @@ Feature: Course Team ...@@ -9,7 +9,7 @@ Feature: Course Team
And "alice" logs in And "alice" logs in
Then she does see the course on her page Then she does see the course on her page
Scenario: Added users cannot delete or add other users Scenario: Added admins cannot delete or add other users
Given I have opened a new course in Studio Given I have opened a new course in Studio
And the user "bob" exists And the user "bob" exists
And I am viewing the course team settings And I am viewing the course team settings
...@@ -18,7 +18,7 @@ Feature: Course Team ...@@ -18,7 +18,7 @@ Feature: Course Team
Then he cannot delete users Then he cannot delete users
And he cannot add users And he cannot add users
Scenario: Users can delete other users Scenario: Admins can delete other users
Given I have opened a new course in Studio Given I have opened a new course in Studio
And the user "carol" exists And the user "carol" exists
And I am viewing the course team settings And I am viewing the course team settings
...@@ -27,8 +27,33 @@ Feature: Course Team ...@@ -27,8 +27,33 @@ Feature: Course Team
And "carol" logs in And "carol" logs in
Then she does not see the course on her page Then she does not see the course on her page
Scenario: Users cannot add users that do not exist Scenario: Admins cannot add users that do not exist
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I am viewing the course team settings And I am viewing the course team settings
When I add "dennis" to the course team When I add "dennis" to the course team
Then I should see "Could not find user by email address" somewhere on the page Then I should see "Could not find user by email address" somewhere on the page
Scenario: Admins should be able to make other people into admins
Given I have opened a new course in Studio
And the user "emily" exists
And I am viewing the course team settings
And I add "emily" to the course team
When I make "emily" a course team admin
And "emily" logs in
And she selects the new course
And she views the course team settings
Then "emily" should be marked as an admin
And she can add users
And she can delete users
Scenario: Admins should be able to remove other admins
Given I have opened a new course in Studio
And the user "frank" exists as a course admin
And I am viewing the course team settings
When I remove admin rights from "frank"
And "frank" logs in
And he selects the new course
And he views the course team settings
Then "frank" should not be marked as an admin
And he cannot add users
And he cannot delete users
...@@ -3,65 +3,105 @@ ...@@ -3,65 +3,105 @@
from lettuce import world, step from lettuce import world, step
from common import create_studio_user, log_into_studio from common import create_studio_user, log_into_studio
from django.contrib.auth.models import Group
from auth.authz import get_course_groupname_for_role
PASSWORD = 'test' PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org' EMAIL_EXTENSION = '@edx.org'
@step(u'I am viewing the course team settings') @step(u'(I am viewing|s?he views) the course team settings')
def view_grading_settings(_step): def view_grading_settings(_step, whom):
world.click_course_settings() world.click_course_settings()
link_css = 'li.nav-course-settings-team a' link_css = 'li.nav-course-settings-team a'
world.css_click(link_css) world.css_click(link_css)
@step(u'the user "([^"]*)" exists$') @step(u'the user "([^"]*)" exists( as a course admin)?$')
def create_other_user(_step, name): def create_other_user(_step, name, course_admin):
create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) user = create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
if course_admin:
location = world.scenario_dict["COURSE"].location
for role in ("staff", "instructor"):
group, __ = Group.objects.get_or_create(name=get_course_groupname_for_role(location, role))
user.groups.add(group)
user.save()
@step(u'I add "([^"]*)" to the course team') @step(u'I add "([^"]*)" to the course team')
def add_other_user(_step, name): def add_other_user(_step, name):
new_user_css = 'a.new-user-button' new_user_css = 'a.create-user-button'
world.css_click(new_user_css) world.css_click(new_user_css)
world.wait(0.5)
email_css = 'input.email-input' email_css = 'input#user-email-input'
f = world.css_find(email_css) f = world.css_find(email_css)
f._element.send_keys(name, EMAIL_EXTENSION) f._element.send_keys(name, EMAIL_EXTENSION)
confirm_css = '#add_user' confirm_css = 'form.create-user button.action-primary'
world.css_click(confirm_css) world.css_click(confirm_css)
@step(u'I delete "([^"]*)" from the course team') @step(u'I delete "([^"]*)" from the course team')
def delete_other_user(_step, name): def delete_other_user(_step, name):
to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
email="{0}{1}".format(name, EMAIL_EXTENSION))
world.css_click(to_delete_css) world.css_click(to_delete_css)
@step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
email=name+EMAIL_EXTENSION)
world.css_click(admin_btn_css)
@step(u'I remove admin rights from "([^"]*)"')
def remove_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
email=name+EMAIL_EXTENSION)
world.css_click(admin_btn_css)
@step(u'"([^"]*)" logs in$') @step(u'"([^"]*)" logs in$')
def other_user_login(_step, name): def other_user_login(_step, name):
log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION)
@step(u's?he does( not)? see the course on (his|her) page') @step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, doesnt_see_course, gender): def see_course(_step, inverted, gender):
class_css = 'span.class-name' class_css = 'span.class-name'
all_courses = world.css_find(class_css, wait_time=1) all_courses = world.css_find(class_css, wait_time=1)
all_names = [item.html for item in all_courses] all_names = [item.html for item in all_courses]
if doesnt_see_course: if inverted:
assert not world.scenario_dict['COURSE'].display_name in all_names assert not world.scenario_dict['COURSE'].display_name in all_names
else: else:
assert world.scenario_dict['COURSE'].display_name in all_names assert world.scenario_dict['COURSE'].display_name in all_names
@step(u's?he cannot delete users') @step(u'"([^"]*)" should( not)? be marked as an admin')
def cannot_delete(_step): def marked_as_admin(_step, name, inverted):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+EMAIL_EXTENSION)
if inverted:
assert world.is_css_not_present(flag_css)
else:
assert world.is_css_present(flag_css)
@step(u's?he can(not)? delete users')
def can_delete_users(_step, inverted):
to_delete_css = 'a.remove-user' to_delete_css = 'a.remove-user'
assert world.is_css_not_present(to_delete_css) if inverted:
assert world.is_css_not_present(to_delete_css)
else:
assert world.is_css_present(to_delete_css)
@step(u's?he cannot add users') @step(u's?he can(not)? add users')
def cannot_add(_step): def can_add_users(_step, inverted):
add_css = 'a.new-user' add_css = 'a.create-user-button'
assert world.is_css_not_present(add_css) if inverted:
assert world.is_css_not_present(add_css)
else:
assert world.is_css_present(add_css)
""" Unit tests for checklist methods in views.py. """ """ Unit tests for checklist methods in views.py. """
from contentstore.utils import get_modulestore, get_url_reverse from contentstore.utils import get_modulestore
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -38,7 +38,11 @@ class ChecklistTestCase(CourseTestCase): ...@@ -38,7 +38,11 @@ class ChecklistTestCase(CourseTestCase):
def test_get_checklists(self): def test_get_checklists(self):
""" Tests the get checklists method. """ """ Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course) checklists_url = reverse("checklists", kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
response = self.client.get(checklists_url) response = self.client.get(checklists_url)
self.assertContains(response, "Getting Started With Studio") self.assertContains(response, "Getting Started With Studio")
payload = response.content payload = response.content
......
...@@ -1167,7 +1167,9 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1167,7 +1167,9 @@ class ContentStoreTest(ModuleStoreTestCase):
# manage users # manage users
resp = self.client.get(reverse('manage_users', resp = self.client.get(reverse('manage_users',
kwargs={'location': loc.url()})) kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
# course info # course info
......
...@@ -72,50 +72,6 @@ class LMSLinksTestCase(TestCase): ...@@ -72,50 +72,6 @@ class LMSLinksTestCase(TestCase):
) )
class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """
def test_course_page_names(self):
""" Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('ManageUsers', course)
)
self.assertEquals(
'/mitX/666/settings-details/URL_Reverse_Course',
utils.get_url_reverse('SettingsDetails', course)
)
self.assertEquals(
'/mitX/666/settings-grading/URL_Reverse_Course',
utils.get_url_reverse('SettingsGrading', course)
)
self.assertEquals(
'/mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('CourseOutline', course)
)
self.assertEquals(
'/mitX/666/checklists/URL_Reverse_Course',
utils.get_url_reverse('Checklists', course)
)
def test_unknown_passes_through(self):
""" Test that unknown values pass through. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'foobar',
utils.get_url_reverse('foobar', course)
)
self.assertEquals(
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
class ExtraPanelTabTestCase(TestCase): class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """ """ Tests adding and removing extra course tabs. """
......
...@@ -188,38 +188,6 @@ def update_item(location, value): ...@@ -188,38 +188,6 @@ def update_item(location, value):
get_modulestore(location).update_item(location, value) get_modulestore(location).update_item(location, value)
def get_url_reverse(course_page_name, course_module):
"""
Returns the course URL link to the specified location. This value is suitable to use as an href link.
course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
course_page_names so that it can also be used for absolute (known) URLs.
course_module is used to obtain the location, org, course, and name properties for a course, if
course_page_name corresponds to an attribute in CoursePageNames.
"""
url_name = getattr(CoursePageNames, course_page_name, None)
ctx_loc = course_module.location
if CoursePageNames.ManageUsers == url_name:
return reverse(url_name, kwargs={"location": ctx_loc})
elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
else:
return course_page_name
class CoursePageNames:
""" Constants for pages that are recognized by get_url_reverse method. """
ManageUsers = "manage_users"
SettingsDetails = "settings_details"
SettingsGrading = "settings_grading"
CourseOutline = "course_index"
Checklists = "checklists"
def add_extra_panel_tab(tab_type, course): def add_extra_panel_tab(tab_type, course):
""" """
Used to add the panel tab to a course if it does not exist. Used to add the panel tab to a course if it does not exist.
......
...@@ -29,7 +29,6 @@ from xmodule.util.date_utils import get_default_time_display ...@@ -29,7 +29,6 @@ from xmodule.util.date_utils import get_default_time_display
from xmodule.modulestore import InvalidLocationError from xmodule.modulestore import InvalidLocationError
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from ..utils import get_url_reverse
from .access import get_location_and_verify_access from .access import get_location_and_verify_access
from util.json_request import JsonResponse from util.json_request import JsonResponse
...@@ -320,7 +319,11 @@ def import_course(request, org, course, name): ...@@ -320,7 +319,11 @@ def import_course(request, org, course, name):
return render_to_response('import.html', { return render_to_response('import.html', {
'context_course': course_module, 'context_course': course_module,
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module) 'successful_import_redirect_url': reverse('course_index', kwargs={
'org': location.org,
'course': location.course,
'name': location.name,
})
}) })
......
...@@ -4,12 +4,13 @@ from util.json_request import JsonResponse ...@@ -4,12 +4,13 @@ from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.urlresolvers import reverse
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse from ..utils import get_modulestore
from .access import get_location_and_verify_access from .access import get_location_and_verify_access
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -96,10 +97,25 @@ def expand_checklist_action_urls(course_module): ...@@ -96,10 +97,25 @@ def expand_checklist_action_urls(course_module):
""" """
checklists = course_module.checklists checklists = course_module.checklists
modified = False modified = False
urlconf_map = {
"ManageUsers": "manage_users",
"SettingsDetails": "settings_details",
"SettingsGrading": "settings_grading",
"CourseOutline": "course_index",
"Checklists": "checklists",
}
for checklist in checklists: for checklist in checklists:
if not checklist.get('action_urls_expanded', False): if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'): for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module) action_url = item.get('action_url')
if action_url not in urlconf_map:
continue
urlconf_name = urlconf_map[action_url]
item['action_url'] = reverse(urlconf_name, kwargs={
'org': course_module.location.org,
'course': course_module.location.course,
'name': course_module.location.name,
})
checklist['action_urls_expanded'] = True checklist['action_urls_expanded'] = True
modified = True modified = True
......
...@@ -93,6 +93,234 @@ form { ...@@ -93,6 +93,234 @@ form {
} }
} }
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ELEM: form
// form styling for creating a new content item (course, user, textbook)
form[class^="create-"] {
@extend .ui-window;
@include box-sizing(border-box);
border-radius: 2px;
width: 100%;
background: $white;
.title {
@extend .t-title4;
font-weight: 600;
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
}
fieldset {
padding: $baseline ($baseline*1.5);
}
.list-input {
@extend .cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
}
&.required {
label {
font-weight: 600;
}
label:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
}
label {
@extend .t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
&.is-focused {
color: $blue;
}
}
input, textarea {
@include transition(all $tmg-f2 ease-in-out 0s);
@extend .t-copy-base;
height: 100%;
width: 100%;
padding: ($baseline/2);
&.long {
width: 100%;
}
&.short {
width: 25%;
}
&:focus {
+ .tip {
color: $gray;
}
}
}
textarea.long {
height: ($baseline*5);
}
input[type="checkbox"] {
display: inline-block;
margin-right: ($baseline/4);
width: auto;
height: auto;
& + label {
display: inline-block;
}
}
.tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.tip-error {
display: none;
float: none;
}
&.error {
label {
color: $red;
}
.tip-error {
@extend .anim-fadeIn;
display: block;
color: $red;
}
input {
border-color: $red;
}
}
}
.field-inline {
input, textarea, select {
width: 62%;
display: inline-block;
}
.tip-stacked {
display: inline-block;
float: right;
width: 35%;
margin-top: 0;
}
&.error {
.tip-error {
}
}
}
.field-group {
@include clearfix();
margin: 0 0 ($baseline/2) 0;
.field {
display: block;
width: 47%;
border-bottom: none;
margin: 0 ($baseline*0.75) 0 0;
padding: ($baseline/4) 0 0 0;
float: left;
position: relative;
&:nth-child(odd) {
float: left;
}
&:nth-child(even) {
float: right;
margin-right: 0;
}
input, textarea {
width: 100%;
}
}
}
}
.actions {
box-shadow: inset 0 1px 2px $shadow;
margin-top: ($baseline*0.75);
border-top: 1px solid $gray-l1;
padding: ($baseline*0.75) ($baseline*1.5);
background: $gray-l6;
.action-primary {
@include blue-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
.action-secondary {
@include grey-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
}
}
// ==================== // ====================
// forms - grandfathered // forms - grandfathered
......
// studio - elements - icons // studio - elements - icons & badges
// ==================== // ====================
.icon { .icon {
...@@ -14,3 +14,45 @@ ...@@ -14,3 +14,45 @@
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/4); margin-right: ($baseline/4);
} }
// ui - badges
.wrapper-ui-badge {
position: absolute;
top: -1px;
left: ($baseline*1.5);
width: 100%;
}
.ui-badge {
@extend .t-title9;
position: relative;
border-bottom-right-radius: ($baseline/10);
border-bottom-left-radius: ($baseline/10);
padding: ($baseline/4) ($baseline/2) ($baseline/4) ($baseline/2);
font-weight: 600;
text-transform: uppercase;
* [class^="icon-"] {
margin-right: ($baseline/5);
}
// OPTION: add this class for a visual hanging display
&.is-hanging {
@include box-sizing(border-box);
@extend .ui-depth2;
top: -($baseline/4);
&:after {
position: absolute;
top: 0;
right: -($baseline/4);
display: block;
height: 0;
width: 0;
border-bottom: ($baseline/4) solid $black-t3;
border-right: ($baseline/4) solid transparent;
content: "";
opacity: 0.5;
}
}
}
...@@ -55,8 +55,8 @@ ...@@ -55,8 +55,8 @@
margin-bottom: $baseline; margin-bottom: $baseline;
.title { .title {
@extend .t-title7; @extend .t-title6;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/2);
font-weight: 700; font-weight: 700;
} }
...@@ -167,6 +167,34 @@ ...@@ -167,6 +167,34 @@
} }
} }
// particular notice - create
.notice-create {
background-color: $gray-l4;
.title {
color: $gray-d2;
}
.copy {
color: $gray-d2;
}
&.has-actions {
.list-actions {
.action-item {
}
.action-primary {
@extend .btn-primary-green;
@extend .t-action3;
}
}
}
}
// particular notice - confirmation // particular notice - confirmation
.notice-confirmation { .notice-confirmation {
background-color: $green-l5; background-color: $green-l5;
......
...@@ -30,7 +30,7 @@ body.course.textbooks { ...@@ -30,7 +30,7 @@ body.course.textbooks {
} }
.textbook { .textbook {
@extend .window; @extend .ui-window;
position: relative; position: relative;
.view-textbook { .view-textbook {
......
...@@ -3,80 +3,227 @@ ...@@ -3,80 +3,227 @@
body.course.users { body.course.users {
.new-user-form { // LAYOUT: page
display: none; .content-primary, .content-supplementary {
padding: 15px 20px; @include box-sizing(border-box);
background-color: $lightBluishGrey2; float: left;
}
#result {
display: none;
float: left;
margin-bottom: 15px;
padding: 3px 15px;
border-radius: 3px;
background: $error-red;
font-size: 14px;
color: #fff;
}
.form-elements { .content-primary {
clear: both; width: flex-grid(9, 12);
} margin-right: flex-gutter();
}
label { .content-supplementary {
display: inline-block; width: flex-grid(3, 12);
margin-right: 10px; }
}
.email-input { // ELEM: content
width: 350px; .content {
padding: 8px 8px 10px;
border-color: $darkGrey; .introduction {
@extend .t-copy-sub1;
margin: 0 0 ($baseline*2) 0;
} }
}
// ELEM: no users notice
.content .notice-create {
width: flexgrid(9, 9);
margin-top: $baseline;
// CASE: notice has actions {
&.has-actions {
.msg, .list-actions {
display: inline-block;
vertical-align: middle;
}
.msg {
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
.list-actions {
width: flex-grid(3, 9);
text-align: right;
margin-top: 0;
.action-item {
}
.add-button { .action-primary {
@include blue-button; @include green-button(); // overwriting for the sake of syncing older green button styles for now
padding: 5px 20px 9px; @extend .t-action3;
padding: ($baseline/2) $baseline;
}
}
} }
}
.cancel-button { // ELEM: new user form
@include white-button; .wrapper-create-user {
padding: 5px 20px 9px;
&.is-shown {
height: ($baseline*15);
} }
} }
// ELEM: listing of users
.user-list, .user-item, .item-metadata, .item-actions {
@include box-sizing(border-box);
}
.user-list { .user-list {
border: 1px solid $mediumGrey;
background: #fff;
li { .user-item {
@extend .ui-window;
@include clearfix();
position: relative; position: relative;
padding: 20px; width: flex-grid(9, 9);
border-bottom: 1px solid $mediumGrey; margin: 0 0 ($baseline/2) 0;
padding: ($baseline*1.25) ($baseline*1.5) $baseline ($baseline*1.5);
&:last-child { &:last-child {
border-bottom: none; margin-bottom: 0;
} }
span { .item-metadata, .item-actions {
display: inline-block; display: inline-block;
vertical-align: middle;
} }
.user-name { // ELEM: item - flag
margin-right: 10px; .flag-role {
font-size: 24px; @extend .ui-badge;
font-weight: 300; color: $white;
.msg-you {
margin-left: ($baseline/5);
text-transform: none;
font-weight: 500;
color: $pink-l3;
}
&:after {
border-bottom-color: $pink-d4;
}
&.flag-role-staff {
background: $pink-u3;
}
&.flag-role-admin {
background: $pink;
}
} }
.user-email { // ELEM: item - metadata
font-size: 14px; .item-metadata {
font-style: italic; width: flex-grid(5, 9);
color: $mediumGrey; margin-right: flex-gutter();
.user-username, .user-email {
display: inline-block;
vertical-align: middle;
}
.user-username {
@extend .t-title4;
@include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 ($baseline/2) ($baseline/10) 0;
color: $gray-d4;
font-weight: 600;
}
.user-email {
@extend .t-title6;
}
} }
// ELEM: item - actions
.item-actions { .item-actions {
top: 24px; width: flex-grid(4, 9);
position: static; // nasty reset needed due to base.scss
text-align: right;
.action {
display: inline-block;
vertical-align: middle;
}
.action-role {
width: flex-grid(3, 4);
margin-right: flex-gutter();
}
.action-delete {
width: flex-grid(1, 4);
// STATE: disabled
&.is-disabled {
opacity: 0.0;
visibility: hidden;
pointer-events: none;
}
}
.delete {
@extend .ui-btn-non;
}
// HACK: nasty reset needed due to base.scss
.delete-button {
margin-right: 0;
float: none;
color: inherit;
}
// ELEM: admin role controls
.toggle-admin-role {
&.add-admin-role {
@include blue-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
}
&.remove-admin-role {
@include grey-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
}
}
.notoggleforyou {
@extend .t-copy-sub1;
color: $gray-l2;
}
}
// STATE: hover
&:hover {
.user-username {
}
.user-email {
}
.item-actions {
}
} }
} }
} }
} }
\ No newline at end of file
...@@ -262,7 +262,7 @@ from contentstore import utils ...@@ -262,7 +262,7 @@ from contentstore import utils
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
</nav> </nav>
......
...@@ -98,7 +98,7 @@ editor.render(); ...@@ -98,7 +98,7 @@ editor.render();
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Course Team")}</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
...@@ -140,7 +140,7 @@ from contentstore import utils ...@@ -140,7 +140,7 @@ from contentstore import utils
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
</nav> </nav>
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
<a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a> <a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a>
</li> </li>
<li class="nav-item nav-course-settings-team"> <li class="nav-item nav-course-settings-team">
<a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a> <a href="${reverse('manage_users', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Course Team")}</a>
</li> </li>
<li class="nav-item nav-course-settings-advanced"> <li class="nav-item nav-course-settings-advanced">
<a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a> <a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a>
......
...@@ -40,14 +40,12 @@ urlpatterns = ('', # nopep8 ...@@ -40,14 +40,12 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
'contentstore.views.upload_asset', name='upload_asset'), 'contentstore.views.upload_asset', name='upload_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/team/(?P<name>[^/]+)$',
'contentstore.views.manage_users', name='manage_users'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/team/(?P<name>[^/]+)/(?P<email>[^/]+)$',
'contentstore.views.course_team_user', name='course_team_user'),
url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
url(r'^add_user/(?P<location>.*?)$',
'contentstore.views.add_user', name='add_user'),
url(r'^remove_user/(?P<location>.*?)$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$',
'contentstore.views.course_info', name='course_info'), 'contentstore.views.course_info', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$',
......
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
// extends - UI archetypes - well // extends - UI archetypes - well
.ui-well { .ui-well {
box-shadow: inset 0 1px 2px 1px $shadow; box-shadow: inset 0 1px 2px 1px $shadow;
padding: ($baseline*0.75); padding: ($baseline*0.75) $baseline;
} }
// ==================== // ====================
......
...@@ -19,6 +19,8 @@ def find_full_path(path_to_file): ...@@ -19,6 +19,8 @@ def find_full_path(path_to_file):
def main(argv): def main(argv):
parser = argparse.ArgumentParser(description="Run just one test") parser = argparse.ArgumentParser(description="Run just one test")
parser.add_argument('--nocapture', '-s', action='store_true', help="Don't capture stdout (any stdout output will be printed immediately)") parser.add_argument('--nocapture', '-s', action='store_true', help="Don't capture stdout (any stdout output will be printed immediately)")
parser.add_argument('--pdb', action='store_true', help="Use pdb for test errors")
parser.add_argument('--pdb-fail', action='store_true', help="Use pdb for test failures")
parser.add_argument('words', metavar="WORDS", nargs='+', help="The description of a test failure, like 'ERROR: test_set_missing_field (courseware.tests.test_model_data.TestStudentModuleStorage)'") parser.add_argument('words', metavar="WORDS", nargs='+', help="The description of a test failure, like 'ERROR: test_set_missing_field (courseware.tests.test_model_data.TestStudentModuleStorage)'")
args = parser.parse_args(argv) args = parser.parse_args(argv)
...@@ -54,6 +56,10 @@ def main(argv): ...@@ -54,6 +56,10 @@ def main(argv):
django_args = ["./manage.py", system, "--settings", "test", "test"] django_args = ["./manage.py", system, "--settings", "test", "test"]
if args.nocapture: if args.nocapture:
django_args.append("-s") django_args.append("-s")
if args.pdb:
django_args.append("--pdb")
if args.pdb_fail:
django_args.append("--pdb-fail")
django_args.append(test_spec) django_args.append(test_spec)
print " ".join(django_args) print " ".join(django_args)
......
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