Make findNearestUsers() code in soc/logic/site/id_user.py more generic and
authorTodd Larsen <tlarsen@google.com>
Fri, 03 Oct 2008 01:32:34 +0000
changeset 263 9b39d93b677f
parent 262 52a42831d9d6
child 264 97b60788cb9a
Make findNearestUsers() code in soc/logic/site/id_user.py more generic and move it to soc/logic/model.py. Orginal findNearest...() functions in id_user.py are now convenience wrappers. Add typed-query string construction functions to model.py. Move getFulLClassName() from key_name.py model.py, since it has more to do with Model types than key names. Swap 'offset' and 'limit' and make 'limit' arguments non-optional. Also, stop adding 1 inside the ...ForLimitAndOffset() functions and make the callers do it (since it was being added for a very UI-specific reason of whether or not to display a "Next>" link). Patch by: Todd Larsen Review by: Pawel Solyga Review URL: http://codereviews.googleopensourceprograms.com/1401
app/soc/logic/document.py
app/soc/logic/key_name.py
app/soc/logic/model.py
app/soc/logic/site/id_user.py
app/soc/logic/sponsor.py
app/soc/logic/works.py
app/soc/views/site/docs/list.py
app/soc/views/site/sponsor/list.py
app/soc/views/site/user/list.py
--- a/app/soc/logic/document.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/logic/document.py	Fri Oct 03 01:32:34 2008 +0000
@@ -31,10 +31,9 @@
 from soc.logic import out_of_band
 from soc.logic.site import id_user
 
-import soc.logic.model
+from soc.logic import model
 
 import soc.models.document
-import soc.models.work
 
 
 def getDocument(path, link_name=None):
@@ -114,20 +113,3 @@
   # got an existing one due to a race, so update with document_properties anyway,
   # in a transaction
   return soc.logic.model.updateModelProperties(document, **document_properties)
-
-
-def getWorksForOffsetAndLimit(offset=0, limit=0, cls=soc.models.work.Work):
-  """Returns Works for given offset and limit or None if not found.
-    
-  Args:
-    offset: offset in entities list which defines first entity to return
-    limit: max amount of entities to return
-    cls: Model class of items to return (including sub-classes of that type);
-      default is Work
-  """
-  query = db.GqlQuery(
-      'SELECT * FROM Work WHERE inheritance_line = :1 ORDER BY title',
-      key_name.getFullClassName(cls))
-
-  # Fetch one more to see if there should be a 'next' link
-  return query.fetch(limit+1, offset)  
--- a/app/soc/logic/key_name.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/logic/key_name.py	Fri Oct 03 01:32:34 2008 +0000
@@ -31,11 +31,6 @@
   pass
 
 
