...
authorTodd Larsen <tlarsen@google.com>
Fri, 12 Sep 2008 02:12:38 +0000
changeset 131 3db97cf7f2c7
parent 130 63248d9db484
child 132 15d89c284106
...
app/soc/logic/site/id_user.py
app/soc/models/user.py
app/soc/views/helpers/response_helpers.py
app/soc/views/user/profile.py
--- a/app/soc/logic/site/id_user.py	Thu Sep 11 21:30:08 2008 +0000
+++ b/app/soc/logic/site/id_user.py	Fri Sep 12 02:12:38 2008 +0000
@@ -22,13 +22,28 @@
   ]
 
 
+import re
+
 from google.appengine.api import users
+from google.appengine.ext import db
 
 from soc.logic import out_of_band
 
 import soc.models.user
 
 
+def getUserKeyNameFromId(id):
+  """Return a Datastore key_name for a User derived from a Google Account.
+  
+  Args:
+    id: a Google Account (users.User) object
+  """
+  if not id:
+    return None
+
+  return 'User:%s' % id.email()
+
+
 def getIdIfMissing(id):
   """Gets Google Account of logged-in user (possibly None) if id is false.
   
@@ -38,7 +53,7 @@
   other view).
 
   Args:
-    id: a Google Account object, or None
+    id: a Google Account (users.User) object, or None
     
   Returns:
     If id is non-false, it is simply returned; otherwise, the Google Account
@@ -51,16 +66,36 @@
 
   return id
 
-  
+
 def getUserFromId(id):
   """Returns User entity for a Google Account, or None if not found.  
     
   Args:
-    id: a Google Account object
+    id: a Google Account (users.User) object
   """
-  return soc.models.user.User.gql('WHERE id = :1', id).get()
+  # first, attempt a lookup by User:id key name
+  key_name = getUserKeyNameFromId(id)
+  
+  if key_name:
+    user = soc.models.user.User.get_by_key_name(key_name)
+  else:
+    user = None
+  
+  if user:
+    return user
 
+  # email address may have changed, so query the id property
+  user = soc.models.user.User.gql('WHERE id = :1', id).get()
 
+  if user:
+    return user
+
+  # last chance: perhaps the User changed their email address at some point
+  user = soc.models.user.User.gql('WHERE former_ids = :1', id).get()
+
+  return user
+
+  
 def getUserIfMissing(user, id):
   """Conditionally returns User entity for a Google Account.
   
@@ -74,7 +109,7 @@
 
   Args:
     user: None (usually), or an existing User entity
