Skip to content

Commit c77cbf8

Browse files
committed
Merge branch 'release-0.12.0' into master
2 parents 420081b + 7627684 commit c77cbf8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+8230
-692
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ writeup/
4444
/TAGS
4545
.tags
4646
/kalite/i18n/static/
47+
ghostdriver.log

kalite/control_panel/static/js/control_panel/zone_management.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ $(function () {
1212
}
1313
else if (confirmDelete === facilityName) {
1414
var delete_facility_url = event.target.parentNode.getAttribute("value");
15-
doRequest(delete_facility_url)
15+
var data = {facility_id: null};
16+
// MUST: provide the data argument to make this a POST request
17+
doRequest(delete_facility_url, data)
1618
.success(function() {
1719
window.location.reload();
1820
});
1921
} else {
2022
show_message("warning", gettext("The facility has not been deleted. Did you spell the facility name correctly?"));
2123
}
2224
});
23-
})
25+
});

kalite/facility/api_views.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from annoying.functions import get_object_or_None
55

6+
from django.conf import settings
7+
from django.core.exceptions import PermissionDenied
68
from django.http import HttpResponseForbidden
79
from django.shortcuts import get_object_or_404
810
from django.utils import simplejson
@@ -13,6 +15,9 @@
1315
from kalite.shared.decorators import require_authorized_admin
1416

1517

18+
log = settings.LOG
19+
20+
1621
@require_authorized_admin
1722
@api_response_causes_reload
1823
def move_to_group(request):
@@ -49,6 +54,9 @@ def facility_delete(request, facility_id=None):
4954
if not request.is_django_user:
5055
raise PermissionDenied("Teachers cannot delete facilities.")
5156

57+
if request.method != 'POST':
58+
return JsonResponseMessageError(_("Method is not allowed."))
59+
5260
facility_id = facility_id or simplejson.loads(request.body or "{}").get("facility_id")
5361
fac = get_object_or_404(Facility, id=facility_id)
5462

kalite/facility/templates/facility/facility_user.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
$("#id_group").parent().hide();
3636
{% endif %}
3737

