Compare commits
39 Commits
main
...
stable/0.9
Author | SHA1 | Date |
---|---|---|
James Bennett | aa5133500b | |
James Bennett | 7d3b409311 | |
James Bennett | 30d4c16ade | |
Jacob Kaplan-Moss | 44debfeaa4 | |
James Bennett | f6c56d473f | |
James Bennett | 6e657e2c40 | |
Jacob Kaplan-Moss | 2c03839d79 | |
Jacob Kaplan-Moss | 8bc36e726c | |
James Bennett | 613cfabdd0 | |
James Bennett | 9cd09e7562 | |
James Bennett | 56c074f565 | |
James Bennett | d797b34bf8 | |
James Bennett | 0417b0f17b | |
James Bennett | e6f09afa78 | |
James Bennett | 17e98808c1 | |
James Bennett | 1680e8709c | |
James Bennett | f63faca4c6 | |
James Bennett | 32b733ccbd | |
James Bennett | d8b1717266 | |
James Bennett | 18cf40006c | |
James Bennett | f46f052e05 | |
James Bennett | 8a0fa75839 | |
James Bennett | 02ca9fd1de | |
James Bennett | d0672a7344 | |
James Bennett | f931905517 | |
James Bennett | 2823775114 | |
James Bennett | 1fcef7a07e | |
James Bennett | 3720a12a2a | |
James Bennett | c08f81ff5e | |
James Bennett | b457108751 | |
James Bennett | e35be34c21 | |
James Bennett | 4b97347921 | |
James Bennett | db2e70a74b | |
James Bennett | d31e39173c | |
James Bennett | 0d60669146 | |
James Bennett | b70c86c36d | |
James Bennett | 06a3c04c87 | |
Adrian Holovaty | 5623203d15 | |
Adrian Holovaty | 9a2f6a542a |
60
README
60
README
|
@ -1,37 +1,49 @@
|
|||
Django is a high-level Python Web framework that encourages rapid development
|
||||
and clean, pragmatic design.
|
||||
Django is a high-level Python Web framework that encourages rapid
|
||||
development and clean, pragmatic design.
|
||||
|
||||
All documentation is in the "docs" directory and online at
|
||||
http://www.djangoproject.com/documentation/. If you're just getting started,
|
||||
here's how we recommend you read the docs:
|
||||
|
||||
* First, read docs/install.txt for instructions on installing Django.
|
||||
About this version
|
||||
==================
|
||||
|
||||
* Next, work through the tutorials in order (docs/tutorial01.txt,
|
||||
docs/tutorial02.txt, etc.).
|
||||
This is the Django 0.91 "bugfixes" branch, which is intended to
|
||||
provide bugfix and patch support for users of Django 0.91 who have not
|
||||
been able to migrate to a more recent version. No new features will be
|
||||
added in this branch, and it is maintained solely as a means of
|
||||
providing support to legacy Django installations.
|
||||
|
||||
* If you want to set up an actual deployment server, read docs/modpython.txt
|
||||
for instructions on running Django under mod_python.
|
||||
If you're completely new to Django we highly recommend that you use
|
||||
either the latest stable release or a Subversion checkout from
|
||||
Django's trunk; Django is always evolving, and the latest and greatest
|
||||
features are only available to users of newer versions of the
|
||||
framework.
|
||||
|
||||
* The rest of the documentation is of the reference-manual variety.
|
||||
Read it -- and the FAQ -- as you run into problems.
|
||||
|
||||
Docs are updated rigorously. If you find any problems in the docs, or think they
|
||||
should be clarified in any way, please take 30 seconds to fill out a ticket
|
||||
here:
|
||||
More information
|
||||
================
|
||||
|
||||
http://code.djangoproject.com/newticket
|
||||
The complete history of bugs fixed in this branch can be viewed online
|
||||
at http://code.djangoproject.com/log/django/branches/0.91-bugfixes.
|
||||
|
||||
To get more help:
|
||||
We also recommend that users of this branch subscribe to the
|
||||
"django-announce" mailing list, a low-traffic, announcements-only list
|
||||
which will send messages whenever an important (i.e.,
|
||||
security-related) bug is fixed. You can subscribe to the list via
|
||||
Google Groups at http://groups.google.com/group/django-announce.
|
||||
|
||||
* Join the #django channel on irc.freenode.net. Lots of helpful people
|
||||
hang out there. Read the archives at http://loglibrary.com/179 .
|
||||
The documentation for this version of Django has been frozen, and is
|
||||
available online at http://www.djangoproject.com/documentation/0_91/.
|
||||
|
||||
* Join the django-users mailing list, or read the archives, at
|
||||
http://groups-beta.google.com/group/django-users.
|
||||
|
||||
To contribute to Django:
|
||||
Submitting bugs
|
||||
===============
|
||||
|
||||
* Check out http://www.djangoproject.com/community/ for information
|
||||
about getting involved.
|
||||
If you run into a bug in Django 0.91, please search the Django ticket
|
||||
database to see if the issue has already been reported; if not, please
|
||||
head over to http://code.djangoproject.com/newticket and file a new
|
||||
ticket with as much information about the bug as you can provide.
|
||||
|
||||
If you're running into a bug which has been reported but not fixed,
|
||||
feel free to update the ticket with any additional information you
|
||||
have, and to assign it to 'ubernostrum' (AKA James Bennett, the
|
||||
maintainer of this branch).
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
VERSION = (0, 9, 1, 'SVN')
|
||||
VERSION = (0, 91, 3, 'SVN')
|
||||
|
|
|
@ -20,7 +20,14 @@ def compile_messages():
|
|||
if f.endswith('.po'):
|
||||
sys.stderr.write('processing file %s in %s\n' % (f, dirpath))
|
||||
pf = os.path.splitext(os.path.join(dirpath, f))[0]
|
||||
cmd = 'msgfmt -o "%s.mo" "%s.po"' % (pf, pf)
|
||||
# Store the names of the .mo and .po files in an environment
|
||||
# variable, rather than doing a string replacement into the
|
||||
# command, so that we can take advantage of shell quoting, to
|
||||
# quote any malicious characters/escaping.
|
||||
# See http://cyberelk.net/tim/articles/cmdline/ar01s02.html
|
||||
os.environ['djangocompilemo'] = pf + '.mo'
|
||||
os.environ['djangocompilepo'] = pf + '.po'
|
||||
cmd = 'msgfmt -o "$djangocompilemo" "$djangocompilepo"'
|
||||
os.system(cmd)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,8 +1,35 @@
|
|||
// Handles related-objects functionality: lookup link for raw_id_admin=True
|
||||
// and Add Another links.
|
||||
|
||||
function html_unescape(text) {
|
||||
// Unescape a string that was escaped using django.utils.html.escape.
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/"/g, '"');
|
||||
text = text.replace(/'/g, "'");
|
||||
text = text.replace(/&/g, '&');
|
||||
return text;
|
||||
}
|
||||
|
||||
// IE doesn't accept periods or dashes in the window name, but the element IDs
|
||||
// we use to generate popup window names may contain them, therefore we map them
|
||||
// to allowed characters in a reversible way so that we can locate the correct
|
||||
// element when the popup window is dismissed.
|
||||
function id_to_windowname(text) {
|
||||
text = text.replace(/\./g, '__dot__');
|
||||
text = text.replace(/\-/g, '__dash__');
|
||||
return text;
|
||||
}
|
||||
|
||||
function windowname_to_id(text) {
|
||||
text = text.replace(/__dot__/g, '.');
|
||||
text = text.replace(/__dash__/g, '-');
|
||||
return text;
|
||||
}
|
||||
|
||||
function showRelatedObjectLookupPopup(triggeringLink) {
|
||||
var name = triggeringLink.id.replace(/^lookup_/, '');
|
||||
name = id_to_windowname(name);
|
||||
var href;
|
||||
if (triggeringLink.href.search(/\?/) >= 0) {
|
||||
href = triggeringLink.href + '&pop=1';
|
||||
|
@ -15,25 +42,36 @@ function showRelatedObjectLookupPopup(triggeringLink) {
|
|||
}
|
||||
|
||||
function dismissRelatedLookupPopup(win, chosenId) {
|
||||
var elem = document.getElementById(win.name);
|
||||
var name = windowname_to_id(win.name);
|
||||
var elem = document.getElementById(name);
|
||||
if (elem.className.indexOf('vRawIdAdminField') != -1 && elem.value) {
|
||||
elem.value += ',' + chosenId;
|
||||
} else {
|
||||
document.getElementById(win.name).value = chosenId;
|
||||
document.getElementById(name).value = chosenId;
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function showAddAnotherPopup(triggeringLink) {
|
||||
var name = triggeringLink.id.replace(/^add_/, '');
|
||||
name = name.replace(/\./g, '___');
|
||||
var win = window.open(triggeringLink.href + '?_popup=1', name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
name = id_to_windowname(name);
|
||||
href = triggeringLink.href
|
||||
if (href.indexOf('?') == -1) {
|
||||
href += '?_popup=1';
|
||||
} else {
|
||||
href += '&_popup=1';
|
||||
}
|
||||
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
function dismissAddAnotherPopup(win, newId, newRepr) {
|
||||
var name = win.name.replace(/___/g, '.');
|
||||
// newId and newRepr are expected to have previously been escaped by
|
||||
// django.utils.html.escape.
|
||||
newId = html_unescape(newId);
|
||||
newRepr = html_unescape(newRepr);
|
||||
var name = windowname_to_id(win.name);
|
||||
var elem = document.getElementById(name);
|
||||
if (elem) {
|
||||
if (elem.nodeName == 'SELECT') {
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<p class="aligned">
|
||||
<label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" />
|
||||
<input type="hidden" name="this_is_the_login_form" value="1" />
|
||||
<input type="hidden" name="post_data" value="{{ post_data }}" />{% comment %} <span class="help">{% trans 'Have you <a href="/password_reset/">forgotten your password</a>?' %}</span>{% endcomment %}
|
||||
</p>
|
||||
|
||||
<div class="aligned ">
|
||||
|
|
|
@ -2,43 +2,21 @@ from django.core.extensions import DjangoContext, render_to_response
|
|||
from django.conf.settings import SECRET_KEY
|
||||
from django.models.auth import users
|
||||
from django.utils import httpwrappers
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy
|
||||
import base64, datetime, md5
|
||||
import cPickle as pickle
|
||||
import base64, datetime
|
||||
|
||||
ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
|
||||
LOGIN_FORM_KEY = 'this_is_the_login_form'
|
||||
|
||||
def _display_login_form(request, error_message=''):
|
||||
request.session.set_test_cookie()
|
||||
if request.POST and request.POST.has_key('post_data'):
|
||||
# User has failed login BUT has previously saved post data.
|
||||
post_data = request.POST['post_data']
|
||||
elif request.POST:
|
||||
# User's session must have expired; save their post data.
|
||||
post_data = _encode_post_data(request.POST)
|
||||
else:
|
||||
post_data = _encode_post_data({})
|
||||
return render_to_response('admin/login', {
|
||||
'title': _('Log in'),
|
||||
'app_path': request.path,
|
||||
'post_data': post_data,
|
||||
'app_path': escape(request.path),
|
||||
'error_message': error_message
|
||||
}, context_instance=DjangoContext(request))
|
||||
|
||||
def _encode_post_data(post_data):
|
||||
pickled = pickle.dumps(post_data)
|
||||
pickled_md5 = md5.new(pickled + SECRET_KEY).hexdigest()
|
||||
return base64.encodestring(pickled + pickled_md5)
|
||||
|
||||
def _decode_post_data(encoded_data):
|
||||
encoded_data = base64.decodestring(encoded_data)
|
||||
pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
|
||||
if md5.new(pickled + SECRET_KEY).hexdigest() != tamper_check:
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
raise SuspiciousOperation, "User may have tampered with session cookie."
|
||||
return pickle.loads(pickled)
|
||||
|
||||
def staff_member_required(view_func):
|
||||
"""
|
||||
Decorator for views that checks that the user is logged in and is a staff
|
||||
|
@ -47,10 +25,6 @@ def staff_member_required(view_func):
|
|||
def _checklogin(request, *args, **kwargs):
|
||||
if not request.user.is_anonymous() and request.user.is_staff:
|
||||
# The user is valid. Continue to the admin page.
|
||||
if request.POST.has_key('post_data'):
|
||||
# User must have re-authenticated through a different window
|
||||
# or tab.
|
||||
request.POST = _decode_post_data(request.POST['post_data'])
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.middleware.sessions.SessionMiddleware'."
|
||||
|
@ -58,7 +32,7 @@ def staff_member_required(view_func):
|
|||
# If this isn't already the login page, display it.
|
||||
if not request.POST.has_key(LOGIN_FORM_KEY):
|
||||
if request.POST:
|
||||
message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.")
|
||||
message = _("Please log in again, because your session has expired.")
|
||||
else:
|
||||
message = ""
|
||||
return _display_login_form(request, message)
|
||||
|
@ -90,16 +64,7 @@ def staff_member_required(view_func):
|
|||
request.session[users.SESSION_KEY] = user.id
|
||||
user.last_login = datetime.datetime.now()
|
||||
user.save()
|
||||
if request.POST.has_key('post_data'):
|
||||
post_data = _decode_post_data(request.POST['post_data'])
|
||||
if post_data and not post_data.has_key(LOGIN_FORM_KEY):
|
||||
# overwrite request.POST with the saved post_data, and continue
|
||||
request.POST = post_data
|
||||
request.user = user
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
request.session.delete_test_cookie()
|
||||
return httpwrappers.HttpResponseRedirect(request.path)
|
||||
return httpwrappers.HttpResponseRedirect(request.path)
|
||||
else:
|
||||
return _display_login_form(request, ERROR_MESSAGE)
|
||||
|
||||
|
|
|
@ -97,8 +97,16 @@ class ChangeList(object):
|
|||
self.mod, self.opts = _get_mod_opts(app_label, module_name)
|
||||
if not request.user.has_perm(app_label + '.' + self.opts.get_change_permission()):
|
||||
raise PermissionDenied
|
||||
|
||||
self.lookup_mod, self.lookup_opts = self.mod, self.opts
|
||||
|
||||
lookup_mod, lookup_opts = self.mod, self.opts
|
||||
if self.opts.one_to_one_field:
|
||||
lookup_mod = self.opts.one_to_one_field.rel.to.get_model_module()
|
||||
lookup_opts = lookup_mod.Klass._meta
|
||||
# If lookup_opts doesn't have admin set, give it the default meta.Admin().
|
||||
if not lookup_opts.admin:
|
||||
lookup_opts.admin = meta.Admin()
|
||||
|
||||
self.lookup_mod, self.lookup_opts = lookup_mod, lookup_opts
|
||||
|
||||
def get_search_parameters(self, request):
|
||||
# Get search parameters from the query string.
|
||||
|
|
|
@ -43,6 +43,6 @@ class LatestCommentsFeed(LatestFreeCommentsFeed):
|
|||
kwargs = LatestFreeCommentsFeed._get_lookup_kwargs(self)
|
||||
kwargs['is_removed__exact'] = False
|
||||
if settings.COMMENTS_BANNED_USERS_GROUP:
|
||||
kwargs['where'] = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)']
|
||||
kwargs['params'] = [COMMENTS_BANNED_USERS_GROUP]
|
||||
return kwargs
|
||||
kwargs['where'] = ['user_id NOT IN (SELECT user_id FROM auth_users_groups WHERE group_id = %s)']
|
||||
kwargs['params'] = [settings.COMMENTS_BANNED_USERS_GROUP]
|
||||
return kwargs
|
||||
|
|
|
@ -107,7 +107,7 @@ class PublicCommentManipulator(AuthenticationForm):
|
|||
# send the comment to the managers.
|
||||
if self.user_cache.get_comments_comment_count() <= COMMENTS_FIRST_FEW:
|
||||
message = ngettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s',
|
||||
'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s') % \
|
||||
'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', COMMENTS_FIRST_FEW) % \
|
||||
{'count': COMMENTS_FIRST_FEW, 'text': c.get_as_text()}
|
||||
mail_managers("Comment posted by rookie user", message)
|
||||
if COMMENTS_SKETCHY_USERS_GROUP and COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.get_group_list()]:
|
||||
|
|
|
@ -16,13 +16,54 @@ except ImportError:
|
|||
# Import copy of _thread_local.py from python 2.4
|
||||
from django.utils._threading_local import local
|
||||
|
||||
def smart_basestring(s, charset):
|
||||
if isinstance(s, unicode):
|
||||
return s.encode(charset)
|
||||
return s
|
||||
|
||||
class UnicodeCursorWrapper(object):
|
||||
"""
|
||||
A thin wrapper around psycopg cursors that allows them to accept Unicode
|
||||
strings as params.
|
||||
|
||||
This is necessary because psycopg doesn't apply any DB quoting to
|
||||
parameters that are Unicode strings. If a param is Unicode, this will
|
||||
convert it to a bytestring using DEFAULT_CHARSET before passing it to
|
||||
psycopg.
|
||||
"""
|
||||
def __init__(self, cursor, charset):
|
||||
self.cursor = cursor
|
||||
self.charset = charset
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
try:
|
||||
params = dict([(k, smart_basestring(v, self.charset)) for (k, v) in params.items()])
|
||||
except AttributeError:
|
||||
params = [smart_basestring(p, self.charset) for p in params]
|
||||
return self.cursor.execute(sql, params)
|
||||
|
||||
def executemany(self, sql, param_list):
|
||||
try:
|
||||
new_param_list = [dict([(k, smart_basestring(v, self.charset)) for (k, v) in params.items()])
|
||||
for params in param_list]
|
||||
except AttributeError:
|
||||
new_param_list = [tuple([smart_basestring(p, self.charset) for p in params])
|
||||
for params in param_list]
|
||||
return self.cursor.executemany(sql, new_param_list)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if self.__dict__.has_key(attr):
|
||||
return self.__dict__[attr]
|
||||
else:
|
||||
return getattr(self.cursor, attr)
|
||||
|
||||
class DatabaseWrapper(local):
|
||||
def __init__(self):
|
||||
self.connection = None
|
||||
self.queries = []
|
||||
|
||||
def cursor(self):
|
||||
from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE
|
||||
from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, DEFAULT_CHARSET, TIME_ZONE
|
||||
if self.connection is None:
|
||||
if DATABASE_NAME == '':
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -40,6 +81,7 @@ class DatabaseWrapper(local):
|
|||
self.connection.set_isolation_level(1) # make transactions transparent to all cursors
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute("SET TIME ZONE %s", [TIME_ZONE])
|
||||
cursor = UnicodeCursorWrapper(cursor, DEFAULT_CHARSET)
|
||||
if DEBUG:
|
||||
return base.CursorDebugWrapper(cursor, self)
|
||||
return cursor
|
||||
|
|
|
@ -325,7 +325,8 @@ class FormField:
|
|||
|
||||
class TextField(FormField):
|
||||
input_type = "text"
|
||||
def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[], member_name=None):
|
||||
def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None, member_name=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name = field_name
|
||||
self.length, self.maxlength = length, maxlength
|
||||
self.is_required = is_required
|
||||
|
@ -362,7 +363,8 @@ class PasswordField(TextField):
|
|||
input_type = "password"
|
||||
|
||||
class LargeTextField(TextField):
|
||||
def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=[], maxlength=None):
|
||||
def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, maxlength=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name = field_name
|
||||
self.rows, self.cols, self.is_required = rows, cols, is_required
|
||||
self.validator_list = validator_list[:]
|
||||
|
@ -380,7 +382,8 @@ class LargeTextField(TextField):
|
|||
self.field_name, self.rows, self.cols, escape(data))
|
||||
|
||||
class HiddenField(FormField):
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name, self.is_required = field_name, is_required
|
||||
self.validator_list = validator_list[:]
|
||||
|
||||
|
@ -410,7 +413,8 @@ class CheckboxField(FormField):
|
|||
html2python = staticmethod(html2python)
|
||||
|
||||
class SelectField(FormField):
|
||||
def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[], member_name=None):
|
||||
def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=None, member_name=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name = field_name
|
||||
# choices is a list of (value, human-readable key) tuples because order matters
|
||||
self.choices, self.size, self.is_required = choices, size, is_required
|
||||
|
@ -446,7 +450,8 @@ class NullSelectField(SelectField):
|
|||
html2python = staticmethod(html2python)
|
||||
|
||||
class RadioSelectField(FormField):
|
||||
def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=[], member_name=None):
|
||||
def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=None, member_name=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name = field_name
|
||||
# choices is a list of (value, human-readable key) tuples because order matters
|
||||
self.choices, self.is_required = choices, is_required
|
||||
|
@ -510,7 +515,8 @@ class RadioSelectField(FormField):
|
|||
|
||||
class NullBooleanField(SelectField):
|
||||
"This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None"
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')],
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
||||
|
@ -563,7 +569,8 @@ class CheckboxSelectMultipleField(SelectMultipleField):
|
|||
back into the single list that validators, renderers and save() expect.
|
||||
"""
|
||||
requires_data_list = True
|
||||
def __init__(self, field_name, choices=[], validator_list=[]):
|
||||
def __init__(self, field_name, choices=[], validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list)
|
||||
|
||||
def prepare(self, new_data):
|
||||
|
@ -594,7 +601,8 @@ class CheckboxSelectMultipleField(SelectMultipleField):
|
|||
####################
|
||||
|
||||
class FileUploadField(FormField):
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name, self.is_required = field_name, is_required
|
||||
self.validator_list = [self.isNonEmptyFile] + validator_list
|
||||
|
||||
|
@ -629,7 +637,8 @@ class ImageUploadField(FileUploadField):
|
|||
####################
|
||||
|
||||
class IntegerField(TextField):
|
||||
def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[], member_name=None):
|
||||
def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None, member_name=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isInteger] + validator_list
|
||||
if member_name is not None:
|
||||
self.member_name = member_name
|
||||
|
@ -648,7 +657,8 @@ class IntegerField(TextField):
|
|||
html2python = staticmethod(html2python)
|
||||
|
||||
class SmallIntegerField(IntegerField):
|
||||
def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isSmallInteger] + validator_list
|
||||
IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
|
||||
|
||||
|
@ -657,7 +667,8 @@ class SmallIntegerField(IntegerField):
|
|||
raise validators.CriticalValidationError, _("Enter a whole number between -32,768 and 32,767.")
|
||||
|
||||
class PositiveIntegerField(IntegerField):
|
||||
def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isPositive] + validator_list
|
||||
IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
|
||||
|
||||
|
@ -666,7 +677,8 @@ class PositiveIntegerField(IntegerField):
|
|||
raise validators.CriticalValidationError, _("Enter a positive number.")
|
||||
|
||||
class PositiveSmallIntegerField(IntegerField):
|
||||
def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isPositiveSmall] + validator_list
|
||||
IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
|
||||
|
||||
|
@ -675,7 +687,8 @@ class PositiveSmallIntegerField(IntegerField):
|
|||
raise validators.CriticalValidationError, _("Enter a whole number between 0 and 32,767.")
|
||||
|
||||
class FloatField(TextField):
|
||||
def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
validator_list = [self.isValidFloat] + validator_list
|
||||
TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list)
|
||||
|
@ -700,7 +713,8 @@ class FloatField(TextField):
|
|||
class DatetimeField(TextField):
|
||||
"""A FormField that automatically converts its data to a datetime.datetime object.
|
||||
The data should be in the format YYYY-MM-DD HH:MM:SS."""
|
||||
def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.field_name = field_name
|
||||
self.length, self.maxlength = length, maxlength
|
||||
self.is_required = is_required
|
||||
|
@ -723,7 +737,8 @@ class DatetimeField(TextField):
|
|||
class DateField(TextField):
|
||||
"""A FormField that automatically converts its data to a datetime.date object.
|
||||
The data should be in the format YYYY-MM-DD."""
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidDate] + validator_list
|
||||
TextField.__init__(self, field_name, length=10, maxlength=10,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -747,7 +762,8 @@ class DateField(TextField):
|
|||
class TimeField(TextField):
|
||||
"""A FormField that automatically converts its data to a datetime.time object.
|
||||
The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm."""
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidTime] + validator_list
|
||||
TextField.__init__(self, field_name, length=8, maxlength=8,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -781,7 +797,8 @@ class TimeField(TextField):
|
|||
|
||||
class EmailField(TextField):
|
||||
"A convenience FormField for validating e-mail addresses"
|
||||
def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidEmail] + validator_list
|
||||
TextField.__init__(self, field_name, length, maxlength=maxlength,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -794,7 +811,8 @@ class EmailField(TextField):
|
|||
|
||||
class URLField(TextField):
|
||||
"A convenience FormField for validating URLs"
|
||||
def __init__(self, field_name, length=50, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=50, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidURL] + validator_list
|
||||
TextField.__init__(self, field_name, length=length, maxlength=200,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -806,7 +824,8 @@ class URLField(TextField):
|
|||
raise validators.CriticalValidationError, e.messages
|
||||
|
||||
class IPAddressField(TextField):
|
||||
def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidIPAddress] + validator_list
|
||||
TextField.__init__(self, field_name, length=length, maxlength=maxlength,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -827,7 +846,8 @@ class IPAddressField(TextField):
|
|||
|
||||
class FilePathField(SelectField):
|
||||
"A SelectField whose choices are the files in a given directory."
|
||||
def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
import os
|
||||
if match is not None:
|
||||
import re
|
||||
|
@ -850,7 +870,8 @@ class FilePathField(SelectField):
|
|||
|
||||
class PhoneNumberField(TextField):
|
||||
"A convenience FormField for validating phone numbers (e.g. '630-555-1234')"
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidPhone] + validator_list
|
||||
TextField.__init__(self, field_name, length=12, maxlength=12,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -863,7 +884,8 @@ class PhoneNumberField(TextField):
|
|||
|
||||
class USStateField(TextField):
|
||||
"A convenience FormField for validating U.S. states (e.g. 'IL')"
|
||||
def __init__(self, field_name, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isValidUSState] + validator_list
|
||||
TextField.__init__(self, field_name, length=2, maxlength=2,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
@ -875,15 +897,13 @@ class USStateField(TextField):
|
|||
raise validators.CriticalValidationError, e.messages
|
||||
|
||||
def html2python(data):
|
||||
if data:
|
||||
return data.upper() # Should always be stored in upper case
|
||||
else:
|
||||
return None
|
||||
return data.upper() # Should always be stored in upper case
|
||||
html2python = staticmethod(html2python)
|
||||
|
||||
class CommaSeparatedIntegerField(TextField):
|
||||
"A convenience FormField for validating comma-separated integer fields"
|
||||
def __init__(self, field_name, maxlength=None, is_required=False, validator_list=[]):
|
||||
def __init__(self, field_name, maxlength=None, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [self.isCommaSeparatedIntegerList] + validator_list
|
||||
TextField.__init__(self, field_name, length=20, maxlength=maxlength,
|
||||
is_required=is_required, validator_list=validator_list)
|
||||
|
|
|
@ -55,14 +55,14 @@ class BaseHandler:
|
|||
# Reset query list per request.
|
||||
db.db.queries = []
|
||||
|
||||
# Apply request middleware
|
||||
for middleware_method in self._request_middleware:
|
||||
response = middleware_method(request)
|
||||
if response:
|
||||
return response
|
||||
|
||||
resolver = urlresolvers.RegexURLResolver(r'^/', ROOT_URLCONF)
|
||||
try:
|
||||
# Apply request middleware
|
||||
for middleware_method in self._request_middleware:
|
||||
response = middleware_method(request)
|
||||
if response:
|
||||
return response
|
||||
|
||||
callback, callback_args, callback_kwargs = resolver.resolve(path)
|
||||
|
||||
# Apply view middleware
|
||||
|
|
|
@ -13,9 +13,30 @@ class ModPythonRequest(httpwrappers.HttpRequest):
|
|||
self.path = req.uri
|
||||
|
||||
def __repr__(self):
|
||||
# Since this is called as part of error handling, we need to be very
|
||||
# robust against potentially malformed input.
|
||||
try:
|
||||
get = pformat(self.GET)
|
||||
except:
|
||||
get = '<could not parse>'
|
||||
try:
|
||||
post = pformat(self.POST)
|
||||
except:
|
||||
post = '<could not parse>'
|
||||
try:
|
||||
cookies = pformat(self.COOKIES)
|
||||
except:
|
||||
cookies = '<could not parse>'
|
||||
try:
|
||||
meta = pformat(self.META)
|
||||
except:
|
||||
meta = '<could not parse>'
|
||||
try:
|
||||
user = self.user
|
||||
except:
|
||||
user = '<could not parse>'
|
||||
return '<ModPythonRequest\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s,\nuser:%s>' % \
|
||||
(self.path, pformat(self.GET), pformat(self.POST), pformat(self.COOKIES),
|
||||
pformat(self.META), pformat(self.user))
|
||||
(self.path, get, post, cookies, meta, user)
|
||||
|
||||
def get_full_path(self):
|
||||
return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '')
|
||||
|
@ -141,13 +162,12 @@ class ModPythonHandler(BaseHandler):
|
|||
try:
|
||||
request = ModPythonRequest(req)
|
||||
response = self.get_response(req.uri, request)
|
||||
# Apply response middleware
|
||||
for middleware_method in self._response_middleware:
|
||||
response = middleware_method(request, response)
|
||||
finally:
|
||||
db.db.close()
|
||||
|
||||
# Apply response middleware
|
||||
for middleware_method in self._response_middleware:
|
||||
response = middleware_method(request, response)
|
||||
|
||||
# Convert our custom HttpResponse object back into the mod_python req.
|
||||
populate_apache_request(response, req)
|
||||
return 0 # mod_python.apache.OK
|
||||
|
|
|
@ -55,9 +55,30 @@ class WSGIRequest(httpwrappers.HttpRequest):
|
|||
|
||||
def __repr__(self):
|
||||
from pprint import pformat
|
||||
return '<DjangoRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \
|
||||
(pformat(self.GET), pformat(self.POST), pformat(self.COOKIES),
|
||||
pformat(self.META))
|
||||
# Since this is called as part of error handling, we need to be very
|
||||
# robust against potentially malformed input.
|
||||
try:
|
||||
get = pformat(self.GET)
|
||||
except:
|
||||
get = '<could not parse>'
|
||||
try:
|
||||
post = pformat(self.POST)
|
||||
except:
|
||||
post = '<could not parse>'
|
||||
try:
|
||||
cookies = pformat(self.COOKIES)
|
||||
except:
|
||||
cookies = '<could not parse>'
|
||||
try:
|
||||
meta = pformat(self.META)
|
||||
except:
|
||||
meta = '<could not parse>'
|
||||
try:
|
||||
user = self.user
|
||||
except:
|
||||
user = '<could not parse>'
|
||||
return '<DjangoRequest\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s,\nuser:%s>' % \
|
||||
(self.path, get, post, cookies, meta, user)
|
||||
|
||||
def get_full_path(self):
|
||||
return '%s%s' % (self.path, self.environ['QUERY_STRING'] and ('?' + self.environ['QUERY_STRING']) or '')
|
||||
|
@ -157,13 +178,12 @@ class WSGIHandler(BaseHandler):
|
|||
try:
|
||||
request = WSGIRequest(environ)
|
||||
response = self.get_response(request.path, request)
|
||||
# Apply response middleware
|
||||
for middleware_method in self._response_middleware:
|
||||
response = middleware_method(request, response)
|
||||
finally:
|
||||
db.db.close()
|
||||
|
||||
# Apply response middleware
|
||||
for middleware_method in self._response_middleware:
|
||||
response = middleware_method(request, response)
|
||||
|
||||
try:
|
||||
status_text = STATUS_CODE_TEXT[response.status_code]
|
||||
except KeyError:
|
||||
|
|
|
@ -151,7 +151,7 @@ class BadKeywordArguments(Exception):
|
|||
class BoundRelatedObject(object):
|
||||
def __init__(self, related_object, field_mapping, original):
|
||||
self.relation = related_object
|
||||
self.field_mappings = field_mapping[related_object.opts.module_name]
|
||||
self.field_mappings = field_mapping[related_object.name]
|
||||
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
@ -165,7 +165,7 @@ class RelatedObject(object):
|
|||
self.opts = opts
|
||||
self.field = field
|
||||
self.edit_inline = field.rel.edit_inline
|
||||
self.name = opts.module_name
|
||||
self.name = '%s_%s' % (opts.app_label, opts.module_name)
|
||||
self.var_name = opts.object_name.lower()
|
||||
|
||||
def flatten_data(self, follow, obj=None):
|
||||
|
@ -1734,7 +1734,7 @@ def manipulator_init(opts, add, change, self, obj_key=None, follow=None):
|
|||
# Sanity check -- Make sure the "parent" object exists.
|
||||
# For example, make sure the Place exists for the Restaurant.
|
||||
# Let the ObjectDoesNotExist exception propagate up.
|
||||
lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to
|
||||
lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to.copy()
|
||||
lookup_kwargs['%s__exact' % opts.one_to_one_field.rel.field_name] = obj_key
|
||||
_ = opts.one_to_one_field.rel.to.get_model_module().get_object(**lookup_kwargs)
|
||||
params = dict([(f.attname, f.get_default()) for f in opts.fields])
|
||||
|
|
|
@ -327,18 +327,26 @@ def get_digit(value, arg):
|
|||
# DATES #
|
||||
###################
|
||||
|
||||
EMPTY_DATE_VALUES = (None, '')
|
||||
|
||||
def date(value, arg=DATE_FORMAT):
|
||||
"Formats a date according to the given format"
|
||||
if value in EMPTY_DATE_VALUES:
|
||||
return ''
|
||||
from django.utils.dateformat import format
|
||||
return format(value, arg)
|
||||
|
||||
def time(value, arg=TIME_FORMAT):
|
||||
"Formats a time according to the given format"
|
||||
if value in EMPTY_DATE_VALUES:
|
||||
return ''
|
||||
from django.utils.dateformat import time_format
|
||||
return time_format(value, arg)
|
||||
|
||||
def timesince(value):
|
||||
'Formats a date as the time since that date (i.e. "4 days, 6 hours")'
|
||||
if value in EMPTY_DATE_VALUES:
|
||||
return ''
|
||||
from django.utils.timesince import timesince
|
||||
return timesince(value)
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
"translation helper functions"
|
||||
|
||||
import os, re, sys
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import gettext as gettext_module
|
||||
from cStringIO import StringIO
|
||||
from django.utils.functional import lazy
|
||||
|
@ -25,15 +28,25 @@ _active = {}
|
|||
# The default translation is based on the settings file.
|
||||
_default = None
|
||||
|
||||
# This is a cache for accept-header to translation object mappings to prevent
|
||||
# the accept parser to run multiple times for one user.
|
||||
# This is a cache for normalised accept-header languages to prevent multiple
|
||||
# file lookups when checking the same locale on repeated requests.
|
||||
_accepted = {}
|
||||
|
||||
def to_locale(language):
|
||||
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
|
||||
accept_language_re = re.compile(r'''
|
||||
([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
|
||||
(?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
|
||||
(?:\s*,\s*|$) # Multiple accepts per header.
|
||||
''', re.VERBOSE)
|
||||
|
||||
def to_locale(language, to_lower=False):
|
||||
"Turns a language name (en-us) into a locale name (en_US)."
|
||||
p = language.find('-')
|
||||
if p >= 0:
|
||||
return language[:p].lower()+'_'+language[p+1:].upper()
|
||||
if to_lower:
|
||||
return language[:p].lower()+'_'+language[p+1:].lower()
|
||||
else:
|
||||
return language[:p].lower()+'_'+language[p+1:].upper()
|
||||
else:
|
||||
return language.lower()
|
||||
|
||||
|
@ -297,46 +310,40 @@ def get_language_from_request(request):
|
|||
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
|
||||
return lang_code
|
||||
|
||||
lang_code = request.COOKIES.get('django_language', None)
|
||||
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
|
||||
lang_code = request.COOKIES.get('django_language')
|
||||
if lang_code and lang_code in supported and check_for_language(lang_code):
|
||||
return lang_code
|
||||
|
||||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
|
||||
if accept is not None:
|
||||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
|
||||
for lang, unused in parse_accept_lang_header(accept):
|
||||
if lang == '*':
|
||||
break
|
||||
|
||||
t = _accepted.get(accept, None)
|
||||
if t is not None:
|
||||
return t
|
||||
# We have a very restricted form for our language files (no encoding
|
||||
# specifier, since they all must be UTF-8 and only one possible
|
||||
# language each time. So we avoid the overhead of gettext.find() and
|
||||
# look up the MO file manually.
|
||||
|
||||
def _parsed(el):
|
||||
p = el.find(';q=')
|
||||
if p >= 0:
|
||||
lang = el[:p].strip()
|
||||
order = int(float(el[p+3:].strip())*100)
|
||||
else:
|
||||
lang = el
|
||||
order = 100
|
||||
p = lang.find('-')
|
||||
if p >= 0:
|
||||
mainlang = lang[:p]
|
||||
else:
|
||||
mainlang = lang
|
||||
return (lang, mainlang, order)
|
||||
normalized = locale.locale_alias.get(to_locale(lang, True))
|
||||
if not normalized:
|
||||
continue
|
||||
|
||||
langs = [_parsed(el) for el in accept.split(',')]
|
||||
langs.sort(lambda a,b: -1*cmp(a[2], b[2]))
|
||||
# Remove the default encoding from locale_alias
|
||||
normalized = normalized.split('.')[0]
|
||||
|
||||
for lang, mainlang, order in langs:
|
||||
if lang in supported or mainlang in supported:
|
||||
langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
|
||||
if langfile:
|
||||
# reconstruct the actual language from the language
|
||||
# filename, because otherwise we might incorrectly
|
||||
# report de_DE if we only have de available, but
|
||||
# did find de_DE because of language normalization
|
||||
lang = langfile[len(globalpath):].split(os.path.sep)[1]
|
||||
_accepted[accept] = lang
|
||||
return lang
|
||||
if normalized in _accepted:
|
||||
# We've seen this locale before and have an MO file for it, so no
|
||||
# need to check again.
|
||||
return _accepted[normalized]
|
||||
|
||||
for lang in (normalized, normalized.split('_')[0]):
|
||||
if lang not in supported:
|
||||
continue
|
||||
langfile = os.path.join(globalpath, lang, 'LC_MESSAGES',
|
||||
'django.mo')
|
||||
if os.path.exists(langfile):
|
||||
_accepted[normalized] = lang
|
||||
return lang
|
||||
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
@ -457,3 +464,23 @@ def templatize(src):
|
|||
else:
|
||||
out.write(blankout(t.contents, 'X'))
|
||||
return out.getvalue()
|
||||
|
||||
def parse_accept_lang_header(lang_string):
|
||||
"""
|
||||
Parses the lang_string, which is the body of an HTTP Accept-Language
|
||||
header, and returns a list of (lang, q-value), ordered by 'q' values.
|
||||
|
||||
Any format errors in lang_string results in an empty list being returned.
|
||||
"""
|
||||
result = []
|
||||
pieces = accept_language_re.split(lang_string)
|
||||
if pieces[-1]:
|
||||
return []
|
||||
for i in range(0, len(pieces) - 1, 3):
|
||||
first, lang, priority = pieces[i : i + 3]
|
||||
if first:
|
||||
return []
|
||||
priority = priority and float(priority) or 1.0
|
||||
result.append((lang, priority))
|
||||
result.sort(lambda x, y: -cmp(x[1], y[1]))
|
||||
return result
|
||||
|
|
Loading…
Reference in New Issue