-    id: a Google Account object
+    id: a Google Account (users.User) object
     
   Returns:
     * user (which may have already been None if passed in that way by the
@@ -100,6 +135,50 @@
     return False
 
 
+def isIdDeveloper(id=None):
+  """Returns True if Google Account is a Developer with special privileges.
+  
+  Args:
+    id: a Google Account (users.User) object; if id is not supplied,
+      the current logged-in user is checked using the App Engine Users API.
+      THIS ARGUMENT IS CURRENTLY IGNORED AND ONLY THE CURRENTLY LOGGED-IN
+      USER IS CHECKED!
+
+  See the TODO in the code below...
+  """
+  if not id:
+    return users.is_current_user_admin()
+
+  # TODO(tlarsen): this Google App Engine function only checks the currently
+  #   logged in user.  There needs to be another way to do this, such as a
+  #   field in the User Model...
+  return users.is_current_user_admin()
+
+
+LINKNAME_PATTERN = r'''(?x)
+    ^
+    [0-9a-z]   # start with ASCII digit or lowercase
+    (
+     [0-9a-z]  # additional ASCII digit or lowercase
+     |         # -OR-
+     _[0-9a-z] # underscore and ASCII digit or lowercase
+    )*         # zero or more of OR group
+    $
+'''
+
+LINKNAME_REGEX = re.compile(LINKNAME_PATTERN)
+
+def isLinkNameFormatValid(link_name):
+  """Returns True if link_name is in a valid format.
+  
+  Args:
+    link_name: link name used in URLs to identify user
+  """
+  if LINKNAME_REGEX.match(link_name):
+    return True
+  return False
+
+
 def getUserFromLinkName(link_name):
   """Returns User entity for link_name or None if not found.
     
@@ -136,3 +215,103 @@
   # else: a link name was supplied, but there is no User that has it
   raise out_of_band.ErrorResponse(
       'There is no user with a "link name" of "%s".' % link_name, status=404)
+
+
+def doesLinkNameBelongToId(link_name, id=None):
+  """Returns True if supplied link name belongs to supplied Google Account.
+  
+  Args:
+    link_name: link name used in URLs to identify user
+    id: a Google Account object; optional, current logged-in user will
+      be used (or False will be returned if no user is logged in)
+  """
+  id = getIdIfMissing(id)
+    
+  if not id:
+    # id not supplied and no Google Account logged in, so link name cannot
+    # belong to an unspecified User
+    return False
+
+  user = getUserFromId(id)
+
+  if not user:
+    # no User corresponding to id Google Account, so no link name at all 
+    return False
+
+  if user.link_name != link_name:
+    # User exists for id, but does not have this link name
+    return False
+
+  return True  # link_name does actually belong to this Google Account
+
+
+def updateOrCreateUserFromId(id, **user_properties):
+  """Update existing User entity, or create new one with supplied properties.
+
+  Args:
+    id: a Google Account object
+    **user_properties: keyword arguments that correspond to User entity
+      properties and their values
+      
+  Returns:
+    the User entity corresponding to the Google Account, with any supplied
+    properties changed, or a new User entity now associated with the Google
+    Account and with the supplied properties
+  """
+  # attempt to retrieve the existing User
+  user = getUserFromId(id)
+  
+  if not user:
+    # user did not exist, so create one in a transaction
+    key_name = getUserKeyNameFromId(id)
+    user = soc.models.user.User.get_or_insert(
+      key_name, id=id, **user_properties)
+
+  # there is no way to be sure if get_or_insert() returned a new User or
+  # got an existing one due to a race, so update with user_properties anyway,
+  # in a transaction
+  return updateUserProperties(user, **user_properties)
+
+
+def updateUserProperties(user, **user_properties):
+  """Update existing User entity using supplied User properties.
+
+  Args:
+    user: a User entity
+    **user_properties: keyword arguments that correspond to User entity
+      properties and their values
+      
+  Returns:
+    the original User entity with any supplied properties changed 
+  """
+  def update():
+    return _unsafeUpdateUserProperties(user, **user_properties)
+
+  return db.run_in_transaction(update)
+
+  
+def _unsafeUpdateUserProperties(user, **user_properties):
+  """(see updateUserProperties)
+  
+  Like updateUserProperties(), but not run within a transaction. 
+  """
+  properties = user.properties()
+  
+  for prop in properties.values():
+    if prop.name in user_properties:
+      if prop.name == 'former_ids':
+        # former_ids cannot be overwritten directly
+        continue
+
+      value = user_properties[prop.name]
+
+      if prop.name == 'id':
+        old_id = user.id
+
+        if value != old_id:
+          user.former_ids.append(old_id)
+
+      prop.__set__(user, value)
+        
+  user.put()
+  return user
--- a/app/soc/models/user.py	Thu Sep 11 21:30:08 2008 +0000
+++ b/app/soc/models/user.py	Fri Sep 12 02:12:38 2008 +0000
@@ -25,7 +25,9 @@
 
 import logging
 
+from google.appengine.api import users
 from google.appengine.ext import db
+
 from django.utils.translation import ugettext_lazy
 
 from soc.models import base
@@ -57,6 +59,10 @@
   #: of any Melange application.
   id = db.UserProperty(required=True)
 
+  #: A list (possibly empty) of former Google Accounts associated with
+  #: this User.
+  former_ids = db.ListProperty(users.User)
+
   #: Required field storing a nickname; displayed publicly.
   #: Nicknames can be any valid UTF-8 text.
   nick_name = db.StringProperty(required=True,
--- a/app/soc/views/helpers/response_helpers.py	Thu Sep 11 21:30:08 2008 +0000
+++ b/app/soc/views/helpers/response_helpers.py	Fri Sep 12 02:12:38 2008 +0000
@@ -112,7 +112,7 @@
   context['user'] = id_user.getUserIfMissing(context.get('user', None),
                                              context['id'])
   context['is_admin'] = context.get(
-      'is_admin', users.is_current_user_admin())
+      'is_admin', id_user.isIdDeveloper(id=context['id'])),
   context['is_debug'] = context.get('is_debug', system.isDebug())
   context['sign_in'] = context.get(
       'sign_in', users.create_login_url(request.path))
--- a/app/soc/views/user/profile.py	Thu Sep 11 21:30:08 2008 +0000
+++ b/app/soc/views/user/profile.py	Fri Sep 12 02:12:38 2008 +0000
@@ -22,6 +22,7 @@
   ]
 
 import re
+import logging
 
 from google.appengine.api import users
 from django import http
@@ -41,17 +42,6 @@
 class UserForm(forms_helpers.DbModelForm):
   """Django form displayed when creating or editing a User.
   """
-  LINKNAME_PATTERN = r'''(?x)
-      ^
-      [0-9a-z]  # start with ASCII digit or lowercase
-      (
-      [0-9a-z]  # additional ASCII digit or lowercase
-      |         # -OR-
-      _[0-9a-z] # underscore and ASCII digit or lowercase
-      )*        # zero or more of OR group
-      $'''
-  LINKNAME_REGEX = re.compile(LINKNAME_PATTERN)
-  
   class Meta:
     """Inner Meta class that defines some behavior for the form.
     """
@@ -59,18 +49,16 @@
     model = soc.models.user.User
     
     #: list of model fields which will *not* be gathered by the form
-    exclude = ['id']
+    exclude = ['id', 'former_ids']
   
   def clean_link_name(self):
-    linkname = self.cleaned_data.get('link_name')
-    linkname_user = id_user.getUserFromLinkName(linkname)
-    id = users.get_current_user()
-    # if linkname exist in datastore and doesn't belong to current user
-    if linkname_user and (linkname_user.id != id):
+    link_name = self.cleaned_data.get('link_name')
+    if not id_user.isLinkNameFormatValid(link_name):
+      raise forms.ValidationError("This link name is in wrong format.")
+    elif not id_user.doesLinkNameBelongToId(link_name):
+      # link_name exists in Datastore but doesn't belong to current user
       raise forms.ValidationError("This link name is already in use.")
-    elif not self.LINKNAME_REGEX.match(linkname):
-      raise forms.ValidationError("This link name is in wrong format.")
-    return linkname
+    return link_name
 
 
 DEF_USER_PROFILE_EDIT_TMPL = 'soc/user/profile/edit.html'
@@ -79,9 +67,9 @@
   """View for a User to modify the properties of a User Model entity.
 
   Args:
-    request: the standard django request object.
+    request: the standard django request object
     linkname: the User's site-unique "linkname" extracted from the URL
-    template: the template path to use for rendering the template.
+    template: the template path to use for rendering the template
 
   Returns:
     A subclass of django.http.HttpResponse which either contains the form to
@@ -118,8 +106,6 @@
     # so show public view for that (other) User entity
     return simple.public(request, template, linkname, context)
 
-  user = id_user.getUserFromId(id)
-  
   if request.method == 'POST':
     form = UserForm(request.POST)
 
@@ -127,19 +113,17 @@
       linkname = form.cleaned_data.get('link_name')
       nickname = form.cleaned_data.get("nick_name")
 
-      if not user:
-        user = soc.models.user.User(id=id, link_name=linkname,
-                                    nick_name=nickname)
-      else:
-        user.nick_name = nickname
-        user.link_name = linkname
+      user = id_user.updateOrCreateUserFromId(
+          id, link_name=linkname, nick_name=nickname)
 
-      user.put()
       # TODO(tlarsen):
       # if old_linkname:  redirect to new /user/profile/new_linkname
       #   (how to preserve displaying the "Profile saved" message?)
       context.update({'submit_message': 'Profile saved.'})
   else: # request.method == 'GET'
+    # try to fetch User entity corresponding to Google Account if one exists    
+    user = id_user.getUserFromId(id)
+
     if user:
       # populate form with the existing User entity
       form = UserForm(instance=user)