Compare commits

...

39 Commits

Author SHA1 Message Date
James Bennett aa5133500b [0.91-bugfixes] Apply a modified version of the most recent fix from #106
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@9661 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-12-19 20:11:18 +00:00
James Bennett 7d3b409311 [0.91-bugfixes] Fix a silly typo in the version number.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@9230 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-10-12 05:01:01 +00:00
James Bennett 30d4c16ade Bump legacy support branch to 0.91.3 for impending security release
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@8878 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-09-02 21:18:07 +00:00
Jacob Kaplan-Moss 44debfeaa4 Security fix. Announcement forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@8877 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-09-02 21:10:00 +00:00
James Bennett f6c56d473f Version bump 0.91.1 -> 0.91.2 for forthcoming security bugfix release
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@7532 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-05-14 04:16:31 +00:00
James Bennett 6e657e2c40 Backport [7521] to 0.91-bugfixes per security policy; announcement and security bugfix release will be forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@7529 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-05-14 04:07:42 +00:00
Jacob Kaplan-Moss 2c03839d79 [0.91-bugfixes] Fixed, once again, the stale-db connection problem. Seems [3725] wasn't applied to the wsgi handler
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@7155 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-02-25 18:02:13 +00:00
Jacob Kaplan-Moss 8bc36e726c i18n security fix. Details will be posted shortly to the Django mailing lists and the official weblog.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@6605 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-26 19:52:04 +00:00
James Bennett 613cfabdd0 0.91-bugfixes: Backport [3066] and some related changes. Refs #1635, #106.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@6474 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-12 18:47:11 +00:00
James Bennett 9cd09e7562 0.91-bugfixes: Minor improvement to [6468
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@6471 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-10 15:51:38 +00:00
James Bennett 56c074f565 0.91-bugfixes: Backport silent failure of date-related template filters
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@6468 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-08 20:53:23 +00:00
James Bennett d797b34bf8 0.91-bugfixes: Backport [3785] from Django trunk
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@6373 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-09-17 19:00:39 +00:00
James Bennett 0417b0f17b 0.91-bugfixes: Revert previous, since it doesn't seem to actually solve the bug
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@5648 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-07-11 17:47:06 +00:00
James Bennett e6f09afa78 0.91-bugfixes: Fix problem parsing values from ManyToManyField with raw_id_admin. Thanks, Tom Tobin
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@5647 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-07-11 17:41:58 +00:00
James Bennett 17e98808c1 0.91-bugfixes: Fixed #4651; UnicodeCursorWrapper should work with dictionaries of parameters now
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@5508 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-06-21 16:46:23 +00:00
James Bennett 1680e8709c 0.91-bugfixes: Fixed a subtle bug with mutable default arguments to form fields. Thanks, Tom Tobin.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@5507 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-06-21 16:38:51 +00:00
James Bennett f63faca4c6 0.91-bugfixes: fix lookup kwarg corruption in certain types of one-to-one listings. Thanks Tom Tobin for finally figuring this one out
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4936 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-04-05 18:36:04 +00:00
James Bennett 32b733ccbd 0.91-bugfixes: Table name needs to be pluralized in that query
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4714 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-12 17:12:10 +00:00
James Bennett d8b1717266 0.91-bugfixes: Fix bad settings reference in comments feeds
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4713 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-12 15:46:36 +00:00
James Bennett 18cf40006c Backing out [4694] because it looks like the version on trunk caused problems
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4696 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-09 22:43:44 +00:00
James Bennett f46f052e05 0.91-bugfixes: backporting [4693] as the other half of the fix in [4673]
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4694 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-09 17:42:47 +00:00
James Bennett 8a0fa75839 0.91-bugfixes: Fixed #999 by resolving name clash in the metasystem which could confuse manipulators about which fields they should follow. Refs #1808, #1826, #1839 and #2415, which are variations of this that persist in trunk.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4673 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-07 17:03:43 +00:00
James Bennett 02ca9fd1de 0.91-bugfixes: settings fix for backport in [4419]
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4453 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-01-30 19:06:08 +00:00
James Bennett d0672a7344 0.91-bufixes: Backport [4244] for those using legacy Django with psycopg1
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4419 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-01-24 19:57:41 +00:00
James Bennett f931905517 0.91-bugfixes: Backport [3820] to 0.91-bugfixes, refs #2745.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4099 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-11-24 18:24:49 +00:00
James Bennett 2823775114 0.91-bugfixes: Backport [3820] to 0.91-bugfixes, refs #2745.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4098 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-11-24 18:24:34 +00:00
James Bennett 1fcef7a07e 0.91-bugfixes: Move request middleware into the handler's try/catch block so we can deal with exceptions it throws
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@4097 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-11-24 18:16:36 +00:00
James Bennett 3720a12a2a 0.91-bugfixes: corrected version of previous (COMMENTS_FIRST_FEW is imported directly)
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3943 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-10-27 19:19:28 +00:00
James Bennett c08f81ff5e 091-bugfixes: backporting fix in [3936] to 0.91-bugfixes
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3942 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-10-27 19:17:38 +00:00
James Bennett b457108751 Fixed #1113, thanks to Jacob for spotting the deeper bug
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3935 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-10-25 20:25:54 +00:00
James Bennett e35be34c21 0.91-bugfixes: fix bad copy/paste in previous version of README
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3729 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-09-05 16:36:09 +00:00
James Bennett 4b97347921 0.91-bugfixes: changes in the README to provide more useful info on the branch and how it relates to Django proper
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3728 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-09-05 16:35:20 +00:00
James Bennett db2e70a74b 0.91-bugfixes: fix a problem on mod_python that could result in stale DB connections
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3725 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-09-05 16:03:02 +00:00
James Bennett d31e39173c 0.91-fixes: Fixed minor security hole in compile-messages.py. See trunk patch in [3592]
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3593 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-08-16 06:28:59 +00:00
James Bennett 0d60669146 0.91-bugfix: Fixed #1113, limit_choices_to on OneToOneField no longer breaks admin changelist
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3576 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-08-13 02:59:41 +00:00
James Bennett b70c86c36d 0.91-bugfix: Fix typo in update to README
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3574 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-08-13 01:39:33 +00:00
James Bennett 06a3c04c87 Update README with info on new branch
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3573 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-08-13 01:38:55 +00:00
Adrian Holovaty 5623203d15 Created 0.91-bugfixes branch, for bug fixes to version 0.91, for people stuck with 0.91. Currently it's a copy of the pre-magic-removal notable moment tag
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.91-bugfixes@3572 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-08-13 00:58:37 +00:00
Adrian Holovaty 9a2f6a542a Created pre-magic-removal tag, so we have an easy way to switch back to the last pre-magic-removal Django code.
git-svn-id: http://code.djangoproject.com/svn/django/tags/notable_moments/pre-magic-removal@2808 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2006-05-02 01:09:45 +00:00
18 changed files with 334 additions and 168 deletions

60
README
View File

@ -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).

View File

@ -1 +1 @@
VERSION = (0, 9, 1, 'SVN')
VERSION = (0, 91, 3, 'SVN')

View File

@ -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__":

View File

@ -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(/&lt;/g, '<');
text = text.replace(/&gt;/g, '>');
text = text.replace(/&quot;/g, '"');
text = text.replace(/&#39;/g, "'");
text = text.replace(/&amp;/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') {

View File

@ -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 ">

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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()]:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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])

View File

@ -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)

View File

@ -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

View File

@ -5,7 +5,7 @@ from setuptools import setup, find_packages
setup(
name = "Django",
version = "0.91",
version = "0.91.3",
url = 'http://www.djangoproject.com/',
author = 'Lawrence Journal-World',
author_email = 'holovaty@gmail.com',