Major revamp of Person model, splitting up poorly-validated properties into
authorTodd Larsen <tlarsen@google.com>
Wed, 28 May 2008 20:52:28 +0000
changeset 39 fa3545f99c02
parent 38 9a6ee3ab1446
child 40 c0151a1f62e5
Major revamp of Person model, splitting up poorly-validated properties into StringProperties, etc. Patch by: Todd Larsen Review by: Pawel Solyga Review issue: 261 Review URL: http://codereviews.googleopensourceprograms.com/261
apps/proto/settings.py
soc/models/person.py
soc/templates/soc/person/profile.html
soc/views/person.py
--- a/apps/proto/settings.py	Fri May 23 20:56:02 2008 +0000
+++ b/apps/proto/settings.py	Wed May 28 20:52:28 2008 +0000
@@ -98,6 +98,7 @@
 )
 
 INSTALLED_APPS = (
+    'soc.views.helpers',
 #    'django.contrib.auth',
 #    'django.contrib.contenttypes',
 #    'django.contrib.sessions',
--- a/soc/models/person.py	Fri May 23 20:56:02 2008 +0000
+++ b/soc/models/person.py	Wed May 28 20:52:28 2008 +0000
@@ -5,9 +5,9 @@
 # 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.
@@ -22,10 +22,13 @@
 ]
 
 from google.appengine.ext import db
+from django.utils.translation import ugettext_lazy
 
 from soc import models
 import soc.models.user
 
+from soc.models import countries
+
 
 class Person(db.Model):
   """Common data fields for all Roles.
@@ -36,14 +39,14 @@
   new Program.
 
   Some details of a Person are considered "public" information, and nearly
-  all of these are optional (except for givenname, surname, and email).
+  all of these are optional (except for given_name, surname, and email).
   Other details of a Person are kept "private" and are only provided to
   other Persons in roles that "need to know" this information.  How these
   fields are revealed is usually covered by Program terms of service.
 
   A Person entity participates in the following relationships implemented
   as a db.ReferenceProperty elsewhere in another db.Model:
-     
+
    author)  a 1:1 relationship of Person details for a specific Author.
      This relation is implemented as the 'author' back-reference Query of
      the Author model 'person' reference.
@@ -52,7 +55,6 @@
      with the Person by Administrators.  This relation is implemented as
      the 'docs' back-reference Query of the Documentation model 'person'
      reference.
-     
   """
 
   #: A required many:1 relationship that ties (possibly multiple
@@ -60,7 +62,7 @@
   #: exist unassociated from a login identity and credentials.  The
   #: back-reference in the User model is a Query named 'persons'.
   user = db.ReferenceProperty(reference_class=models.user.User,
-                              required=True, collection_name="persons")
+                              required=True, collection_name='persons')
 
   #====================================================================
   #  (public) name information
@@ -68,82 +70,200 @@
 
   #: Required field storing the parts of the Person's name
   #: corresponding to the field names; displayed publicly.
-  #: Givenname can only be lower ASCII, not UTF-8 text, because it is
-  #: used, for example, as part of the shipping (mailing) address. 
-  givenname = db.StringProperty(required=True)
+  #: given_name can only be lower ASCII, not UTF-8 text, because it is
+  #: used, for example, as part of the shipping (mailing) address.
+  given_name = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('First (given) name'))
+  given_name.help_text = ugettext_lazy('lower ASCII characters only')
 
-  #: Required field storing the parts of the Person's name 
+  #: Required field storing the parts of the Person's name
   #: corresponding to the field names; displayed publicly.
   #: Surname can only be lower ASCII, not UTF-8 text, because it is
-  #: used, for example, as part of the shipping (mailing) address. 
-  surname = db.StringProperty(required=True)  # last name
+  #: used, for example, as part of the shipping (mailing) address.
+  surname = db.StringProperty(
+      required=True,
+      verbose_name=ugettext_lazy('Last (family) name'))
+  surname.help_text = ugettext_lazy('lower ASCII characters only')
 
   #: Optional field storing a nickname; displayed publicly.
-  #: Nicknames can be any valid UTF-8 text. 
-  nickname = db.StringProperty()
-  
-  #: optional field used as a display name, such as for awards
-  #: certificates. Should be the entire display name in the format 
+  #: Nicknames can be any valid UTF-8 text.
+  nickname = db.StringProperty(
+      verbose_name=ugettext_lazy('Nick name'))
+
+  #: Optional field used as a display name, such as for awards
+  #: certificates. Should be the entire display name in the format
   #: the Person would like it displayed (could be surname followed by
   #: given name in some cultures, for example). Display names can be
   #: any valid UTF-8 text.