-def getFullClassName(cls):
-  """Returns fully-qualified module.class name string.""" 
-  return '%s.%s' % (cls.__module__, cls.__name__) 
-
-
 def nameDocument(partial_path, link_name=None):
   """Returns a Document key name constructed from a path and link name.
     
--- a/app/soc/logic/model.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/logic/model.py	Fri Oct 03 01:32:34 2008 +0000
@@ -19,11 +19,163 @@
 
 __authors__ = [
   '"Pawel Solyga" <pawel.solyga@gmail.com>',
+  '"Todd Larsen" <tlarsen@google.com>',
   ]
 
+
 from google.appengine.ext import db
 
 
+def getFullClassName(cls):
+  """Returns fully-qualified module.class name string.""" 
+  return '%s.%s' % (cls.__module__, cls.__name__) 
+
+
+def buildTypedQueryString(base_class, derived_class=None):
+  """Returns a GQL query string compatible with PolyModel.
+  
+  Args:
+    base_class: Model class that inherits directly from
+      polymodel.PolyModel, such as soc.models.work.Work
+    derived_class: optional more-specific Model class that
+      derives from base_class, such as soc.model.document.Document;
+      default is None, in which case the inheritance_line
+      property is *not* tested by the returned query string
+  """
+  query_str_parts = ['SELECT * FROM ', base_class.__name__]
+
+  if derived_class:
+    query_str_parts.extend(
+      [" WHERE inheritance_line = '", getFullClassName(derived_class), "'"])
+
+  return ''.join(query_str_parts)
+
+
+def buildOrderedQueryString(base_class, derived_class=None, order_by=None):
+  """Returns a an ordered GQL query string compatible with PolyModel.
+  
+  Args:
+    base_class, derived_class: see buildTypedQueryString()
+    order_by: optional field name by which to order the query results;
+      default is None, in which case no ORDER BY clause is placed in
+      the query string
+  """
+  query_str_parts = [
+    buildTypedQueryString(base_class, derived_class=derived_class)]
+
+  if order_by:
+    query_str_parts.extend([' ORDER BY ', order_by])
+
+  return ''.join(query_str_parts)
+
+
+def getEntitiesForLimitAndOffset(base_class, limit, offset=0,
+                                 derived_class=None, order_by=None):
+  """Returns entities for given offset and limit or None if not found.
+    
+  Args:
+    limit: max amount of entities to return
+    offset: optional offset in entities list which defines first entity to
+      return; default is zero (first entity)
+  """
+  query_string = buildOrderedQueryString(
+      base_class, derived_class=derived_class, order_by=order_by)
+
+  query = db.GqlQuery(query_string)
+
+  return query.fetch(limit, offset)
+
+
+def getNearestEntities(base_class, fields_to_try, derived_class=None):
+  """Get entities just before and just after the described entity.
+    
+  Args:
+    fields_to_try: ordered list of key/value pairs that "describe"
+      the desired entity (which may not necessarily exist), where key is
+      the name of the field, and value is an instance of that field
+      used in the comparison; if value is None, that field is skipped
+
+  Returns:
+    a two-tuple: ([nearest_entities], 'field_name')
+    
+    nearest_entities: list of entities being those just before and just
+      after the (possibly non-existent) entity identified by the first
+      of the supplied (non-None) fields
+        OR
+      possibly None if query had no results for the supplied field
+      that was used.
+  """
+  # SELECT * FROM base_class WHERE inheritance_line = 'derived_class'
+  typed_query_str = buildTypedQueryString(
+    base_class, derived_class=derived_class)
+  
+  if derived_class:
+    typed_query_str = typed_query_str + ' AND '
+  else:
+    typed_query_str = typed_query_str + ' WHERE '
+    
+  for field, value in fields_to_try:
+    if value is None:
+      # skip this not-supplied field
+      continue
+
+    query = db.GqlQuery('%s%s > :1' % (typed_query_str, field), value)
+    return query.fetch(1), field
+
+  # all fields exhausted, and all had None values
+  return (None, None)
+
+
+def findNearestEntitiesOffset(width, base_class, fields_to_try,
+                              derived_class=None):
+  """Finds offset of beginning of a range of entities around the nearest.
+  
+  Args:
+    width: the width of the "found" window around the nearest User found
+    base_class, fields_to_try, derived_class:  see getNearestEntities()
+    
+  Returns:
+    an offset into the list of entities that is width/2 less than the
+    offset of the first entity returned by getNearestEntities(), or zero
+    if that offset would be less than zero
+      OR
+    None if there are no nearest entities or the offset of the beginning of
+    the range cannot be found for some reason 
+  """
+  # find entity "nearest" to supplied fields
+  nearest_entities, field = getNearestEntities(
+      base_class, fields_to_try, derived_class=derived_class)
+  
+  if not nearest_entities:
+    # no "nearest" entity, so indicate that with None
+    return None
+
+  nearest_entity = nearest_entities[0]
+
+  # start search for beginning of nearest Users range at offset zero
+  offset = 0
+  entities = getEntitiesForLimitAndOffset(
+      base_class, width, offset=offset, derived_class=derived_class)
+  
+  while True:
+    for entity in entities:
+      if getattr(nearest_entity, field) == getattr(entity, field):
+        # nearest User found in current search range, so return a range start
+        return max(0, (offset - (width/2)))
+
+      offset = offset + 1
+
+    # nearest User was not in the current search range, so fetch the next set
+    entities = getEntitiesForLimitAndOffset(
+        base_class, width, offset=offset, derived_class=derived_class)
+
+    if not entities:
+      # nearest User never found, so indicate that with None
+      break
+
+  return None
+
+
 def updateModelProperties(model, **model_properties):
   """Update existing model entity using supplied model properties.
 
