From 0b5c67a7465f354ceb531489d2124921f832db27 Mon Sep 17 00:00:00 2001 From: Joseph Kocherhans Date: Tue, 5 Jan 2010 00:39:42 +0000 Subject: [PATCH] [soc2009/model-validation] Merged from trunk up to [12093]. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/model-validation@12094 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 2 +- .../admin/media/js/admin/DateTimeShortcuts.js | 15 +- django/contrib/admin/media/js/calendar.js | 16 +- .../comments/templates/comments/list.html | 10 + .../contrib/comments/templatetags/comments.py | 80 ++++++- django/contrib/gis/db/models/sql/compiler.py | 6 +- django/contrib/localflavor/us/forms.py | 4 +- django/core/mail/__init__.py | 13 +- django/core/servers/basehttp.py | 23 +- django/test/utils.py | 2 +- django/utils/termcolors.py | 22 +- django/views/i18n.py | 40 +++- docs/ref/contrib/comments/example.txt | 208 ++++++++++++++++++ docs/ref/contrib/comments/index.txt | 70 +++++- docs/ref/contrib/csrf.txt | 2 +- docs/ref/django-admin.txt | 6 + docs/releases/1.2.txt | 4 +- docs/topics/email.txt | 14 +- docs/topics/forms/formsets.txt | 4 +- docs/topics/forms/index.txt | 14 +- docs/topics/forms/modelforms.txt | 8 +- .../comment_tests/tests/templatetag_tests.py | 32 +++ tests/regressiontests/csrf_tests/tests.py | 2 +- tests/regressiontests/mail/tests.py | 16 +- 24 files changed, 542 insertions(+), 71 deletions(-) create mode 100644 django/contrib/comments/templates/comments/list.html create mode 100644 docs/ref/contrib/comments/example.txt diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index e3ba6602cc..0cc591daa5 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -143,7 +143,7 @@ DATABASES = { # The default is to use the SMTP backend. # Third-party backends can be specified by providing a Python path # to a module that defines an EmailBackend class. -EMAIL_BACKEND = 'django.core.mail.backends.smtp' +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # Host for sending e-mail. EMAIL_HOST = 'localhost' diff --git a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js index 0c6eff0fa9..016de90f28 100644 --- a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js @@ -44,7 +44,7 @@ var DateTimeShortcuts = { var shortcuts_span = document.createElement('span'); inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); var now_link = document.createElement('a'); - now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + gettext('TIME_INPUT_FORMATS') + "'));"); + now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + get_format('TIME_INPUT_FORMATS')[0] + "'));"); now_link.appendChild(document.createTextNode(gettext('Now'))); var clock_link = document.createElement('a'); clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); @@ -80,10 +80,11 @@ var DateTimeShortcuts = { quickElement('h2', clock_box, gettext('Choose a time')); time_list = quickElement('ul', clock_box, ''); time_list.className = 'timelist'; - quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + gettext('TIME_INPUT_FORMATS') + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + gettext('TIME_INPUT_FORMATS') + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + gettext('TIME_INPUT_FORMATS') + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + gettext('TIME_INPUT_FORMATS') + "'));"); + time_format = get_format('TIME_INPUT_FORMATS')[0]; + quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));"); + quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));"); + quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));"); + quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));"); cancel_p = quickElement('p', clock_box, ''); cancel_p.className = 'calendar-cancel'; @@ -236,7 +237,7 @@ var DateTimeShortcuts = { DateTimeShortcuts.calendars[num].drawNextMonth(); }, handleCalendarCallback: function(num) { - format = gettext('DATE_INPUT_FORMATS'); + format = get_format('DATE_INPUT_FORMATS')[0]; // the format needs to be escaped a little format = format.replace('\\', '\\\\'); format = format.replace('\r', '\\r'); @@ -248,7 +249,7 @@ var DateTimeShortcuts = { handleCalendarQuickLink: function(num, offset) { var d = new Date(); d.setDate(d.getDate() + offset) - DateTimeShortcuts.calendarInputs[num].value = d.strftime(gettext('DATE_INPUT_FORMATS')); + DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); DateTimeShortcuts.dismissCalendar(num); }, cancelEventPropagation: function(e) { diff --git a/django/contrib/admin/media/js/calendar.js b/django/contrib/admin/media/js/calendar.js index 3f57145041..c95a95db1b 100644 --- a/django/contrib/admin/media/js/calendar.js +++ b/django/contrib/admin/media/js/calendar.js @@ -25,7 +25,7 @@ function quickElement() { var CalendarNamespace = { monthsOfYear: gettext('January February March April May June July August September October November December').split(' '), daysOfWeek: gettext('S M T W T F S').split(' '), - firstDayOfWeek: parseInt(gettext('FIRST_DAY_OF_WEEK')), + firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), isLeapYear: function(year) { return (((year % 4)==0) && ((year % 100)!=0) || ((year % 400)==0)); }, @@ -46,6 +46,12 @@ var CalendarNamespace = { return days; }, draw: function(month, year, div_id, callback) { // month = 1-12, year = 1-9999 + var today = new Date(); + var todayDay = today.getDate(); + var todayMonth = today.getMonth()+1; + var todayYear = today.getFullYear(); + var todayClass = ''; + month = parseInt(month); year = parseInt(year); var calDiv = document.getElementById(div_id); @@ -76,7 +82,13 @@ var CalendarNamespace = { if (i%7 == 0 && currentDay != 1) { tableRow = quickElement('tr', tableBody); } - var cell = quickElement('td', tableRow, ''); + if ((currentDay==todayDay) && (month==todayMonth) && (year==todayYear)) { + todayClass='today'; + } else { + todayClass=''; + } + var cell = quickElement('td', tableRow, '', 'class', todayClass); + quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '('+year+','+month+','+currentDay+'));'); currentDay++; } diff --git a/django/contrib/comments/templates/comments/list.html b/django/contrib/comments/templates/comments/list.html new file mode 100644 index 0000000000..3d4ec1ed8f --- /dev/null +++ b/django/contrib/comments/templates/comments/list.html @@ -0,0 +1,10 @@ +
+ {% for comment in comment_list %} +
+ {{ comment.submit_date }} - {{ comment.name }} +
+
+

{{ comment.comment }}

+
+ {% endfor %} +
diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 9f8180ab08..42691c63e4 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -81,10 +81,10 @@ class BaseCommentNode(template.Node): object_pk = smart_unicode(object_pk), site__pk = settings.SITE_ID, ) - + # The is_public and is_removed fields are implementation details of the # built-in comment model's spam filtering system, so they might not - # be present on a custom comment model subclass. If they exist, we + # be present on a custom comment model subclass. If they exist, we # should filter on them. field_names = [f.name for f in self.comment_model._meta.fields] if 'is_public' in field_names: @@ -169,6 +169,46 @@ class RenderCommentFormNode(CommentFormNode): else: return '' +class RenderCommentListNode(CommentListNode): + """Render the comment list directly""" + + #@classmethod + def handle_token(cls, parser, token): + """Class method to parse render_comment_list and return a Node.""" + tokens = token.contents.split() + if tokens[1] != 'for': + raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) + + # {% render_comment_list for obj %} + if len(tokens) == 3: + return cls(object_expr=parser.compile_filter(tokens[2])) + + # {% render_comment_list for app.models pk %} + elif len(tokens) == 4: + return cls( + ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), + object_pk_expr = parser.compile_filter(tokens[3]) + ) + handle_token = classmethod(handle_token) + + def render(self, context): + ctype, object_pk = self.get_target_ctype_pk(context) + if object_pk: + template_search_list = [ + "comments/%s/%s/list.html" % (ctype.app_label, ctype.model), + "comments/%s/list.html" % ctype.app_label, + "comments/list.html" + ] + qs = self.get_query_set(context) + context.push() + liststr = render_to_string(template_search_list, { + "comment_list" : self.get_context_value_from_queryset(context, qs) + }, context) + context.pop() + return liststr + else: + return '' + # We could just register each classmethod directly, but then we'd lose out on # the automagic docstrings-into-admin-docs tricks. So each node gets a cute # wrapper function that just exists to hold the docstring. @@ -216,6 +256,24 @@ def get_comment_list(parser, token): """ return CommentListNode.handle_token(parser, token) +#@register.tag +def render_comment_list(parser, token): + """ + Render the comment list (as returned by ``{% get_comment_list %}``) + through the ``comments/list.html`` template + + Syntax:: + + {% render_comment_list for [object] %} + {% render_comment_list for [app].[model] [object_id] %} + + Example usage:: + + {% render_comment_list for event %} + + """ + return RenderCommentListNode.handle_token(parser, token) + #@register.tag def get_comment_form(parser, token): """ @@ -248,12 +306,28 @@ def comment_form_target(): Example:: -
+ """ return comments.get_form_target() +#@register.simple_tag +def get_comment_permalink(comment, anchor_pattern=None): + """ + Get the permalink for a comment, optionally specifying the format of the + named anchor to be appended to the end of the URL. + + Example:: + {{ get_comment_permalink comment "#c%(id)s-by-%(user_name)s" }} + """ + + if anchor_pattern: + return comment.get_absolute_url(anchor_pattern) + return comment.get_absolute_url() + register.tag(get_comment_count) register.tag(get_comment_list) register.tag(get_comment_form) register.tag(render_comment_form) register.simple_tag(comment_form_target) +register.simple_tag(get_comment_permalink) +register.tag(render_comment_list) diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index a94093fec3..78eeeafe19 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -55,8 +55,8 @@ class GeoSQLCompiler(compiler.SQLCompiler): aliases.add(r) col_aliases.add(col[1]) else: - result.append(col.as_sql(qn=qn)) - + result.append(col.as_sql(qn, self.connection)) + if hasattr(col, 'alias'): aliases.add(col.alias) col_aliases.add(col.alias) @@ -70,7 +70,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): max_name_length = self.connection.ops.max_name_length() result.extend([ '%s%s' % ( - self.get_extra_select_format(alias) % aggregate.as_sql(qn=qn, connection=self.connection), + self.get_extra_select_format(alias) % aggregate.as_sql(qn, self.connection), alias is not None and ' AS %s' % qn(truncate_name(alias, max_name_length)) or '' diff --git a/django/contrib/localflavor/us/forms.py b/django/contrib/localflavor/us/forms.py index dc6daaf165..c426d36c66 100644 --- a/django/contrib/localflavor/us/forms.py +++ b/django/contrib/localflavor/us/forms.py @@ -23,7 +23,7 @@ class USZipCodeField(RegexField): class USPhoneNumberField(CharField): default_error_messages = { - 'invalid': u'Phone numbers must be in XXX-XXX-XXXX format.', + 'invalid': _('Phone numbers must be in XXX-XXX-XXXX format.'), } def clean(self, value): @@ -85,7 +85,7 @@ class USStateField(Field): abbreviation for the given state. """ default_error_messages = { - 'invalid': u'Enter a U.S. state or territory.', + 'invalid': _('Enter a U.S. state or territory.'), } def clean(self, value): diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py index 9a629035cf..f9d1210791 100644 --- a/django/core/mail/__init__.py +++ b/django/core/mail/__init__.py @@ -28,16 +28,17 @@ def get_connection(backend=None, fail_silently=False, **kwds): """ path = backend or settings.EMAIL_BACKEND try: - mod = import_module(path) + mod_name, klass_name = path.rsplit('.', 1) + mod = import_module(mod_name) except ImportError, e: - raise ImproperlyConfigured(('Error importing email backend %s: "%s"' - % (path, e))) + raise ImproperlyConfigured(('Error importing email backend module %s: "%s"' + % (mod_name, e))) try: - cls = getattr(mod, 'EmailBackend') + klass = getattr(mod, klass_name) except AttributeError: raise ImproperlyConfigured(('Module "%s" does not define a ' - '"EmailBackend" class' % path)) - return cls(fail_silently=fail_silently, **kwds) + '"%s" class' % (mod_name, klass_name))) + return klass(fail_silently=fail_silently, **kwds) def send_mail(subject, message, from_email, recipient_list, diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index f7c0a77f7c..415756c403 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -15,6 +15,7 @@ import stat import sys import urllib +from django.core.management.color import color_style from django.utils.http import http_date from django.utils._os import safe_join @@ -557,6 +558,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): # We set self.path to avoid crashes in log_message() on unsupported # requests (like "OPTIONS"). self.path = '' + self.style = color_style() BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def get_environ(self): @@ -608,7 +610,26 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): # Don't bother logging requests for admin images or the favicon. if self.path.startswith(self.admin_media_prefix) or self.path == '/favicon.ico': return - sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), format % args)) + + msg = "[%s] %s\n" % (self.log_date_time_string(), format % args) + + # Utilize terminal colors, if available + if args[1][0] == '2': + # Put 2XX first, since it should be the common case + msg = self.style.HTTP_SUCCESS(msg) + elif args[1][0] == '1': + msg = self.style.HTTP_INFO(msg) + elif args[1][0] == '3': + msg = self.style.HTTP_REDIRECT(msg) + elif args[1] == '404': + msg = self.style.HTTP_NOT_FOUND(msg) + elif args[1][0] == '4': + msg = self.style.HTTP_BAD_REQUEST(msg) + else: + # Any 5XX, or any other response + msg = self.style.HTTP_SERVER_ERROR(msg) + + sys.stderr.write(msg) class AdminMediaHandler(object): """ diff --git a/django/test/utils.py b/django/test/utils.py index 5990e9d07f..1129976e4e 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -43,7 +43,7 @@ def setup_test_environment(): mail.SMTPConnection = locmem.EmailBackend mail.original_email_backend = settings.EMAIL_BACKEND - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem' + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' mail.outbox = [] diff --git a/django/utils/termcolors.py b/django/utils/termcolors.py index 009b69ed03..4bcf634061 100644 --- a/django/utils/termcolors.py +++ b/django/utils/termcolors.py @@ -78,6 +78,12 @@ PALETTES = { 'SQL_COLTYPE': {}, 'SQL_KEYWORD': {}, 'SQL_TABLE': {}, + 'HTTP_INFO': {}, + 'HTTP_SUCCESS': {}, + 'HTTP_REDIRECT': {}, + 'HTTP_BAD_REQUEST': {}, + 'HTTP_NOT_FOUND': {}, + 'HTTP_SERVER_ERROR': {}, }, DARK_PALETTE: { 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, @@ -86,6 +92,12 @@ PALETTES = { 'SQL_COLTYPE': { 'fg': 'green' }, 'SQL_KEYWORD': { 'fg': 'yellow' }, 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green' }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'yellow' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, }, LIGHT_PALETTE: { 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, @@ -94,6 +106,12 @@ PALETTES = { 'SQL_COLTYPE': { 'fg': 'green' }, 'SQL_KEYWORD': { 'fg': 'blue' }, 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'red' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, } } DEFAULT_PALETTE = DARK_PALETTE @@ -117,7 +135,9 @@ def parse_color_setting(config_string): definition will augment the base palette definition. Valid roles: - 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table' + 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table', + 'http_info', 'http_success', 'http_redirect', 'http_bad_request', + 'http_not_found', 'http_server_error' Valid colors: 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' diff --git a/django/views/i18n.py b/django/views/i18n.py index 6a1ee514cd..dc0adf356e 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -53,7 +53,14 @@ def get_formats(): result[attr] = getattr(module, attr) except AttributeError: pass - return result + src = [] + for k, v in result.items(): + if isinstance(v, (basestring, int)): + src.append("formats['%s'] = '%s';\n" % (javascript_quote(k), javascript_quote(smart_unicode(v)))) + elif isinstance(v, (tuple, list)): + v = [javascript_quote(smart_unicode(value)) for value in v] + src.append("formats['%s'] = ['%s'];\n" % (javascript_quote(k), "', '".join(v))) + return ''.join(src) NullSource = """ /* gettext identity library */ @@ -90,6 +97,25 @@ function ngettext(singular, plural, count) { } function gettext_noop(msgid) { return msgid; } + +""" + +LibFormatHead = """ +/* formatting library */ + +var formats = new Array(); + +""" + +LibFormatFoot = """ +function get_format(format_type) { + var value = formats[format_type]; + if (typeof(value) == 'undefined') { + return msgid; + } else { + return value; + } +} """ SimplePlural = """ @@ -122,7 +148,8 @@ def null_javascript_catalog(request, domain=None, packages=None): Returns "identity" versions of the JavaScript i18n functions -- i.e., versions that don't actually do anything. """ - return http.HttpResponse(NullSource + InterPolate, 'text/javascript') + src = [NullSource, InterPolate, LibFormatHead, get_formats(), LibFormatFoot] + return http.HttpResponse(''.join(src), 'text/javascript') def javascript_catalog(request, domain='djangojs', packages=None): """ @@ -210,15 +237,12 @@ def javascript_catalog(request, domain='djangojs', packages=None): csrc.sort() for k, v in pdict.items(): src.append("catalog['%s'] = [%s];\n" % (javascript_quote(k), ','.join(["''"]*(v+1)))) - for k, v in get_formats().items(): - if isinstance(v, (basestring, int)): - src.append("catalog['%s'] = '%s';\n" % (javascript_quote(k), javascript_quote(smart_unicode(v)))) - elif isinstance(v, (tuple, list)): - v = [javascript_quote(smart_unicode(value)) for value in v] - src.append("catalog['%s'] = ['%s'];\n" % (javascript_quote(k), "', '".join(v))) src.extend(csrc) src.append(LibFoot) src.append(InterPolate) + src.append(LibFormatHead) + src.append(get_formats()) + src.append(LibFormatFoot) src = ''.join(src) return http.HttpResponse(src, 'text/javascript') diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt new file mode 100644 index 0000000000..ec600f7219 --- /dev/null +++ b/docs/ref/contrib/comments/example.txt @@ -0,0 +1,208 @@ +.. _ref-contrib-comments-example: + +.. highlightlang:: html+django + +=========================================== +Example of using the in-built comments app +=========================================== + +Follow the first three steps of the quick start guide in the +:ref:`documentation `. + +Now suppose, you have an app (``blog``) with a model (``Post``) +to which you want to attach comments. Let us also suppose that +you have a template called ``blog_detail.html`` where you want +to display the comments list and comment form. + +Template +======== + +First, we should load the ``comment`` template tags in the +``blog_detail.html`` so that we can use it's functionality. So +just like all other custom template tag libraries:: + + {% load comments %} + +Next, let us add the number of comments attached to the particular +model instance of ``Post``. For this we assume that a context +variable ``object_pk`` is present which gives the ``id`` of the +instance of ``Post``. + +The usage of the :ttag:`get_comment_count` tag is like below:: + + {% get_comment_count for blog.post object_pk as comment_count %} +

{{ comment_count }} comments have been posted.

+ +If you have the instance (say ``entry``) of the model (``Post``) +available in the context, then you can refer to it directly:: + + {% get_comment_count for entry as comment_count %} +

{{ comment_count }} comments have been posted.

+ +Next, we can use the :ttag:`render_comment_list` tag, to render all comments +to the given instance (``entry``) by using the ``comments/list.html`` template. + + {% render_comment_list for entry %} + +Django will will look for the ``list.html`` under the following directories +(for our example):: + + comments/blog/post/list.html + comments/blog/list.html + comments/list.html + +To get a list of comments, we make use of the :ttag:`get_comment_list` tag. +This tag's usage is very similar to the :ttag:`get_comment_count` tag. We +need to remember that the :ttag:`get_comment_list` returns a list of comments +and hence we will have to iterate through them to display them:: + + {% get_comment_list for blog.post object_pk as comment_list %} + {% for comment in comment_list %} +

Posted by: {{ comment.user_name }} on {{ comment.submit_date }}

+ ... +

Comment: {{ comment.comment }}

+ ... + {% endfor %} + +Finally, we display the comment form, enabling users to enter their +comments. There are two ways of doing so. The first is when you want to +display the comments template available under your ``comments/form.html``. +The other method gives you a chance to customize the form. + +The first method makes use of the :ttag:`render_comment_form` tag. It's usage +too is similar to the other three tags we have discussed above:: + + {% render_comment_form for entry %} + +It looks for the ``form.html`` under the following directories +(for our example):: + + comments/blog/post/form.html + comments/blog/form.html + comments/form.html + +Since we customize the form in the second method, we make use of another +tag called :ttag:`comment_form_target`. This tag on rendering gives the URL +where the comment form is posted. Without any :ref:`customization +`, :ttag:`comment_form_target` evaluates to +``/comments/post/``. We use this tag in the form's ``action`` attribute. + +The :ttag:`get_comment_form` tag renders a ``form`` for a model instance by +creating a context variable. One can iterate over the ``form`` object to +get individual fields. This gives you fine-grain control over the form:: + + {% for field in form %} + {% ifequal field.name "comment" %} + + ... + {% endfor %} + +But let's look at a simple example:: + + {% get_comment_form for entry as form %} + + + + {{ form }} + + + + + +
+ +Flagging +======== + +If you want your users to be able to flag comments (say for profanity), you +can just direct them (by placing a link in your comment list) to ``/flag/{{ +comment.id }}/``. Similarly, a user with requisite permissions (``"Can +moderate comments"``) can approve and delete comments. This can also be +done through the ``admin`` as you'll see later. You might also want to +customize the following templates: + + * ``flag.html`` + * ``flagged.html`` + * ``approve.html`` + * ``approved.html`` + * ``delete.html`` + * ``deleted.html`` + +found under the directory structure we saw for ``form.html``. + +Feeds +===== + +Suppose you want to export a :ref:`feed ` of the +latest comments, you can use the in-built :class:`LatestCommentFeed`. Just +enable it in your project's ``urls.py``: + +.. code-block:: python + + from django.conf.urls.defaults import * + from django.contrib.comments.feeds import LatestCommentFeed + + feeds = { + 'latest': LatestCommentFeed, + } + + urlpatterns = patterns('', + # ... + (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', + {'feed_dict': feeds}), + # ... + ) + +Now you should have the latest comment feeds being served off ``/feeds/latest/``. + +Moderation +========== + +Now that we have the comments framework working, we might want to have some +moderation setup to administer the comments. The comments framework comes +in-built with :ref:`generic comment moderation +`. The comment moderation has the following +features (all of which or only certain can be enabled): + + * Enable comments for a particular model instance. + * Close comments after a particular (user-defined) number of days. + * Email new comments to the site-staff. + +To enable comment moderation, we subclass the :class:`CommentModerator` and +register it with the moderation features we want. Let us suppose we want to +close comments after 7 days of posting and also send out an email to the +site staff. In ``blog/models.py``, we register a comment moderator in the +following way: + +.. code-block:: python + + from django.contrib.comments.moderation import CommentModerator, moderator + from django.db import models + + class Post(models.Model): + title = models.CharField(max_length = 255) + content = models.TextField() + posted_date = models.DateTimeField() + + class PostModerator(CommentModerator): + email_notification = True + auto_close_field = 'posted_date' + # Close the comments after 7 days. + close_after = 7 + + moderator.register(Post, PostModerator) + +The generic comment moderation also has the facility to remove comments. +These comments can then be moderated by any user who has access to the +``admin`` site and the ``Can moderate comments`` permission (can be set +under the ``Users`` page in the ``admin``). + +The moderator can ``Flag``, ``Approve`` or ``Remove`` comments using the +``Action`` drop-down in the ``admin`` under the ``Comments`` page. + +.. note:: + + Only a super-user will be able to delete comments from the database. + ``Remove Comments`` only sets the ``is_public`` attribute to + ``False``. diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index 6ee109782f..319dadff08 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -84,11 +84,34 @@ different ways you can specify which object to attach to: In the above, ``blog.entry`` is the app label and (lower-cased) model name of the model class. -.. templatetag:: get_comment_list - Displaying comments ------------------- +To display a list of comments, you can use the template tags +:ttag:`render_comment_list` or :ttag:`get_comment_list`. + +.. templatetag:: render_comment_list + +Quickly rendering a comment list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to display a list of comments for some object is by using +:ttag:`render_comment_list`:: + + {% render_comment_list for [object] %} + +For example:: + + {% render_comment_list for event %} + +This will render comments using a template named ``comments/list.html``, a +default version of which is included with Django. + +.. templatetag:: get_comment_list + +Rendering a custom comment list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + To get the list of comments for some object, use :ttag:`get_comment_list`:: {% get_comment_list for [object] as [varname] %} @@ -104,6 +127,44 @@ This returns a list of :class:`~django.contrib.comments.models.Comment` objects; see :ref:`the comment model documentation ` for details. +.. templatetag:: get_comment_permalink + +Linking to comments +------------------- + +To provide a permalink to a specific comment, use :ttag:`get_comment_permalink`:: + + {% get_comment_permalink comment_obj [format_string] %} + +By default, the named anchor that will be appended to the URL will be the letter +'c' followed by the comment id, for example 'c82'. You may specify a custom +format string if you wish to override this behavior:: + + {% get_comment_permalink comment "#c%(id)s-by-%(user_name)s"%} + +The format string is a standard python format string. Valid mapping keys +include any attributes of the comment object. + +Regardless of whether you specify a custom anchor pattern, you must supply a +matching named anchor at a suitable place in your template. + +For example:: + + {% for comment in comment_list %} + + + permalink for comment #{{ forloop.counter }} + + ... + {% endfor %} + +.. warning:: + + There's a known bug in Safari/Webkit which causes the named anchor to be + forgotten following a redirect. The practical impact for comments is that + the Safari/webkit browsers will arrive at the correct page but will not + scroll to the named anchor. + .. templatetag:: get_comment_count Counting comments @@ -157,7 +218,7 @@ you can use in the template:: A complete form might look like:: {% get_comment_form for event as form %} -
+ {{ form }} @@ -178,7 +239,7 @@ You may have noticed that the above example uses another template tag -- form. This will always return the correct URL that comments should be posted to; you'll always want to use it like above:: - + Redirecting after the comment post ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -238,3 +299,4 @@ More information custom forms moderation + example diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index ea76fc3739..e635a0380b 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -49,7 +49,7 @@ To enable CSRF protection for your views, follow these steps: 2. In any template that uses a POST form, use the :ttag:`csrf_token` tag inside the ```` element if the form is for an internal URL, e.g.:: - {% csrf_token %} + {% csrf_token %} This should not be done for POST forms that target external URLs, since that would cause the CSRF token to be leaked, leading to a vulnerability. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 88715ebdc7..af47759745 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1026,6 +1026,12 @@ number of roles in which color is used: * ``sql_coltype`` - The type of a model field in SQL. * ``sql_keyword`` - A SQL keyword. * ``sql_table`` - The name of a model in SQL. + * ``http_info`` - A 1XX HTTP Informational server response. + * ``http_success`` - A 2XX HTTP Success server response. + * ``http_redirect`` - A 3XX HTTP Redirect server response. + * ``http_not_found`` - A 404 HTTP Not Found server response. + * ``http_bad_request`` - A 4XX HTTP Bad Request server response other than 404. + * ``http_server_error`` - A 5XX HTTP Server Error response. Each of these roles can be assigned a specific foreground and background color, from the following list: diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index a549bd0cce..d66b79f9c5 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -286,7 +286,7 @@ connection with which to send e-mail, you can explicitly request an SMTP connection:: from django.core.mail import get_connection - connection = get_connection('django.core.mail.backends.smtp') + connection = get_connection('django.core.mail.backends.smtp.EmailBackend') messages = get_notification_email() connection.send_messages(messages) @@ -294,7 +294,7 @@ If your call to construct an instance of ``SMTPConnection`` required additional arguments, those arguments can be passed to the :meth:`~django.core.mail.get_connection()` call:: - connection = get_connection('django.core.mail.backends.smtp', hostname='localhost', port=1234) + connection = get_connection('django.core.mail.backends.smtp.EmailBackend', hostname='localhost', port=1234) User Messages API ----------------- diff --git a/docs/topics/email.txt b/docs/topics/email.txt index eee77cb4a0..b23b5d328f 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -408,7 +408,7 @@ settings file. The SMTP backend is the default configuration inherited by Django. If you want to specify it explicitly, put the following in your settings:: - EMAIL_BACKEND = 'django.core.mail.backends.smtp' + EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' .. admonition:: SMTPConnection objects @@ -433,7 +433,7 @@ providing the ``stream`` keyword argument when constructing the connection. To specify this backend, put the following in your settings:: - EMAIL_BACKEND = 'django.core.mail.backends.console' + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' This backend is not intended for use in production -- it is provided as a convenience that can be used during development. @@ -451,7 +451,7 @@ the ``file_path`` keyword when creating a connection with To specify this backend, put the following in your settings:: - EMAIL_BACKEND = 'django.core.mail.backends.filebased' + EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location This backend is not intended for use in production -- it is provided as a @@ -470,7 +470,7 @@ be send. To specify this backend, put the following in your settings:: - EMAIL_BACKEND = 'django.core.mail.backends.locmem' + EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' This backend is not intended for use in production -- it is provided as a convenience that can be used during development and testing. @@ -483,7 +483,7 @@ Dummy backend As the name suggests the dummy backend does nothing with your messages. To specify this backend, put the following in your settings:: - EMAIL_BACKEND = 'django.core.mail.backends.dummy' + EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' This backend is not intended for use in production -- it is provided as a convenience that can be used during development. @@ -495,7 +495,7 @@ Defining a custom e-mail backend If you need to change how e-mails are send you can write your own e-mail backend. The ``EMAIL_BACKEND`` setting in your settings file is then the -Python import path for your backend. +Python import path for your backend class. Custom e-mail backends should subclass ``BaseEmailBackend`` that is located in the ``django.core.mail.backends.base`` module. A custom e-mail backend must @@ -503,7 +503,7 @@ implement the ``send_messages(email_messages)`` method. This method receives a list of :class:`~django.core.mail.EmailMessage` instances and returns the number of successfully delivered messages. If your backend has any concept of a persistent session or connection, you should also implement the ``open()`` -and ``close()`` methods. Refer to ``SMTPEmailBackend`` for a reference +and ``close()`` methods. Refer to ``smtp.EmailBackend`` for a reference implementation. .. _topics-sending-multiple-emails: diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index e6146aeaba..974a5ca251 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -355,7 +355,7 @@ The ``manage_articles.html`` template might look like this: .. code-block:: html+django - + {{ formset.management_form }} {% for form in formset.forms %} @@ -369,7 +369,7 @@ with the management form: .. code-block:: html+django - +
{{ formset }}
diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 0de5a6b8cb..119e943889 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -172,7 +172,7 @@ Forms are designed to work with the Django template language. In the above example, we passed our ``ContactForm`` instance to the template using the context variable ``form``. Here's a simple example template:: - + {{ form.as_p }}
@@ -183,7 +183,7 @@ The form only outputs its own fields; it is up to you to provide the surrounding ``form.as_p`` will output the form with each form field and accompanying label wrapped in a paragraph. Here's the output for our example template:: -
+

@@ -211,7 +211,7 @@ If the default generated HTML is not to your taste, you can completely customize the way a form is presented using the Django template language. Extending the above example:: - +

{{ form.subject.errors }} @@ -263,7 +263,7 @@ If you're using the same HTML for each of your form fields, you can reduce duplicate code by looping through each field in turn using a ``{% for %}`` loop:: - + {% for field in form %}
{{ field.errors }} @@ -322,7 +322,7 @@ and visible fields independently: ``hidden_fields()`` and ``visible_fields()``. Here's a modification of an earlier example that uses these two methods:: - + {% for field in form.visible_fields %}
@@ -356,7 +356,7 @@ If your site uses the same rendering logic for forms in multiple places, you can reduce duplication by saving the form's loop in a standalone template and using the :ttag:`include` tag to reuse it in other templates:: - + {% include "form_snippet.html" %}

@@ -373,7 +373,7 @@ using the :ttag:`include` tag to reuse it in other templates:: If the form object passed to a template has a different name within the context, you can alias it using the :ttag:`with` tag:: -
+ {% with comment_form as form %} {% include "form_snippet.html" %} {% endwith %} diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 024479508a..7049464f86 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -705,14 +705,14 @@ There are three ways to render a formset in a Django template. First, you can let the formset do most of the work:: - + {{ formset }}
Second, you can manually render the formset, but let the form deal with itself:: -
+ {{ formset.management_form }} {% for form in formset.forms %} {{ form }} @@ -725,7 +725,7 @@ form as shown above. See the :ref:`management form documentation Third, you can manually render each field:: - + {{ formset.management_form }} {% for form in formset.forms %} {% for field in form %} @@ -738,7 +738,7 @@ If you opt to use this third method and you don't iterate over the fields with a ``{% for %}`` loop, you'll need to render the primary key field. For example, if you were rendering the ``name`` and ``age`` fields of a model:: - + {{ formset.management_form }} {% for form in formset.forms %} {{ form.id }} diff --git a/tests/regressiontests/comment_tests/tests/templatetag_tests.py b/tests/regressiontests/comment_tests/tests/templatetag_tests.py index a1187ca732..669caff667 100644 --- a/tests/regressiontests/comment_tests/tests/templatetag_tests.py +++ b/tests/regressiontests/comment_tests/tests/templatetag_tests.py @@ -1,5 +1,6 @@ from django.contrib.comments.forms import CommentForm from django.contrib.comments.models import Comment +from django.contrib.contenttypes.models import ContentType from django.template import Template, Context from regressiontests.comment_tests.models import Article, Author from regressiontests.comment_tests.tests import CommentTestCase @@ -63,3 +64,34 @@ class CommentTemplateTagTests(CommentTestCase): def testGetCommentListFromObject(self): self.testGetCommentList("{% get_comment_list for a as cl %}") + + def testGetCommentPermalink(self): + self.createSomeComments() + t = "{% load comments %}{% get_comment_list for comment_tests.author author.id as cl %}" + t += "{% get_comment_permalink cl.0 %}" + ct = ContentType.objects.get_for_model(Author) + author = Author.objects.get(pk=1) + ctx, out = self.render(t, author=author) + self.assertEqual(out, "/cr/%s/%s/#c2" % (ct.id, author.id)) + + def testGetCommentPermalinkFormatted(self): + self.createSomeComments() + t = "{% load comments %}{% get_comment_list for comment_tests.author author.id as cl %}" + t += "{% get_comment_permalink cl.0 '#c%(id)s-by-%(user_name)s' %}" + ct = ContentType.objects.get_for_model(Author) + author = Author.objects.get(pk=1) + ctx, out = self.render(t, author=author) + self.assertEqual(out, "/cr/%s/%s/#c2-by-Joe Somebody" % (ct.id, author.id)) + + def testRenderCommentList(self, tag=None): + t = "{% load comments %}" + (tag or "{% render_comment_list for comment_tests.article a.id %}") + ctx, out = self.render(t, a=Article.objects.get(pk=1)) + self.assert_(out.strip().startswith("
")) + self.assert_(out.strip().endswith("
")) + + def testRenderCommentListFromLiteral(self): + self.testRenderCommentList("{% render_comment_list for comment_tests.article 1 %}") + + def testRenderCommentListFromObject(self): + self.testRenderCommentList("{% render_comment_list for a %}") + diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py index 5688293647..0a24522d9c 100644 --- a/tests/regressiontests/csrf_tests/tests.py +++ b/tests/regressiontests/csrf_tests/tests.py @@ -13,7 +13,7 @@ from django.template import RequestContext, Template # Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests def post_form_response(): resp = HttpResponse(content=""" -
+
""", mimetype="text/html") return resp diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 10d2ae7df3..79aff35694 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -174,7 +174,7 @@ Content # Test that the console backend can be pointed at an arbitrary stream >>> s = StringIO() ->>> connection = mail.get_connection('django.core.mail.backends.console', stream=s) +>>> connection = mail.get_connection('django.core.mail.backends.console.EmailBackend', stream=s) >>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection) 1 >>> print s.getvalue() @@ -270,7 +270,7 @@ Content True # Test custom backend defined in this suite. ->>> conn = mail.get_connection('regressiontests.mail.custombackend') +>>> conn = mail.get_connection('regressiontests.mail.custombackend.EmailBackend') >>> hasattr(conn, 'test_outbox') True >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) @@ -280,23 +280,23 @@ True 1 # Test backend argument of mail.get_connection() ->>> isinstance(mail.get_connection('django.core.mail.backends.smtp'), smtp.EmailBackend) +>>> isinstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend) True ->>> isinstance(mail.get_connection('django.core.mail.backends.locmem'), locmem.EmailBackend) +>>> isinstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend) True ->>> isinstance(mail.get_connection('django.core.mail.backends.dummy'), dummy.EmailBackend) +>>> isinstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend) True ->>> isinstance(mail.get_connection('django.core.mail.backends.console'), console.EmailBackend) +>>> isinstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend) True >>> tmp_dir = tempfile.mkdtemp() ->>> isinstance(mail.get_connection('django.core.mail.backends.filebased', file_path=tmp_dir), filebased.EmailBackend) +>>> isinstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend) True >>> shutil.rmtree(tmp_dir) >>> isinstance(mail.get_connection(), locmem.EmailBackend) True # Test connection argument of send_mail() et al ->>> connection = mail.get_connection('django.core.mail.backends.console') +>>> connection = mail.get_connection('django.core.mail.backends.console.EmailBackend') >>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0