-  displayname = db.StringProperty() 
+  display_name = db.StringProperty(
+      verbose_name=ugettext_lazy('Display Name'))
+  display_name.help_text = ugettext_lazy(
+      'Optional field used as a display name, such as for awards '
+      'certificates. Should be the entire display name in the format '
+      'the person would like it displayed (could be family name followed '
+      'by given name in some cultures, for example). Display names can be '
+      'any valid UTF-8 text.')
 
   #====================================================================
   #  (public) contact information
   #====================================================================
 
-  #: Required field used as the "public" contact mechanism for the
+  #: Required field used as the 'public' contact mechanism for the
   #: Person (as opposed to the user.id email address which is
   #: kept secret).
-  email = db.EmailProperty(required=True)
+  email = db.EmailProperty(
+      required=True,
+      verbose_name=ugettext_lazy('Email Address'))
 
-  #: Optional field storing Instant Messaging network contact
-  #: information; displayed publicly.
-  im = db.IMProperty()
+  #: Optional field storing Instant Messaging network; displayed publicly.
+  im_network = db.StringProperty(
+      verbose_name=ugettext_lazy('IM Network'))
+  im_network.help_text=ugettext_lazy(
+      'examples: irc:irc.freenode.org xmpp:gmail.com/Home')
+
+  #: Optional field storing Instant Messaging handle; displayed publicly.
+  im_handle = db.StringProperty(
+      verbose_name=ugettext_lazy('IM Handle'))
+  im_handle.help_text=ugettext_lazy(
+      'personal identifier, such as: screen name, IRC nick, user name')
 
   #: Optional field storing a home page URL; displayed publicly.
-  homepage = db.LinkProperty()
+  home_page = db.LinkProperty(
+      verbose_name=ugettext_lazy('Home Page URL'))
 
   #: Optional field storing a blog URL; displayed publicly.
-  blog = db.LinkProperty()
+  blog = db.LinkProperty(
+      verbose_name=ugettext_lazy('Blog URL'))
 
   #: Optional field storing a URL to an image, expected to be a
   #: personal photo (or cartoon avatar, perhaps); displayed publicly.
-  photo = db.LinkProperty()
+  photo_url = db.LinkProperty(
+      verbose_name=ugettext_lazy('Thumbnail Photo URL'))
+  photo_url.help_text = ugettext_lazy(
+      'URL of 64x64 pixel thumbnail image')
 
-  #: Optional field storing the latitude and longitude provided by
-  #: the Person; displayed publicly.
-  location = db.GeoPtProperty()
+  #: Optional field storing the latitude provided by the Person; displayed
+  #: publicly.
+  latitude = db.FloatProperty(
+      verbose_name=ugettext_lazy('Latitude'))
+  latitude.help_text = ugettext_lazy(
+      'decimal degrees northerly (N), use minus sign (-) for southerly (S)')
+
+  #: Optional field storing the longitude provided by the Person; displayed
+  #: publicly.
+  longitude = db.FloatProperty(
+      verbose_name=ugettext_lazy('Longitude'))
+  longitude.help_text = ugettext_lazy(
+      'decimal degrees easterly (E), use minus sign (-) for westerly (W)')
 
   #====================================================================
   # (private) contact information
   #====================================================================
 
-  #: Required field containing residence address; kept private.
-  residence = db.PostalAddressProperty(required=True)
+  #: Required field containing residence street address; kept private.
+  #: Residence street address can only be lower ASCII, not UTF-8 text, because
+  #: it may be used as a shipping address.
+  res_street = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('Street address'))
+  res_street.help_text = ugettext_lazy(
+      'street number and name, lower ASCII characters only')
+
+  #: Required field containing residence address city; kept private.
+  #: Residence city can only be lower ASCII, not UTF-8 text, because it
+  #: may be used as a shipping address.
+  res_city = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('City'))
+  res_city.help_text = ugettext_lazy('lower ASCII characters only')
+
+  #: Required field containing residence address state or province; kept
+  #: private.  Residence state/province can only be lower ASCII, not UTF-8
+  #: text, because it may be used as a shipping address.
+  res_state = db.StringProperty(
+      verbose_name=ugettext_lazy('State/Province'))
+  res_state.help_text = ugettext_lazy(
+      'optional if country/territory does not have states or provinces, '
+      'lower ASCII characters only')
+
+  #: Required field containing residence address country or territory; kept
+  #: private.
+  res_country = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('Country/Territory'),
+      choices=countries.COUNTRIES_AND_TERRITORIES)
+
+  #: Required field containing residence address postal code (ZIP code in
+  #: the United States); kept private.  Residence postal code can only be
+  #: lower ASCII, not UTF-8 text, because it may be used as a shipping address.
+  res_postalcode = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('ZIP/Postal Code'))
+  res_postalcode.help_text=ugettext_lazy('lower ASCII characters only')
 