38+
{# Do not allow students to edit their own group #}
39+
{% if not request.is_admin %}
40+
$("#id_group").hide();
41+
$('#id_group').siblings('.helptext').hide()
42+
var group_name = $("#id_group option:selected").text()
43+
if (group_name === "---------") {
44+
group_name = "{% trans 'None' %}"
45+
}
46+
$(sprintf("<span>%s</span>", group_name)).insertAfter($("#id_group"));
47+
{% endif %}
48+
3849
{# Show facility info #}
3950
{% if request.session.facility_count == 1 %}
4051
// Show the dropdown

kalite/facility/tests/form_tests.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from .base import FacilityTestCase
1414
from ..forms import FacilityUserForm, FacilityForm, FacilityGroupForm, LoginForm
1515
from ..models import Facility, FacilityUser, FacilityGroup
16+
from kalite.distributed.tests.browser_tests.base import KALiteDistributedBrowserTestCase
1617
from kalite.testing import KALiteTestCase
18+
from kalite.testing.mixins.facility_mixins import FacilityMixins
1719
from securesync.models import Zone, Device, DeviceMetadata
1820

1921

@@ -202,3 +204,49 @@ def test_form_duplicate_name_list(self):
202204
# Fails for a second; userlist if admin
203205
user_form = FacilityUserForm(facility=self.facility, data=self.data)
204206
self.assertFalse(user_form.is_valid(), "Form must NOT be valid.")
207+
208+
209+
class FormBrowserTests(FacilityMixins, KALiteDistributedBrowserTestCase):
210+
211+
def setUp(self):
212+
self.facility = self.create_facility()
213+
super(FormBrowserTests, self).setUp()
214+
215+
def test_no_groups_no_select(self):
216+
signup_url = "%s%s%s" % (self.reverse('facility_user_signup'), "?&facility=", self.facility.id)
217+
self.browse_to(signup_url)
218+
group_label = self.browser.find_element_by_xpath("//label[@for='id_group']")
219+
self.assertFalse(group_label.is_displayed())
220+
group_select = self.browser.find_element_by_id('id_group')
221+
self.assertFalse(group_select.is_displayed())
222+
223+
def test_signup_cannot_select_group(self):
224+
self.group = self.create_group(facility=self.facility)
225+
signup_url = "%s%s%s" % (self.reverse('facility_user_signup'), "?&facility=", self.facility.id)
226+
self.browse_to(signup_url)
227+
group_label = self.browser.find_element_by_xpath("//label[@for='id_group']")
228+
self.assertTrue(group_label.is_displayed())
229+
group_select = self.browser.find_element_by_id('id_group')
230+
self.assertFalse(group_select.is_displayed())
231+
232+
def test_logged_in_student_cannot_select_group(self):
233+
self.group = self.create_group(facility=self.facility)
234+
self.student = self.create_student(facility=self.facility, group=self.group)
235+
self.browser_login_student(username=self.student.username, password='password', facility_name=self.facility.name)
236+
self.browse_to(self.reverse('edit_facility_user', kwargs={'facility_user_id': self.student.id}))
237+
group_label = self.browser.find_element_by_xpath("//label[@for='id_group']")
238+
self.assertTrue(group_label.is_displayed())
239+
group_select = self.browser.find_element_by_id('id_group')
240+
self.assertFalse(group_select.is_displayed())
241+
242+
def test_teacher_can_select_group(self):
243+
self.group = self.create_group(facility=self.facility)
244+
self.student = self.create_student(facility=self.facility, group=self.group)
245+
self.teacher = self.create_teacher(facility=self.facility)
246+
self.browser_login_teacher(username=self.teacher.username, password='password', facility_name=self.facility.name)
247+
self.browse_to(self.reverse('edit_facility_user', kwargs={'facility_user_id': self.student.id}))
248+
group_label = self.browser.find_element_by_xpath("//label[@for='id_group']")
249+
self.assertTrue(group_label.is_displayed())
250+
group_select = self.browser.find_element_by_id('id_group')
251+
self.assertTrue(group_select.is_displayed())
252+

kalite/testing/browser.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,51 @@ def setup_test_env(browser_type="Firefox", test_user="testadmin", test_password=
3737
else:
3838
local_browser = browser
3939

40+
hacks_for_phantomjs(local_browser)
41+
4042
return (local_browser, admin_user, test_password)
4143

4244

45+
def hacks_for_phantomjs(browser):
46+
"""
47+
HACK: If using PhantomJS, override the window.alert()/confirm()/prompt() functions to return true because
48+
the GhostDriver does not support modal dialogs (alert, confirm, prompt).
49+
50+
What we do is override the alert/confirm/prompt functions so any call that expects the dialog with return true.
51+
52+
REF: http://stackoverflow.com/questions/15708518/how-can-i-handle-an-alert-with-ghostdriver-via-python
53+
REF: https://groups.google.com/forum/#!topic/phantomjs/w_rKkFJ0g8w
54+
REF: http://stackoverflow.com/questions/13536752/phantomjs-click-a-link-on-a-page?rq=1
55+
"""
56+
if isinstance(browser, webdriver.PhantomJS):
57+
js = """
58+
window.confirm = function(message) {
59+
return true;
60+
}
61+
window.alert = window.prompt = window.confirm;
62+
63+
// REF: http://stackoverflow.com/questions/13536752/phantomjs-click-a-link-on-a-page?rq=1
64+
// REF: http://stackoverflow.com/questions/2705583/how-to-simulate-a-click-with-javascript/2706236#2706236
65+
window.eventFire = function(el, etype) {
66+
if (el.fireEvent) {
67+
el.fireEvent('on' + etype);
68+
} else {
69+
var evObj = document.createEvent('Events');
70+
evObj.initEvent(etype, true, false);
71+
el.dispatchEvent(evObj);
72+
}
73+
};
74+
75+
// shorter alternative of above method
76+
window.simulateClick = function(el) {
77+
var e = document.createEvent('MouseEvents');
78+
e.initEvent( 'click', true, true );
79+
el.dispatchEvent(e);
80+
};
81+
"""
82+
browser.execute_script("%s" % js)
83+
84+
4385
def browse_to(browser, dest_url, wait_time=0.1, max_retries=50):
4486
"""Given a selenium browser, open the given url and wait until the browser has completed."""
4587
if dest_url == browser.current_url:

kalite/version.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33
VERSION = "0.12.0"
44
VERSION_INFO = {
55

6+
"0.12.7": {
7+
"release_date": "2014/10/08",
8+
"git_commit": "75591f",
9+
"new_features": {
10+
"all": [],
11+
"students": [],
12+
"coaches": [],
13+
"admins": [],
14+
},
15+
"bugs_fixed": {
16+
"all": ["fix to handle multiple psutil versions", "fix to handle multiple psutil versions"],
17+
"students": [],
18+
"coaches": [],
19+
"admins": [],
20+
},
21+
22+
},
23+
624
"0.12.6": {
725
"release_date": "2014/09/08",
826
"git_commit": "9cf2f05",

python-packages/fle_utils/set_process_priority.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,16 @@ def _set_linux_mac_priority(priority, logging=logging):
5555
return False
5656

5757
this_process = psutil.Process(os.getpid())
58-
if "runcherrypyserver" in this_process.cmdline():
58+
# Psutil is builtin to some python installations, and the versions
59+
# may differ across devices. It affects the code below, b/c for the
60+
# 2.x psutil version. 'this_process.cmdline is a function that
61+
# returns a list; in the 1.x version it's just a list.
62+
# So we check what kind of cmdline we have, and go from there.
63+
if isinstance(this_process.cmdline, list):
64+
cmdline = this_process.cmdline
65+
else:
66+
cmdline = this_process.cmdline()
67+
if "runcherrypyserver" in cmdline:
5968
logging.debug("Will not set priority, this is the webserver process")
6069
return False
6170

python-packages/mimeparse.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""MIME-Type Parser
2+
3+
This module provides basic functions for handling mime-types. It can handle
4+
matching mime-types against a list of media-ranges. See section 14.1 of the
5+
HTTP specification [RFC 2616] for a complete explanation.
6+
7+
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
8+
9+
Contents:
10+
- parse_mime_type(): Parses a mime-type into its component parts.
11+
- parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q'
12+
quality parameter.
13+
- quality(): Determines the quality ('q') of a mime-type when
14+
compared against a list of media-ranges.
15+
- quality_parsed(): Just like quality() except the second parameter must be
16+
pre-parsed.
17+
- best_match(): Choose the mime-type with the highest quality ('q')
18+
from a list of candidates.
19+
"""
20+
from functools import reduce
21+
22+
__version__ = '0.1.4'
23+
__author__ = 'Joe Gregorio'
24+
__email__ = 'joe@bitworking.org'
25+
__license__ = 'MIT License'
26+
__credits__ = ''
27+
28+
29+
def parse_mime_type(mime_type):
30+
"""Parses a mime-type into its component parts.
31+
32+
Carves up a mime-type and returns a tuple of the (type, subtype, params)
33+
where 'params' is a dictionary of all the parameters for the media range.
34+
For example, the media range 'application/xhtml;q=0.5' would get parsed
35+
into:
36+
37+
('application', 'xhtml', {'q', '0.5'})
38+
"""
39+
parts = mime_type.split(';')
40+
params = dict([tuple([s.strip() for s in param.split('=', 1)])
41+
for param in parts[1:]
42+
])
43+
full_type = parts[0].strip()
44+
# Java URLConnection class sends an Accept header that includes a
45+
# single '*'. Turn it into a legal wildcard.
46+
if full_type == '*':
47+
full_type = '*/*'
48+
(type, subtype) = full_type.split('/')
49+
50+
return (type.strip(), subtype.strip(), params)
51+
52+
53+
def parse_media_range(range):
54+
"""Parse a media-range into its component parts.
55+
56+
Carves up a media range and returns a tuple of the (type, subtype,
57+
params) where 'params' is a dictionary of all the parameters for the media
58+
range. For example, the media range 'application/*;q=0.5' would get parsed
59+
into:
60+
61+
('application', '*', {'q', '0.5'})
62+
63+
In addition this function also guarantees that there is a value for 'q'
64+
in the params dictionary, filling it in with a proper default if
65+
necessary.
66+
"""
67+
(type, subtype, params) = parse_mime_type(range)
68+
if not 'q' in params or not params['q'] or \
69+
not float(params['q']) or float(params['q']) > 1\
70+
or float(params['q']) < 0:
71+
params['q'] = '1'
72+
73+
return (type, subtype, params)
74+
75+
76+
def fitness_and_quality_parsed(mime_type, parsed_ranges):
77+
"""Find the best match for a mime-type amongst parsed media-ranges.
78+
79+
Find the best match for a given mime-type against a list of media_ranges
80+
that have already been parsed by parse_media_range(). Returns a tuple of
81+
the fitness value and the value of the 'q' quality parameter of the best
82+
match, or (-1, 0) if no match was found. Just as for quality_parsed(),
83+
'parsed_ranges' must be a list of parsed media ranges.
84+
"""
85+
best_fitness = -1
86+
best_fit_q = 0
87+
(target_type, target_subtype, target_params) =\
88+
parse_media_range(mime_type)
89+
for (type, subtype, params) in parsed_ranges:
90+
type_match = (type == target_type or
91+
type == '*' or
92+
target_type == '*')
93+
subtype_match = (subtype == target_subtype or
94+
subtype == '*' or
95+
target_subtype == '*')
96+
if type_match and subtype_match:
97+
param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in
98+
list(target_params.items()) if key != 'q' and
99+
key in params and value == params[key]], 0)
100+
fitness = (type == target_type) and 100 or 0
101+
fitness += (subtype == target_subtype) and 10 or 0
102+
fitness += param_matches
103+
if fitness > best_fitness:
104+
best_fitness = fitness
105+
best_fit_q = params['q']
106+
107+
return best_fitness, float(best_fit_q)
108+
109+
110+
def quality_parsed(mime_type, parsed_ranges):
111+
"""Find the best match for a mime-type amongst parsed media-ranges.
112+
113+
Find the best match for a given mime-type against a list of media_ranges
114+
that have already been parsed by parse_media_range(). Returns the 'q'
115+
quality parameter of the best match, 0 if no match was found. This function
116+
bahaves the same as quality() except that 'parsed_ranges' must be a list of
117+
parsed media ranges. """
118+
119+
return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
120+
121+
122+
def quality(mime_type, ranges):
123+
"""Return the quality ('q') of a mime-type against a list of media-ranges.
124+
125+
Returns the quality 'q' of a mime-type when compared against the
126+
media-ranges in ranges. For example:
127+
128+
>>> quality('text/html','text/*;q=0.3, text/html;q=0.7,
129+
text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
130+
0.7
131+
132+
"""
133+
parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
134+
135+
return quality_parsed(mime_type, parsed_ranges)
136+
137+
138+
def best_match(supported, header):
139+
"""Return mime-type with the highest quality ('q') from list of candidates.
140+
141+
Takes a list of supported mime-types and finds the best match for all the
142+
media-ranges listed in header. The value of header must be a string that
143+
conforms to the format of the HTTP Accept: header. The value of 'supported'
144+
is a list of mime-types. The list of supported mime-types should be sorted
145+
in order of increasing desirability, in case of a situation where there is
146+
a tie.
147+
148+
>>> best_match(['application/xbel+xml', 'text/xml'],
149+
'text/*;q=0.5,*/*; q=0.1')
150+
'text/xml'
151+
"""
152+
split_header = _filter_blank(header.split(','))
153+
parsed_header = [parse_media_range(r) for r in split_header]
154+
weighted_matches = []
155+
pos = 0
156+
for mime_type in supported:
157+
weighted_matches.append((fitness_and_quality_parsed(mime_type,
158+
parsed_header), pos, mime_type))
159+
pos += 1
160+
weighted_matches.sort()
161+
162+
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ''
163+
164+
165+
def _filter_blank(i):
166+
for s in i:
167+
if s.strip():
168+
yield s

0 commit comments

Comments
 (0)