# HG changeset patch # User Todd Larsen # Date 1221185558 0 # Node ID 3db97cf7f2c7eb76a4847a6d40a7ee82187eac40 # Parent 63248d9db484c4114013b3e9a86ef49fd3866401 ... diff -r 63248d9db484 -r 3db97cf7f2c7 app/soc/logic/site/id_user.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 diff -r 63248d9db484 -r 3db97cf7f2c7 app/soc/models/user.py --- 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, diff -r 63248d9db484 -r 3db97cf7f2c7 app/soc/views/helpers/response_helpers.py --- 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)) diff -r 63248d9db484 -r 3db97cf7f2c7 app/soc/views/user/profile.py --- 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)