[Python-checkins] r88881 - in tracker/instances/python-dev: extensions/jnosy.py html/issue.item.html html/issue.item.js html/style.css html/user.devs.html html/user.experts.html
ezio.melotti
python-checkins at python.org
Sat Aug 13 09:50:30 CEST 2011
Author: ezio.melotti
Date: Sat Aug 13 09:50:30 2011
New Revision: 88881
Log:
#417: add an autocomplete for the nosy list.
Added:
tracker/instances/python-dev/extensions/jnosy.py
tracker/instances/python-dev/html/user.devs.html
tracker/instances/python-dev/html/user.experts.html
Modified:
tracker/instances/python-dev/html/issue.item.html
tracker/instances/python-dev/html/issue.item.js
tracker/instances/python-dev/html/style.css
Added: tracker/instances/python-dev/extensions/jnosy.py
==============================================================================
--- (empty file)
+++ tracker/instances/python-dev/extensions/jnosy.py Sat Aug 13 09:50:30 2011
@@ -0,0 +1,84 @@
+"""
+This module provides two helper functions used by the Javascript autocomplete
+of the nosy list:
+ 1) a simple state machine to parse the tables of the
+ experts index and turn them in a JSON object;
+ 2) a function to get the list of developers as a JSON object;
+"""
+
+import urllib
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+url = 'http://hg.python.org/devguide/raw-file/default/experts.rst'
+
+# possible states
+no_table = 0 # not parsing a table
+table_header = 1 # parsing the header
+table_content = 2 # parsing the content
+table_end = 3 # reached the end of the table
+
+def experts_as_json():
+ """
+ Parse the tables of the experts index and turn them into a JSON object.
+ """
+ data = {}
+ table_state = no_table
+
+ try:
+ page = urllib.urlopen(url)
+ except Exception:
+ # if something goes wrong just return an empty JSON object
+ return '{}'
+
+ for line in page:
+ columns = [column.strip() for column in line.split(' ', 1)]
+ # all the tables have 2 columns (some entries might not have experts,
+ # so we just skip them)
+ if len(columns) != 2:
+ continue
+ first, second = columns
+ # check if we found a table separator
+ if set(first) == set(second) == set('='):
+ table_state += 1
+ if table_state == table_end:
+ table_state = no_table
+ continue
+ if table_state == table_header:
+ # create a dict for the category (e.g. 'Modules', 'Interest areas')
+ category = first
+ data[category] = {}
+ if table_state == table_content:
+ # add to the category dict the entries for that category
+ # (e.g.module names) and the list of experts
+ # if the entry is empty the names belong to the previous entry
+ entry = first or entry
+ names = (name.strip(' *') for name in second.split(','))
+ names = ','.join(name for name in names if '(inactive)' not in name)
+ if not first:
+ data[category][entry] += names
+ else:
+ data[category][entry] = names
+ return json.dumps(data, separators=(',',':'))
+
+
+def devs_as_json(cls):
+ """
+ Generate a JSON object that contains the username and realname of all
+ the committers.
+ """
+ users = []
+ for user in cls.filter(None, {'iscommitter': 1}):
+ username = user.username.plain()
+ realname = user.realname.plain(unchecked=1)
+ if not realname:
+ continue
+ users.append([username, realname])
+ return json.dumps(users, separators=(',',':'))
+
+
+def init(instance):
+ instance.registerUtil('experts_as_json', experts_as_json)
+ instance.registerUtil('devs_as_json', devs_as_json)
Modified: tracker/instances/python-dev/html/issue.item.html
==============================================================================
--- tracker/instances/python-dev/html/issue.item.html (original)
+++ tracker/instances/python-dev/html/issue.item.html Sat Aug 13 09:50:30 2011
@@ -11,8 +11,10 @@
</title>
<metal:slot fill-slot="more-javascript">
-<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.js"></script>
<script type="text/javascript" src="@@file/issue.item.js"></script>
+<link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/smoothness/jquery-ui.css" />
</metal:slot>
<tal:block metal:fill-slot="body_title">
Modified: tracker/instances/python-dev/html/issue.item.js
==============================================================================
--- tracker/instances/python-dev/html/issue.item.js (original)
+++ tracker/instances/python-dev/html/issue.item.js Sat Aug 13 09:50:30 2011
@@ -62,3 +62,173 @@
}
});
})
+
+
+$(document).ready(function() {
+ /* Add an autocomplete to the nosy list that searches the term in two lists:
+ 1) the list of developers (both the user and the real name);
+ 2) the list of experts in the devguide;
+ See also the "categories" and "multiple values" examples at
+ http://jqueryui.com/demos/autocomplete/. */
+
+ if ($("input[name=nosy]").length == 0) {
+ // if we can't find the nosy <input>, the user can't edit the nosy
+ // so there's no need to load the autocomplete
+ return;
+ }
+
+ // create a custom widget to group the entries in categories
+ $.widget("custom.catcomplete", $.ui.autocomplete, {
+ _renderMenu: function(ul, items) {
+ var self = this, current_category = "";
+ // loop through the items, adding a <li> when a new category is
+ // found, and then render the item in the <ul>
+ $.each(items, function(index, item) {
+ if (item.category != current_category) {
+ ul.append("<li class='ui-autocomplete-category'>" + item.category + "</li>");
+ current_category = item.category;
+ }
+ self._renderItem(ul, item);
+ });
+ }
+ });
+
+ function split(val) {
+ return val.split(/\s*,\s*/);
+ }
+ function extract_last(term) {
+ return split(term).pop();
+ }
+ function unix_time() {
+ return Math.floor(new Date().getTime() / 1000);
+ }
+ function is_expired(time_str) {
+ // check if the cached file is older than 1 day
+ return ((unix_time() - parseInt(time_str)) > 24*60*60);
+ }
+
+ // this will be called once we have retrieved the data
+ function add_autocomplete(data) {
+ $("input[name=nosy]")
+ // don't navigate away from the field on tab when selecting an item
+ .bind("keydown", function(event) {
+ if (event.keyCode === $.ui.keyCode.TAB &&
+ $(this).data("autocomplete").menu.active) {
+ event.preventDefault();
+ }
+ })
+ .catcomplete({
+ minLength: 2, // this doesn't seem to work
+ delay: 0,
+ source: function(request, response) {
+ // delegate back to autocomplete, but extract the last term
+ response($.ui.autocomplete.filter(
+ data, extract_last(request.term)));
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function(event, ui) {
+ var usernames = split(this.value);
+ // remove the current input
+ usernames.pop();
+ // add the selected item
+ $.each(split(ui.item.value), function(i, username) {
+ // check if any of the usernames are already there
+ if ($.inArray(username, usernames) == -1)
+ usernames.push(username);
+ });
+ // add placeholder to get the comma at the end
+ usernames.push("");
+ this.value = usernames.join(",") ;
+ return false;
+ }
+ });
+ }
+
+
+ // check if we have HTML5 storage available
+ try {
+ var supports_html5_storage = !!localStorage.getItem;
+ } catch(e) {
+ var supports_html5_storage = false;
+ }
+
+ // this object receives the entries for the devs and experts and
+ // when it has both it calls add_autocomplete
+ var data = {
+ devs: null,
+ experts: null,
+ add: function(data, type) {
+ // type is either 'devs' or 'experts'
+ this[type] = data;
+ if (this.devs && this.experts)
+ add_autocomplete(this.devs.concat(this.experts))
+ }
+ };
+
+ /* Note: instead of using a nested structure like:
+ {"Platform": {"plat1": "name1,name2", "plat2": "name3,name4", ...},
+ "Module": {"mod1": "name1,name2", "mod2": "name3,name4", ...},
+ ...}
+ (i.e. the same format sent by the server), we have to change it and
+ repeat the category for each entry, because the autocomplete wants a
+ flat structure like:
+ [{label: "plat1: name1,name2", value: "name1,name2", category: "Platform"},
+ {label: "plat2: name3,name4", value: "name3,name4", category: "Platform"},
+ {label: "mod1: name1,name2", value: "name1,name2", category: "Module"},
+ {label: "mod2: name3,name4", value: "name3,name4", category: "Module"},
+ ...].
+ Passing a nested structure to ui.autocomplete.filter() and attempt
+ further parsing in _renderMenu doesn't seem to work.
+ */
+ function get_json(file, callback) {
+ // Get the JSON from either the HTML5 storage or the server.
+ // file is either 'devs' or 'experts',
+ // the callback is called once the json is retrieved
+ var json;
+ if (supports_html5_storage &&
+ ((json = localStorage[file]) != null) &&
+ !is_expired(localStorage[file+'time'])) {
+ // if we have HTML5 storage and already cached the JSON, use it
+ callback(JSON.parse(json), file);
+ }
+ else {
+ // if we don't have HTML5 storage or the cache is empty, request
+ // the JSON to the server
+ $.getJSON('user?@template='+file, function(rawdata) {
+ var objects = []; // array of objs with label, value, category
+ if (file == 'devs') {
+ // save devs as 'Name Surname (user.name)'
+ $.each(rawdata, function(index, names) {
+ objects.push({label: names[1] + ' (' + names[0] + ')',
+ value: names[0], category: 'Developer'});
+ });
+ }
+ else {
+ // save experts as e.g. 'modname: user1,user2'
+ $.each(rawdata, function(category, entries) {
+ $.each(entries, function(entry, names) {
+ objects.push({label: entry + ': ' + names,
+ value: names, category: category});
+ });
+ });
+ }
+ // cache the objects if we have HTML5 storage
+ if (supports_html5_storage) {
+ localStorage[file] = JSON.stringify(objects);
+ localStorage[file+'time'] = unix_time();
+ }
+ callback(objects, file);
+ });
+ }
+ }
+
+ // request the JSON. This will get it from the HTML5 storage if it's there
+ // or request it to the server if it's not, The JSON will be passed to the
+ // data object, that will wait to get both the files before calling the
+ // add_autocomplete function.
+ get_json('experts', data.add);
+ get_json('devs', data.add);
+});
Modified: tracker/instances/python-dev/html/style.css
==============================================================================
--- tracker/instances/python-dev/html/style.css (original)
+++ tracker/instances/python-dev/html/style.css Sat Aug 13 09:50:30 2011
@@ -513,3 +513,16 @@
.calendar_display .today {
background-color: #afafaf;
}
+
+.ui-autocomplete-category {
+ font-weight: bold;
+ padding: 0 .2em;
+ line-height: 1.2;
+}
+
+.ui-autocomplete {
+ font-size: 75% !important;
+ max-height: 25em;
+ max-width: 20em;
+ overflow: auto;
+}
Added: tracker/instances/python-dev/html/user.devs.html
==============================================================================
--- (empty file)
+++ tracker/instances/python-dev/html/user.devs.html Sat Aug 13 09:50:30 2011
@@ -0,0 +1,4 @@
+<tal:block tal:condition="context/is_view_ok"
+ content="python:utils.devs_as_json(context)">
+ [["username1","Real Name1"],["username2", "Real Name2"],...]
+</tal:block>
Added: tracker/instances/python-dev/html/user.experts.html
==============================================================================
--- (empty file)
+++ tracker/instances/python-dev/html/user.experts.html Sat Aug 13 09:50:30 2011
@@ -0,0 +1,5 @@
+<tal:block content="python:utils.experts_as_json()">
+{"Platform":{"platname":"name1,name2",...},
+ "Module":{"modname":"name1,name2",...},
+ ...}
+</tal:block>
More information about the Python-checkins
mailing list