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