--- a/app/soc/logic/site/id_user.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/logic/site/id_user.py	Fri Oct 03 01:32:34 2008 +0000
@@ -29,6 +29,7 @@
 from google.appengine.ext import db
 
 from soc.logic import key_name
+from soc.logic import model
 from soc.logic import out_of_band
 
 import soc.models.user
@@ -69,17 +70,16 @@
   return id
 
 
-def getUsersForOffsetAndLimit(offset=0, limit=0):
+def getUsersForLimitAndOffset(limit, offset=0):
   """Returns Users entities for given offset and limit or None if not found.
     
   Args:
-    offset: offset in entities list which defines first entity to return
     limit: max amount of entities to return
+    offset: optional offset in entities list which defines first entity to
+      return; default is zero (first entity)
   """
-  query = db.GqlQuery('SELECT * FROM User ORDER BY id')
-
-  # Fetch one more to see if there should be a 'next' link
-  return query.fetch(limit+1, offset)  
+  return model.getEntitiesForLimitAndOffset(
+      soc.models.user.User, limit, offset=offset, order_by='id')
 
 
 def getUserFromId(id):
@@ -132,16 +132,8 @@
     possibly None if query had no results or neither id or link_name were
     supplied.
   """
-  if id:
-    query = db.GqlQuery("SELECT * FROM User WHERE id > :1", id)
-    return query.fetch(1)
-
-  #if id not supplied, try link name.
-  if link_name:
-    query = db.GqlQuery("SELECT * FROM User WHERE link_name > :1", link_name)
-    return query.fetch(1)
-
-  return None
+  return model.getNearestEntities(
+      soc.models.user.User, [('id', id), ('link_name', link_name)])
 
 
 def findNearestUsersOffset(width, id=None, link_name=None):
@@ -160,35 +152,8 @@
     None if there are no nearest Users or the offset of the beginning of
     the range cannot be found for some reason 
   """
-  # find User "nearest" to supplied id (Google Account) or link_name
-  nearest_users = getNearestUsers(id=id, link_name=link_name)
-  
-  if not nearest_users:
-    # no "nearest" User, so indicate that with None
-    return None
-
-  nearest_user = nearest_users[0]
-
-  # start search for beginning of nearest Users range at offset zero
-  offset = 0
-  users = getUsersForOffsetAndLimit(offset=offset, limit=width)
-  
-  while True:
-    for user in users:
-      if nearest_user.id == user.id:
-        # nearest User found in current search range, so return a range start
-        return max(0, (offset - (width/2)))
-
-      offset = offset + 1
-
-    # nearest User was not in the current search range, so fetch the next set
-    users = getUsersForOffsetAndLimit(offset=offset, limit=width)
-
-    if not users:
-      # nearest User never found, so indicate that with None
-      break
-
-  return None
+  return model.findNearestEntitiesOffset(
+    width, soc.models.user.User, [('id', id), ('link_name', link_name)])
 
 
 def doesUserExist(id):
--- a/app/soc/logic/sponsor.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/logic/sponsor.py	Fri Oct 03 01:32:34 2008 +0000
@@ -99,17 +99,16 @@
   return key_name.nameSponsor(link_name)
 
 
-def getSponsorsForOffsetAndLimit(offset=0, limit=0):
+def getSponsorsForLimitAndOffset(limit, offset=0):
   """Returns Sponsors entities for given offset and limit or None if not found.
 
   Args:
-    offset: offset in entities list which defines first entity to return
     limit: max amount of entities to return
+    offset: optional offset in entities list which defines first entity to
+      return; default is zero (first entity)
   """
   query = soc.models.sponsor.Sponsor.all()
-  
-  # Fetch one more to see if there should be a 'next' link
-  return query.fetch(limit+1, offset)
+  return query.fetch(limit, offset)
 
 
 def updateOrCreateSponsorFromLinkName(sponsor_link_name, **sponsor_properties):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/logic/works.py	Fri Oct 03 01:32:34 2008 +0000
