# HG changeset patch # User Pawel Solyga # Date 1226365376 0 # Node ID 519c298a4f874da1fa60e34e85606c75dd1fa7ff # Parent dcb1f7821b3911a73ec32ac040c661a7a8d56dd6 Move "(required)" text to third column instead of second column in templatetags, it's much more user friendly that way. Add new version of as_table that support our current error messages format, information about required fields and tooltips. Patch by: Pawel Solyga diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/content/css/soc-081108.css --- a/app/soc/content/css/soc-081108.css Mon Nov 10 23:18:06 2008 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,457 +0,0 @@ -/* -Copyright 2008 the Melange authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - */ - -/* - * STANDARD HTML TAGS - */ - -body { - background-color: white; - color: black; - - font-family: Arial, sans-serif; - font-size: medium; - - margin: 8px; - margin-top: 3px; -} - -img { - border: 0; -} - -form { - margin: 0; - padding: 0; -} - -li { - margin-bottom: 0.25em; -} - -/* HEADERS */ -h1 { - font-size: x-large; - margin-top: 0px; -} - -h2 { - font-size: large; -} - -h3 { - font-size: medium; -} - -h4 { - font-size: small; -} - -/* PRE-FORMATTED TEXT */ -pre, code { - color: #007000; - font-family: "bogus font here", monospace; - font-size: 100%; -} - -pre { - border: 1px solid silver; - background-color: #f5f5f5; - padding: 0.5em; - overflow: auto; - margin: 2em; -} - -pre ins { - color: #cc0000; - font-weight: bold; - text-decoration: none; -} - -/* LINKS AND ANCHORS */ -a:link { - color: #0000cc; -} - -a:active { - color: #cc0000; -} - -a:visited { - color: #551a8b; -} - -a.selected, .selected a, .selected { - color: black; - font-weight: bold; - text-decoration: none; -} - -a.novisit { - color: #2a55a3; -} - -a.noul, a.noulv { - color: #4182fa; /* #93b7fa; */ - text-decoration: none; -} - -a:hover.noul { - text-decoration: underline; -} - -a:visited.noul { - color: #a32a91; /* #2a55a3; */ -} - - -/* Styles used by Django Forms */ -ul.errorlist { - color: #FF0000; - font-size: small; -} - - -/* TABLES */ -table { - border-collapse: collapse; -} - -th, td { - /*padding: 0;*/ - padding:2px 5px; - vertical-align: top; - text-align: left; -} - -/* FORM TABLE FIELDS */ -td.formfieldrequired { - font-style: italic; - font-size: small; -} - -td.formfieldhelptext { - font-style: italic; - font-size: small; -} - -td.formfielderror { - color: #FF0000; - font-size: small; -} - -td.formfieldheading { - font-weight: bold; - font-size: small; -} - -td.formfieldlabel { - font-weight: bold; - font-size: small; -} - -td.formfielderrorlabel { - font-weight: bold; - color: #FF0000; - font-size: small; -} - -/* TABLE QUEUES (used with .list) */ -table#queues { - border-collapse: collapse; - width: 100%; -} - -table#queues tr { - border-bottom: thin solid lightgray; -} - -table#queues td { - padding: 2px; -} - -/* - * PAGE ELEMENTS - */ - -#title { - border-top: 1px solid #3366cc; - background-color: #e5ecf9; - font-size: large; - font-weight: bold; - margin: 0; - padding: 0; - padding-top: 1px; - padding-bottom: 1px; - margin-top: 5px; - margin-left: 200px; - padding-left: 3px; -} - -#notice { - margin-left: 200px; - padding: 3px; -} - -#logo { - padding-right: 18px; - position: absolute; - left: 0; - top: -5px; -} - -#login { - text-align: right; -} - -#badge { - clear: both; - margin-top: 3.5em; - margin-bottom: 1em; - height: 53px; - font-style: italic; -} - -#body { - border-left: 1px dotted silver; - margin-left: 200px; - margin-right: 25px; - padding-left: 18px; - padding-bottom: 25px; -} - -#body .buttons { - margin-right: 4px; - margin-top: 20px; -} - -#body a.button, input[type^="submit"], input[type^="button"] { - margin: 0; - padding: 2px 5px 2px 5px; - font-family: Arial, Sans-serif; - font-size: 12px; - text-decoration: none; - color: #222; - cursor: default; - background: #ddd url("/soc/content/images/button-background.gif") repeat-x 0 0; - border: 1px solid #aaa; -} - -#body a.button:hover, input[type^="submit"]:hover, input[type^="button"]:hover { - border-color: #9cf #69e #69e #7af; -} - -#header { - height: 50px; - margin-bottom: 11px; - position: relative; -} - -#footer { - clear: both; - text-align: center; - margin-top: 3.5em; - margin-bottom: 1em; - background-image: url("http://www.google.com/images/art.gif"); - height: 53px; - background-repeat: no-repeat; - background-position: left center; -} - -#footer .text { - padding-top: 20px; -} - -#created { - font-size: small; -} - -/* SIDEBAR MENU */ -#side { - width: 200px; - margin-bottom: 3em; - float: left; - font-size: small; -} - -#menu ul { - margin: 0; - padding: 0; - list-style-type: none; - margin-bottom: 1em; - font-size: 95%; -} - -#menu ul ul { - margin-left: 10px; - margin-bottom: 0; -} - -#menu li { - margin-top: 4px; - list-style-type: none; - list-style-image: none; -} - -#menu img { - margin-right: 4px; -} - -#menu li.leaf { - padding-left: 14px; -} - -#menu h4 { - margin: 0; - padding: 0; - margin-bottom: 1em; -} - - -/* SEARCH FIELD */ -#search { - margin-top: 2em; -} - -#search .header { - font-weight: bold; - font-size: 90%; - margin-bottom: 1px; -} - -#search .button { - margin-top: 1px; -} - -#search .input input { - width: 125px; -} - -/* BLOG FEEDS */ -.blog { - border: 10px solid #e5ecf9; - border-top: 1px solid #3366cc; -} - -.blog h2 { - margin-top: 0.1em; - background-color: #e5ecf9; -} - -.blog h2 a { - text-decoration: none; - color: black; -} - -.blog h2 a:visited { - text-decoration: none; - color: black; -} - - -.blog .entry { - margin-bottom: 1em; -} - -.blog .title { - font-size: medium; -} - -.blog .author { - color: gray; - margin-bottom: 0.5em; -} - -.blog .snippet { - background-color: white; -} - -/* LIST */ -.list { - background-color: #E5ECF9; - border: 1px solid #93b7fa; - border-bottom: 2px solid #93b7fa; - padding: 3px; - -moz-border-radius: 5px 5px 0px 0px; -} - -.list .pagination { - text-align: right; - padding: 3px; -} - -.list table{ - background-color: white; -} - -.list table th { - background-color: #eeeeec; - border-right: 1px solid lightgray; - border-top: 1px solid lightgray; -} - -.list table tr.on { - background-color: #ff9; -} - -.list table tr.off { - background-color: #fff; -} - -.list table td.last { - border-right: 1px solid lightgray; -} - -.list table .first { - border-left: 1px solid lightgray; -} - -/* - * CUSTOM CLASSES - */ - -.todo { - color: #cc0000; - font-size: 80%; -} - -.newmark { - color: red; - font-size: 80%; - vertical-align: top; -} - -.error { - color: red; -} - -.notice { - background:#fad163; - font-size: small; - font-weight: bold; -} - -.rounded_ul { background: url(/soc/content/images/ul.gif) no-repeat top left; } -.rounded_ur { background: url(/soc/content/images/ur.gif) no-repeat top right; } -.rounded_ll { background: url(/soc/content/images/ll.gif) no-repeat bottom left; } -.rounded_lr { background: url(/soc/content/images/lr.gif) no-repeat bottom right; } - -/* Disabled text. */ -.disabled { - color: gray; -} - - diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/content/css/soc-081111.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/soc/content/css/soc-081111.css Tue Nov 11 01:02:56 2008 +0000 @@ -0,0 +1,457 @@ +/* +Copyright 2008 the Melange authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +/* + * STANDARD HTML TAGS + */ + +body { + background-color: white; + color: black; + + font-family: Arial, sans-serif; + font-size: medium; + + margin: 8px; + margin-top: 3px; +} + +img { + border: 0; +} + +form { + margin: 0; + padding: 0; +} + +li { + margin-bottom: 0.25em; +} + +/* HEADERS */ +h1 { + font-size: x-large; + margin-top: 0px; +} + +h2 { + font-size: large; +} + +h3 { + font-size: medium; +} + +h4 { + font-size: small; +} + +/* PRE-FORMATTED TEXT */ +pre, code { + color: #007000; + font-family: "bogus font here", monospace; + font-size: 100%; +} + +pre { + border: 1px solid silver; + background-color: #f5f5f5; + padding: 0.5em; + overflow: auto; + margin: 2em; +} + +pre ins { + color: #cc0000; + font-weight: bold; + text-decoration: none; +} + +/* LINKS AND ANCHORS */ +a:link { + color: #0000cc; +} + +a:active { + color: #cc0000; +} + +a:visited { + color: #551a8b; +} + +a.selected, .selected a, .selected { + color: black; + font-weight: bold; + text-decoration: none; +} + +a.novisit { + color: #2a55a3; +} + +a.noul, a.noulv { + color: #4182fa; /* #93b7fa; */ + text-decoration: none; +} + +a:hover.noul { + text-decoration: underline; +} + +a:visited.noul { + color: #a32a91; /* #2a55a3; */ +} + + +/* Styles used by Django Forms */ +ul.errorlist { + color: #FF0000; + font-size: small; +} + + +/* TABLES */ +table { + border-collapse: collapse; +} + +th, td { + /*padding: 0;*/ + padding:2px 5px; + vertical-align: top; + text-align: left; +} + +/* FORM FIELDS */ +td.formfieldrequired { + font-style: italic; + font-size: small; +} + +td.formfieldhelptext { + font-style: italic; + font-size: small; +} + +td.formfielderror { + color: #FF0000; + font-size: small; +} + +td.formfieldheading { + font-weight: bold; + font-size: small; +} + +td.formfieldlabel { + font-weight: bold; + font-size: small; +} + +td.formfielderrorlabel, span.formfielderrorlabel { + font-weight: bold; + color: #FF0000; + font-size: small; +} + +/* TABLE QUEUES (used with .list) */ +table#queues { + border-collapse: collapse; + width: 100%; +} + +table#queues tr { + border-bottom: thin solid lightgray; +} + +table#queues td { + padding: 2px; +} + +/* + * PAGE ELEMENTS + */ + +#title { + border-top: 1px solid #3366cc; + background-color: #e5ecf9; + font-size: large; + font-weight: bold; + margin: 0; + padding: 0; + padding-top: 1px; + padding-bottom: 1px; + margin-top: 5px; + margin-left: 200px; + padding-left: 3px; +} + +#notice { + margin-left: 200px; + padding: 3px; +} + +#logo { + padding-right: 18px; + position: absolute; + left: 0; + top: -5px; +} + +#login { + text-align: right; +} + +#badge { + clear: both; + margin-top: 3.5em; + margin-bottom: 1em; + height: 53px; + font-style: italic; +} + +#body { + border-left: 1px dotted silver; + margin-left: 200px; + margin-right: 25px; + padding-left: 18px; + padding-bottom: 25px; +} + +#body .buttons { + margin-right: 4px; + margin-top: 20px; +} + +#body a.button, input[type^="submit"], input[type^="button"] { + margin: 0; + padding: 2px 5px 2px 5px; + font-family: Arial, Sans-serif; + font-size: 12px; + text-decoration: none; + color: #222; + cursor: default; + background: #ddd url("/soc/content/images/button-background.gif") repeat-x 0 0; + border: 1px solid #aaa; +} + +#body a.button:hover, input[type^="submit"]:hover, input[type^="button"]:hover { + border-color: #9cf #69e #69e #7af; +} + +#header { + height: 50px; + margin-bottom: 11px; + position: relative; +} + +#footer { + clear: both; + text-align: center; + margin-top: 3.5em; + margin-bottom: 1em; + background-image: url("http://www.google.com/images/art.gif"); + height: 53px; + background-repeat: no-repeat; + background-position: left center; +} + +#footer .text { + padding-top: 20px; +} + +#created { + font-size: small; +} + +/* SIDEBAR MENU */ +#side { + width: 200px; + margin-bottom: 3em; + float: left; + font-size: small; +} + +#menu ul { + margin: 0; + padding: 0; + list-style-type: none; + margin-bottom: 1em; + font-size: 95%; +} + +#menu ul ul { + margin-left: 10px; + margin-bottom: 0; +} + +#menu li { + margin-top: 4px; + list-style-type: none; + list-style-image: none; +} + +#menu img { + margin-right: 4px; +} + +#menu li.leaf { + padding-left: 14px; +} + +#menu h4 { + margin: 0; + padding: 0; + margin-bottom: 1em; +} + + +/* SEARCH FIELD */ +#search { + margin-top: 2em; +} + +#search .header { + font-weight: bold; + font-size: 90%; + margin-bottom: 1px; +} + +#search .button { + margin-top: 1px; +} + +#search .input input { + width: 125px; +} + +/* BLOG FEEDS */ +.blog { + border: 10px solid #e5ecf9; + border-top: 1px solid #3366cc; +} + +.blog h2 { + margin-top: 0.1em; + background-color: #e5ecf9; +} + +.blog h2 a { + text-decoration: none; + color: black; +} + +.blog h2 a:visited { + text-decoration: none; + color: black; +} + + +.blog .entry { + margin-bottom: 1em; +} + +.blog .title { + font-size: medium; +} + +.blog .author { + color: gray; + margin-bottom: 0.5em; +} + +.blog .snippet { + background-color: white; +} + +/* LIST */ +.list { + background-color: #E5ECF9; + border: 1px solid #93b7fa; + border-bottom: 2px solid #93b7fa; + padding: 3px; + -moz-border-radius: 5px 5px 0px 0px; +} + +.list .pagination { + text-align: right; + padding: 3px; +} + +.list table{ + background-color: white; +} + +.list table th { + background-color: #eeeeec; + border-right: 1px solid lightgray; + border-top: 1px solid lightgray; +} + +.list table tr.on { + background-color: #ff9; +} + +.list table tr.off { + background-color: #fff; +} + +.list table td.last { + border-right: 1px solid lightgray; +} + +.list table .first { + border-left: 1px solid lightgray; +} + +/* + * CUSTOM CLASSES + */ + +.todo { + color: #cc0000; + font-size: 80%; +} + +.newmark { + color: red; + font-size: 80%; + vertical-align: top; +} + +.error { + color: red; +} + +.notice { + background:#fad163; + font-size: small; + font-weight: bold; +} + +.rounded_ul { background: url(/soc/content/images/ul.gif) no-repeat top left; } +.rounded_ur { background: url(/soc/content/images/ur.gif) no-repeat top right; } +.rounded_ll { background: url(/soc/content/images/ll.gif) no-repeat bottom left; } +.rounded_lr { background: url(/soc/content/images/lr.gif) no-repeat bottom right; } + +/* Disabled text. */ +.disabled { + color: gray; +} + + diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/templates/soc/base.html --- a/app/soc/templates/soc/base.html Mon Nov 10 23:18:06 2008 +0000 +++ b/app/soc/templates/soc/base.html Tue Nov 11 01:02:56 2008 +0000 @@ -16,7 +16,7 @@ {% block stylesheet %} - + {% endblock %} {% block page_title %} diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/templates/soc/templatetags/_field_as_table_row.html --- a/app/soc/templates/soc/templatetags/_field_as_table_row.html Mon Nov 10 23:18:06 2008 +0000 +++ b/app/soc/templates/soc/templatetags/_field_as_table_row.html Tue Nov 11 01:02:56 2008 +0000 @@ -28,12 +28,12 @@ <label for="{{ field.auto_id }}">{{ field.label }}: </label> </td> + <td> + {{ field }} + </td> <td class="formfieldrequired"> {% if field.field.required %}(required){% endif %} </td> - <td> - {{ field }} - </td> </tr> diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/templates/soc/templatetags/_readonly_field_as_table_row.html --- a/app/soc/templates/soc/templatetags/_readonly_field_as_table_row.html Mon Nov 10 23:18:06 2008 +0000 +++ b/app/soc/templates/soc/templatetags/_readonly_field_as_table_row.html Tue Nov 11 01:02:56 2008 +0000 @@ -15,7 +15,6 @@ <td class="formfieldlabel"> {{ field_label }}: </td> - <td> </td> <td class="formfieldvalue"> {{ field_value }} </td> diff -r dcb1f7821b39 -r 519c298a4f87 app/soc/views/helper/forms.py --- a/app/soc/views/helper/forms.py Mon Nov 10 23:18:06 2008 +0000 +++ b/app/soc/views/helper/forms.py Tue Nov 11 01:02:56 2008 +0000 @@ -20,13 +20,38 @@ __authors__ = [ '"Chen Lunpeng" <forever.clp@gmail.com>', '"Todd Larsen" <tlarsen@google.com>', + '"Pawel Solyga" <pawel.solyga@gmail.com>', ] from google.appengine.ext.db import djangoforms from django import forms +from django.forms import forms as forms_in +from django.forms import util +from django.utils import encoding from django.utils import safestring +from django.utils.encoding import force_unicode +from django.utils.html import escape +from django.utils.safestring import mark_safe + + +class CustomErrorList(util.ErrorList): + """A collection of errors that knows how to display itself in various formats. + + This class has customized as_text method output which puts errors inside <span> + with formfielderrorlabel class. + """ + def __unicode__(self): + return self.as_text() + + def as_text(self): + """Returns error list rendered as text inside <span>.""" + if not self: + return u'' + errors_text = u'\n'.join([u'%s' % encoding.force_unicode(e) for e in self]) + return u'<span class="formfielderrorlabel">%(errors)s</span><br />' % \ + {'errors': errors_text} class DbModelForm(djangoforms.ModelForm): @@ -82,13 +107,14 @@ prints itself also has changed. Help text is displayed in the same row as label and input. """ - # TODO(pawel.solyga): Add class names for form errors and required fields. - DEF_NORMAL_ROW = u'<tr><td class="formfieldlabel">%(label)s</td>' \ - '<td>%(errors)s%(field)s%(help_text)s</td></tr>' - DEF_ERROR_ROW = u'<tr><td colspan="2">%s</td></tr>' + DEF_NORMAL_ROW = u'<tr title="%(help_text)s"><td class=' \ + '"%(field_class_type)s">%(label)s</td><td>' \ + '%(errors)s%(field)s%(required)s</td></tr>' + DEF_ERROR_ROW = u'<tr><td> </td><td class="formfielderror">%s</td></tr>' DEF_ROW_ENDER = '</td></tr>' - DEF_HELP_TEXT_HTML = u'<td class="formfieldhelptext">%s</td>' + DEF_REQUIRED_HTML = u'<td class="formfieldrequired">(required)</td>' + DEF_HELP_TEXT_HTML = u'%s' def __init__(self, *args, **kwargs): """Parent class initialization. @@ -96,14 +122,98 @@ Args: *args, **kwargs: passed through to parent __init__() constructor """ - super(BaseForm, self).__init__(*args, **kwargs) + super(BaseForm, self).__init__(error_class=CustomErrorList, *args, **kwargs) + + def _html_output_with_required(self, normal_row, error_row, row_ender, + help_text_html, required_html, errors_on_separate_row): + """Helper function for outputting HTML. + + Used by as_table(), as_ul(), as_p(). Displays information + about required fields. + """ + # Errors that should be displayed above all fields. + top_errors = self.non_field_errors() + output, hidden_fields = [], [] + for name, field in self.fields.items(): + bf = forms_in.BoundField(self, field, name) + # Escape and cache in local variable. + bf_errors = self.error_class([escape(error) for error in bf.errors]) + if bf.is_hidden: + if bf_errors: + top_errors.extend([u'(Hidden field %s) %s' % \ + (name, force_unicode(e)) for e in bf_errors]) + hidden_fields.append(unicode(bf)) + else: + if errors_on_separate_row and bf_errors: + output.append(error_row % force_unicode(bf_errors)) + + if bf.label: + label = escape(force_unicode(bf.label)) + # Only add the suffix if the label does not end in + # punctuation. + if self.label_suffix: + if label[-1] not in ':?.!': + label += self.label_suffix + label = bf.label_tag(label) or '' + else: + label = '' + if field.help_text: + help_text = help_text_html % force_unicode(field.help_text) + else: + help_text = u'' + + if bf_errors: + field_class_type = u'formfielderrorlabel' + else: + field_class_type = u'formfieldlabel' + + if field.required: + required = required_html + else: + required = u'' + + if errors_on_separate_row and bf_errors: + errors = u'' + else: + errors = force_unicode(bf_errors) + + output.append(normal_row % {'field_class_type': field_class_type, + 'errors': errors, + 'label': force_unicode(label), + 'field': unicode(bf), + 'required': required, + 'help_text': help_text}) + if top_errors: + output.insert(0, error_row % force_unicode(top_errors)) + if hidden_fields: # Insert any hidden fields in the last row. + str_hidden = u''.join(hidden_fields) + if output: + last_row = output[-1] + # Chop off the trailing row_ender (e.g. '</td></tr>') and + # insert the hidden fields. + if not last_row.endswith(row_ender): + # This can happen in the as_p() case (and possibly others + # that users write): if there are only top errors, we may + # not be able to conscript the last row for our purposes, + # so insert a new, empty row. + last_row = normal_row % {'errors': '', 'label': '', + 'field': '', 'help_text': ''} + output.append(last_row) + output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + else: + # If there aren't any rows in the output, just append the + # hidden fields. + output.append(str_hidden) + return mark_safe(u'\n'.join(output)) def as_table(self): """Returns form rendered as HTML <tr> rows -- with no <table></table>.""" - return self._html_output(self.DEF_NORMAL_ROW, - self.DEF_ERROR_ROW, - self.DEF_ROW_ENDER, - self.DEF_HELP_TEXT_HTML, False) + + return self._html_output_with_required(self.DEF_NORMAL_ROW, + self.DEF_ERROR_ROW, + self.DEF_ROW_ENDER, + self.DEF_HELP_TEXT_HTML, + self.DEF_REQUIRED_HTML, True) class SelectQueryArgForm(forms.Form):