-  #: Optional field containg a separate shipping; kept private.
-  shipping = db.PostalAddressProperty()
+  #: Optional field containing a separate shipping street address; kept
+  #: private.  If shipping address is not present in its entirety, the
+  #: residence address will be used instead.  Shipping street address can only
+  #: be lower ASCII, not UTF-8 text, because, if supplied, it is used as a
+  #: shipping address.
+  ship_street = db.StringProperty(
+      verbose_name=ugettext_lazy('Street address'))
+  ship_street.help_text = ugettext_lazy(
+      'street number and name, lower ASCII characters only')
+
+  #: Optional field containing shipping address city; kept private.
+  #: Shipping city can only be lower ASCII, not UTF-8 text, because, if
+  #: supplied, it is used as a shipping address.
+  ship_city = db.StringProperty(
+      verbose_name=ugettext_lazy('City'))
+  ship_city.help_text = ugettext_lazy('lower ASCII characters only')
+
+  #: Optional field containing shipping address state or province; kept
+  #: private.  Shipping state/province can only be lower ASCII, not UTF-8
+  #: text, because, if supplied, it is used as a shipping address.
+  ship_state = db.StringProperty(
+      verbose_name=ugettext_lazy('State/Province'))
+  ship_state.help_text = ugettext_lazy(
+      'optional if country/territory does not have states or provinces, '
+      'lower ASCII characters only')
+
+  #: Optional field containing shipping address country or territory; kept
+  #: private.
+  ship_country = db.StringProperty(
+      verbose_name=ugettext_lazy('Country/Territory'),
+      choices=countries.COUNTRIES_AND_TERRITORIES)
+
+  #: Optional field containing shipping address postal code (ZIP code in
+  #: the United States); kept private.  Shipping postal code can only be
+  #: lower ASCII, not UTF-8 text, because, if supplied, it is used as a
+  #: shipping address.
+  ship_postalcode = db.StringProperty(
+      verbose_name=ugettext_lazy('ZIP/Postal Code'))
+  ship_postalcode.help_text=ugettext_lazy('lower ASCII characters only')
 
   #: Required field containing a phone number that will be supplied
   #: to shippers; kept private.
-  phone = db.PhoneNumberProperty(required=True) 
-  
+  phone = db.PhoneNumberProperty(
+      required=True,
+      verbose_name=ugettext_lazy('Phone Number'))
+  phone.help_text = ugettext_lazy(
+      'include complete international calling number with country code')
+
   #====================================================================
   # (private) personal information
   #====================================================================
 
-  #: Required field containing the Person's birthdate (for 
+  #: Required field containing the Person's birthdate (for
   #: determining Program participation eligibility); kept private.
-  birthdate = db.DateProperty(required=True)
+  birth_date = db.DateProperty(
+      required=True,
+      verbose_name=ugettext_lazy('Birth Date'))
+  birth_date.help_text = ugettext_lazy(
+      'required for determining program eligibility')
 
-  #: Optional field indicating choice of t-shirt, from XXS to XXXL; 
+  #: Optional field indicating choice of t-shirt, from XXS to XXXL;
   #: kept private.
-  tshirtsize = db.StringProperty(
-      choices=set(("XXS", "XS", "S", "M", "L", "XL", "XXL", "XXXL")))
+  tshirt_size = db.StringProperty(
+      verbose_name=ugettext_lazy('T-shirt Size'),
+      choices=('XXS', 'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'))
 