@@ -0,0 +1,45 @@
+#!/usr/bin/python2.5
+#
+# 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.
+
+"""Works (Model) query functions.
+"""
+
+__authors__ = [
+  '"Todd Larsen" <tlarsen@google.com>',
+  ]
+
+
+from google.appengine.ext import db
+
+from soc.logic import model
+
+import soc.models.work
+
+
+def getWorksForLimitAndOffset(limit, offset=0, cls=soc.models.work.Work):
+  """Returns Works for given offset and limit or None if not found.
+    
+  Args:
+    limit: max amount of entities to return
+    offset: optional offset in entities list which defines first entity to
+      return; default is zero (first entity)
+    cls: Model class of items to return (including sub-classes of that type);
+      default is Work
+  """
+  query = db.GqlQuery(model.buildOrderedQueryString(
+      soc.models.work.Work, derived_class=cls, order_by='title'))
+ 
+  return query.fetch(limit, offset)  
--- a/app/soc/views/site/docs/list.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/views/site/docs/list.py	Fri Oct 03 01:32:34 2008 +0000
@@ -22,7 +22,7 @@
   ]
 
 
-from soc.logic import document
+from soc.logic import works
 from soc.views import simple
 from soc.views.helpers import list_helpers
 from soc.views.helpers import response_helpers
@@ -56,9 +56,14 @@
   limit = request.GET.get('limit')
 
   offset, limit = list_helpers.getListParemeters(offset=offset, limit=limit)
-  
-  docs = document.getWorksForOffsetAndLimit(
-      offset=offset, limit=limit, cls=soc.models.document.Document)
+
+  # Fetch one more to see if there should be a 'next' link
+  docs = works.getWorksForLimitAndOffset(
+      limit + 1, offset=offset, cls=soc.models.document.Document)
+
+  # TODO(tlarsen): uncomment when pagination select control is working.
+  # form = list_helpers.makeSelectNumItemsForm(request, limit)
+  # context['form'] = form
 
   list_templates = {'list_main': 'soc/list/list_main.html',
                     'list_pagination': 'soc/list/list_pagination.html',
--- a/app/soc/views/site/sponsor/list.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/views/site/sponsor/list.py	Fri Oct 03 01:32:34 2008 +0000
@@ -46,7 +46,12 @@
 
   offset, limit = list_helpers.getListParemeters(offset, limit)
   
-  sponsors = sponsor.getSponsorsForOffsetAndLimit(offset, limit)
+  # Fetch one more to see if there should be a 'next' link
+  sponsors = sponsor.getSponsorsForLimitAndOffset(limit + 1, offset=offset)
+
+  # TODO(tlarsen): uncomment when pagination select control is working.
+  # form = list_helpers.makeSelectNumItemsForm(request, limit)
+  # context['form'] = form
   
   list_templates = {'list_main': 'soc/list/list_main.html',
                     'list_pagination': 'soc/list/list_pagination.html',
--- a/app/soc/views/site/user/list.py	Thu Oct 02 20:22:15 2008 +0000
+++ b/app/soc/views/site/user/list.py	Fri Oct 03 01:32:34 2008 +0000
@@ -29,6 +29,7 @@
 
 import soc.models.user
 
+
 DEF_SITE_USER_LIST_ALL_TMPL = 'soc/site/user/list/all.html'
 
 def all(request, template=DEF_SITE_USER_LIST_ALL_TMPL):
@@ -55,8 +56,13 @@
   limit = request.GET.get('limit')
 
   offset, limit = list_helpers.getListParemeters(offset=offset, limit=limit)
-  
-  users = id_user.getUsersForOffsetAndLimit(offset=offset, limit=limit)
+
+  # Fetch one more to see if there should be a 'next' link
+  users = id_user.getUsersForLimitAndOffset(limit + 1, offset=offset)
+
+  # TODO(tlarsen): uncomment when pagination select control is working.
+  # form = list_helpers.makeSelectNumItemsForm(request, limit)
+  # context['form'] = form
   
   list_templates = {'list_main': 'soc/list/list_main.html',
                     'list_pagination': 'soc/list/list_pagination.html',
@@ -68,4 +74,4 @@
       offset=offset, limit=limit, list_templates=list_templates)
 
   return response_helpers.respond(request, template, context)
-                 
\ No newline at end of file
+