-  #: Optional field indicating choice of male or t-shirt
-  #: fit; kept private.
-  tshirt_gender = db.StringProperty(choices=set(("male", "female")))
-
+  #: Optional field indicating choice of t-shirt fit; kept private.
+  tshirt_style = db.StringProperty(
+      verbose_name=ugettext_lazy('T-shirt Style'),
+      choices=('male', 'female'))
--- a/soc/templates/soc/person/profile.html	Fri May 23 20:56:02 2008 +0000
+++ b/soc/templates/soc/person/profile.html	Wed May 28 20:52:28 2008 +0000
@@ -11,6 +11,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 {% endcomment %}
+{% load forms_helpers %}
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
 <html lang="en">
  <head>
@@ -20,11 +21,83 @@
  <body>
   <p>
    {% block 'logo' %}{% endblock %}
-   Welcome, {{user.nickname}}. Please update your profile:
+   Welcome, {{user.nickname}}.
+   {% block 'instructions' %}Please update your profile:{% endblock %}
    <form method="POST">
-    <table>{{form.as_table}}</table>
+    <table>
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Name <i>(publicly displayed)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.given_name %}
+     {% field_as_table_row form.surname %}
+     {% field_as_table_row form.nickname %}
+     {% field_as_table_row form.display_name %}
+     <tr><td colspan="4">&nbsp;</td></tr>
+
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Online Profile <i>(publicly displayed)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.email %}
+     {% field_as_table_row form.im_network %}
+     {% field_as_table_row form.im_handle %}
+     {% field_as_table_row form.home_page %}
+     {% field_as_table_row form.blog %}
+     {% field_as_table_row form.photo_url %}
+     <tr><td colspan="4">&nbsp;</td></tr>
+
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Location <i>(publicly displayed, these will be replaced with a
+                    Google Maps control)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.latitude %}
+     {% field_as_table_row form.longitude %}
+     <tr><td colspan="4">&nbsp;</td></tr>
+
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Residence Address <i>(kept private)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.res_street %}
+     {% field_as_table_row form.res_city %}
+     {% field_as_table_row form.res_state %}
+     {% field_as_table_row form.res_postalcode %}
+     {% field_as_table_row form.res_country %}
+     <tr><td colspan="4">&nbsp;</td></tr>
+
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Shipping Address <i>(kept private; optional, if omitted, Residence
+                            Address will be used)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.ship_street %}
+     {% field_as_table_row form.ship_city %}
+     {% field_as_table_row form.ship_state %}
+     {% field_as_table_row form.ship_postalcode %}
+     {% field_as_table_row form.ship_country %}
+
+     <tr><td colspan="4">&nbsp;</td></tr>
+
+     <tr>
+      <td class="formfieldheading" colspan="4">
+       Personal Information <i>(kept private)</i>
+      </td>
+     </tr>
+     {% field_as_table_row form.phone %}
+     {% field_as_table_row form.birth_date %}
+     {% field_as_table_row form.tshirt_size %}
+     {% field_as_table_row form.tshirt_style %}
+    </table>
     <input type="submit" />
    </form>
   </p>
  </body>
 </html>
+
--- a/soc/views/person.py	Fri May 23 20:56:02 2008 +0000
+++ b/soc/views/person.py	Wed May 28 20:52:28 2008 +0000
@@ -23,23 +23,26 @@
 
 
 from google.appengine.api import users
-from google.appengine.ext.db import djangoforms
 from django import http
 from django import shortcuts
 from django import newforms as forms
 
 from soc.models import person
+from soc.views.helpers import forms_helpers
 
 
-class ProfileForm(djangoforms.ModelForm):
+class ProfileForm(forms_helpers.DbModelForm):
+  """Django form displayed when creating or editing a Person.
+  """
+
   class Meta:
     """Inner Meta class that defines some behavior for the form.
+    """
+    #: db.Model subclass for which the form will gather information
+    model = person.Person
 
-    """
-    #: the db.Model subclass for which the form will gather information.
-    model = person.Person
-    #: the list of model fields which will *not* be gathered by the form.
-    exclude = ['user', ]
+    #: list of model fields which will *not* be gathered by the form
+    exclude = ['user']
 
 
 def profile(request, template='soc/person/profile.html'):
@@ -56,10 +59,13 @@
   user = users.get_current_user()
   if not user:
     return http.HttpResponseRedirect(users.create_login_url(request.path))
+
   form = ProfileForm()
   if request.method=='POST':
     form = ProfileForm(request.POST)
+
     if not form.errors:
       return http.HttpResponse('This would update the model')
+
   return shortcuts.render_to_response(
       template, dictionary={'template': template, 'form': form, 'user': user})