thirdparty/google_appengine/google/appengine/ext/admin/__init__.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/ext/admin/__init__.py	Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,875 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# 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.
+#
+
+"""Simple datastore view and interactive console, for use in dev_appserver."""
+
+
+
+
+
+import cgi
+import csv
+import cStringIO
+import datetime
+import logging
+import math
+import mimetypes
+import os.path
+import random
+import sys
+import time
+import traceback
+import types
+import urllib
+import urlparse
+import wsgiref.handlers
+
+from google.appengine.api import datastore
+from google.appengine.api import datastore_types
+from google.appengine.api import datastore_errors
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+_DEBUG = True
+
+
+class ImageHandler(webapp.RequestHandler):
+  """Serves a static image.
+
+  This exists because we don't want to burden the user with specifying
+  a static file handler for the image resources used by the admin tool.
+  """
+
+  PATH = '/images/.*'
+
+  def get(self):
+    image_name = os.path.basename(self.request.path)
+    content_type, encoding = mimetypes.guess_type(image_name)
+    if not content_type or not content_type.startswith('image/'):
+      logging.debug('image_name=%r, content_type=%r, encoding=%r',
+                    image_name, content_type, encoding)
+      self.error(404)
+      return
+    directory = os.path.dirname(__file__)
+    path = os.path.join(directory, 'templates', 'images', image_name)
+    try:
+      image_stream = open(path, 'rb')
+    except IOError, e:
+      logging.error('Cannot open image %s: %s', image_name, e)
+      self.error(404)
+      return
+    try:
+      image_data = image_stream.read()
+    finally:
+      image_stream.close()
+    self.response.headers['Content-Type'] = content_type
+    self.response.out.write(image_data)
+
+
+class BaseRequestHandler(webapp.RequestHandler):
+  """Supplies a common template generation function.
+
+  When you call generate(), we augment the template variables supplied with
+  the current user in the 'user' variable and the current webapp request
+  in the 'request' variable.
+  """
+
+  def generate(self, template_name, template_values={}):
+    base_path = self.base_path()
+    values = {
+      'user': users.get_current_user(),
+      'request': self.request,
+      'home_path': base_path + DefaultPageHandler.PATH,
+      'datastore_path': base_path + DatastoreQueryHandler.PATH,
+      'datastore_edit_path': base_path + DatastoreEditHandler.PATH,
+      'datastore_batch_edit_path': base_path + DatastoreBatchEditHandler.PATH,
+      'interactive_path': base_path + InteractivePageHandler.PATH,
+      'interactive_execute_path': base_path + InteractiveExecuteHandler.PATH,
+    }
+    values.update(template_values)
+    directory = os.path.dirname(__file__)
+    path = os.path.join(directory, os.path.join('templates', template_name))
+    self.response.out.write(template.render(path, values, debug=_DEBUG))
+
+  def base_path(self):
+    """Returns the base path of this admin app, which is chosen by the user.
+
+    The user specifies which paths map to this application in their app.cfg.
+    You can get that base path with this method. Combine with the constant
+    paths specified by the classes to construct URLs.
+    """
+    path = self.__class__.PATH
+    return self.request.path[:-len(path)]
+
+  def filter_url(self, args):
+    """Filters the current URL to only have the given list of arguments.
+
+    For example, if your URL is /search?q=foo&num=100&start=10, then
+
+       self.filter_url(['start', 'num']) => /search?num=100&start=10
+       self.filter_url(['q']) => /search?q=10
+       self.filter_url(['random']) => /search?
+
+    """
+    queries = []
+    for arg in args:
+      value = self.request.get(arg)
+      if value:
+        queries.append(arg + '=' + urllib.quote_plus(self.request.get(arg)))
+    return self.request.path + '?' + '&'.join(queries)
+
+
+class DefaultPageHandler(BaseRequestHandler):
+  """Redirects to the Datastore application by default."""
+
+  PATH = '/'
+
+  def get(self):
+    if self.request.path.endswith('/'):
+      base = self.request.path[:-1]
+    else:
+      base = self.request.path
+    self.redirect(base + DatastoreQueryHandler.PATH)
+
+
+class InteractivePageHandler(BaseRequestHandler):
+  """Shows our interactive console HTML."""
+  PATH = '/interactive'
+
+  def get(self):
+    self.generate('interactive.html')
+
+
+class InteractiveExecuteHandler(BaseRequestHandler):
+  """Executes the Python code submitted in a POST within this context.
+
+  For obvious reasons, this should only be available to administrators
+  of the applications.
+  """
+
+  PATH = InteractivePageHandler.PATH + '/execute'
+
+  def post(self):
+    self.response.headers['Content-Type'] = 'text/plain'
+
+    save_stdout = sys.stdout
+    try:
+      sys.stdout = self.response.out
+
+      code = self.request.get('code')
+      code = code.replace("\r\n", "\n")
+
+      try:
+        compiled_code = compile(code, '<string>', 'exec')
+        exec(compiled_code, globals())
+      except Exception, e:
+        lines = traceback.format_exception(*sys.exc_info())
+        self.response.out.write(''.join(lines))
+    finally:
+      sys.stdout = save_stdout
+
+
+class DatastoreRequestHandler(BaseRequestHandler):
+  """The base request handler for our datastore admin pages.
+
+  We provide utility functions for quering the datastore and infering the
+  types of entity properties.
+  """
+
+  def start(self):
+    """Returns the santized "start" argument from the URL."""
+    return self.request.get_range('start', min_value=0, default=0)
+
+  def num(self):
+    """Returns the sanitized "num" argument from the URL."""
+    return self.request.get_range('num', min_value=1, max_value=100,
+                                  default=10)
+
+  def execute_query(self, start=0, num=0, no_order=False):
+    """Parses the URL arguments and executes the query.
+
+    We return a tuple (list of entities, total entity count).
+
+    If the appropriate URL arguments are not given, we return an empty
+    set of results and 0 for the entity count.
+    """
+    kind = self.request.get('kind')
+    if not kind:
+      return ([], 0)
+    query = datastore.Query(kind)
+
+    order = self.request.get('order')
+    order_type = self.request.get('order_type')
+    if order and order_type:
+      order_type = DataType.get_by_name(order_type).python_type()
+      if order.startswith('-'):
+        direction = datastore.Query.DESCENDING
+        order = order[1:]
+      else:
+        direction = datastore.Query.ASCENDING
+      try:
+        query.Order((order, order_type, direction))
+      except datastore_errors.BadArgumentError:
+        pass
+
+    if not start:
+      start = self.start()
+    if not num:
+      num = self.num()
+    total = query.Count()
+    entities = query.Get(start + num)[start:]
+    return (entities, total)
+
+  def get_key_values(self, entities):
+    """Returns the union of key names used by the given list of entities.
+
+    We return the union as a dictionary mapping the key names to a sample
+    value from one of the entities for the key name.
+    """
+    key_dict = {}
+    for entity in entities:
+      for key, value in entity.iteritems():
+        if key_dict.has_key(key):
+          key_dict[key].append(value)
+        else:
+          key_dict[key] = [value]
+    return key_dict
+
+
+class DatastoreQueryHandler(DatastoreRequestHandler):
+  """Our main request handler that executes queries and lists entities.
+
+  We use execute_query() in our base request handler to parse URL arguments
+  and execute the datastore query.
+  """
+
+  PATH = '/datastore'
+
+  def get(self):
+    """Formats the results from execute_query() for datastore.html.
+
+    The only complex part of that process is calculating the pager variables
+    to generate the Gooooogle pager at the bottom of the page.
+    """
+    result_set, total = self.execute_query()
+    key_values = self.get_key_values(result_set)
+    keys = key_values.keys()
+    keys.sort()
+
+    headers = []
+    for key in keys:
+      sample_value = key_values[key][0]
+      headers.append({
+        'name': key,
+        'type': DataType.get(sample_value).name(),
+      })
+
+    entities = []
+    edit_path = self.base_path() + DatastoreEditHandler.PATH
+    for entity in result_set:
+      attributes = []
+      for key in keys:
+        if entity.has_key(key):
+          raw_value = entity[key]
+          value = DataType.get(raw_value).format(raw_value)
+          short_value = DataType.get(raw_value).short_format(raw_value)
+        else:
+          value = ''
+          short_value = ''
+        attributes.append({
+          'name': key,
+          'value': value,
+          'short_value': short_value,
+        })
+      entities.append({
+        'key': str(entity.key()),
+        'key_name': entity.key().name(),
+        'key_id': entity.key().id(),
+        'shortened_key': str(entity.key())[:8] + '...',
+        'attributes': attributes,
+        'edit_uri': edit_path + '?key=' + str(entity.key()) + '&kind=' + urllib.quote(self.request.get('kind')) + '&next=' + urllib.quote(self.request.uri),
+      })
+
+    start = self.start()
+    num = self.num()
+    max_pager_links = 8
+    current_page = start / num
+    num_pages = int(math.ceil(total * 1.0 / num))
+    page_start = max(math.floor(current_page - max_pager_links / 2), 0)
+    page_end = min(page_start + max_pager_links, num_pages)
+
+    pages = []
+    for page in range(page_start + 1, page_end + 1):
+      pages.append({
+        'number': page,
+        'start': (page - 1) * num,
+      })
+    current_page += 1
+
+    values = {
+      'request': self.request,
+      'kind': self.request.get('kind'),
+      'order': self.request.get('order'),
+      'headers': headers,
+      'entities': entities,
+      'message': self.request.get('msg'),
+      'pages': pages,
+      'current_page': current_page,
+      'num': num,
+      'next_start': -1,
+      'prev_start': -1,
+      'start': start,
+      'total': total,
+      'start_base_url': self.filter_url(['kind', 'order', 'order_type',
+                                         'num']),
+      'order_base_url': self.filter_url(['kind', 'num']),
+    }
+    if current_page > 1:
+      values['prev_start'] = int((current_page - 2) * num)
+    if current_page < num_pages:
+      values['next_start'] = int(current_page * num)
+
+    self.generate('datastore.html', values)
+
+
+class DatastoreBatchEditHandler(DatastoreRequestHandler):
+  """Request handler for a batch operation on entities.
+
+  Supports deleting multiple entities by key, then redirecting to another url.
+  """
+
+  PATH = DatastoreQueryHandler.PATH + '/batchedit'
+
+  def post(self):
+    kind = self.request.get('kind')
+
+    keys = []
+    index = 0
+    num_keys = int(self.request.get('numkeys'))
+    for i in xrange(1, num_keys+1):
+      key = self.request.get('key%d' % i)
+      if key:
+        keys.append(key)
+
+    if self.request.get('action') == 'Delete':
+      num_deleted = 0
+      for key in keys:
+        datastore.Delete(datastore.Key(key))
+        num_deleted = num_deleted + 1
+      message = '%d entit%s deleted.' % (
+        num_deleted, ('ies', 'y')[num_deleted == 1])
+      self.redirect(
+        '%s&msg=%s' % (self.request.get('next'), urllib.quote_plus(message)))
+      return
+
+    self.error(404)
+
+
+class DatastoreEditHandler(DatastoreRequestHandler):
+  """Request handler for the entity create/edit form.
+
+  We determine how to generate a form to edit an entity by doing a query
+  on the entity kind and looking at the set of keys and their types in
+  the result set. We use the DataType subclasses for those introspected types
+  to generate the form and parse the form results.
+  """
+
+  PATH = DatastoreQueryHandler.PATH + '/edit'
+
+  def get(self):
+    kind = self.request.get('kind')
+    sample_entities = self.execute_query()[0]
+    if len(sample_entities) < 1:
+      next_uri = self.request.get('next')
+      kind_param = 'kind=%s' % kind
+      if not kind_param in next_uri:
+        if '?' in next_uri:
+          next_uri += '&' + kind_param
+        else:
+          next_uri += '?' + kind_param
+      self.redirect(next_uri)
+      return
+
+    entity_key = self.request.get('key')
+    if entity_key:
+      key_instance = datastore.Key(entity_key)
+      entity_key_name = key_instance.name()
+      entity_key_id = key_instance.id()
+      parent_key = key_instance.parent()
+      entity = datastore.Get(key_instance)
+    else:
+      key_instance = None
+      entity_key_name = None
+      entity_key_id = None
+      parent_key = None
+      entity = None
+
+    if parent_key:
+      parent_kind = parent_key.kind()
+    else:
+      parent_kind = None
+
+    fields = []
+    key_values = self.get_key_values(sample_entities)
+    for key, sample_values in key_values.iteritems():
+      if entity and entity.has_key(key):
+        data_type = DataType.get(entity[key])
+      else:
+        data_type = DataType.get(sample_values[0])
+      name = data_type.name() + "|" + key
+      if entity and entity.has_key(key):
+        value = entity[key]
+      else:
+        value = None
+      field = data_type.input_field(name, value, sample_values)
+      fields.append((key, data_type.name(), field))
+
+    self.generate('datastore_edit.html', {
+      'kind': kind,
+      'key': entity_key,
+      'key_name': entity_key_name,
+      'key_id': entity_key_id,
+      'fields': fields,
+      'focus': self.request.get('focus'),
+      'next': self.request.get('next'),
+      'parent_key': parent_key,
+      'parent_kind': parent_kind,
+    })
+
+  def post(self):
+    kind = self.request.get('kind')
+    entity_key = self.request.get('key')
+    if entity_key:
+      if self.request.get('action') == 'Delete':
+        datastore.Delete(datastore.Key(entity_key))
+        self.redirect(self.request.get('next'))
+        return
+      entity = datastore.Get(datastore.Key(entity_key))
+    else:
+      entity = datastore.Entity(kind)
+
+    args = self.request.arguments()
+    for arg in args:
+      bar = arg.find('|')
+      if bar > 0:
+        data_type_name = arg[:bar]
+        field_name = arg[bar + 1:]
+        form_value = self.request.get(arg)
+        data_type = DataType.get_by_name(data_type_name)
+        if entity and entity.has_key(field_name):
+          old_formatted_value = data_type.format(entity[field_name])
+          if old_formatted_value == form_value:
+            continue
+
+        if len(form_value) > 0:
+          value = data_type.parse(form_value)
+          entity[field_name] = value
+        elif entity.has_key(field_name):
+          del entity[field_name]
+
+    datastore.Put(entity)
+
+    self.redirect(self.request.get('next'))
+
+
+class DataType(object):
+  """A DataType represents a data type in the datastore.
+
+  Each DataType subtype defines four methods:
+
+     format: returns a formatted string for a datastore value
+     input_field: returns a string HTML <input> element for this DataType
+     name: the friendly string name of this DataType
+     parse: parses the formatted string representation of this DataType
+     python_type: the canonical Python type for this datastore type
+
+  We use DataType instances to display formatted values in our result lists,
+  and we uses input_field/format/parse to generate forms and parse the results
+  from those forms to allow editing of entities.
+  """
+  @staticmethod
+  def get(value):
+    return _DATA_TYPES[value.__class__]
+
+  @staticmethod
+  def get_by_name(name):
+    return _NAMED_DATA_TYPES[name]
+
+  def format(self, value):
+    return str(value)
+
+  def short_format(self, value):
+    return self.format(value)
+
+  def input_field(self, name, value, sample_values):
+    if value is not None:
+      string_value = self.format(value)
+    else:
+      string_value = ''
+    return '<input class="%s" name="%s" type="text" size="%d" value="%s"/>' % (cgi.escape(self.name()), cgi.escape(name), self.input_field_size(),
+            cgi.escape(string_value))
+
+  def input_field_size(self):
+    return 30
+
+
+class StringType(DataType):
+  def format(self, value):
+    return value
+
+  def input_field(self, name, value, sample_values):
+    multiline = False
+    if value:
+      multiline = len(value) > 255 or value.find('\n') >= 0
+    if not multiline:
+      for sample_value in sample_values:
+        if len(sample_value) > 255 or sample_value.find('\n') >= 0:
+          multiline = True
+          break
+    if multiline:
+      if not value:
+        value = ''
+      return '<textarea name="%s" rows="5" cols="50">%s</textarea>' % (cgi.escape(name), cgi.escape(value))
+    else:
+      return DataType.input_field(self, name, value, sample_values)
+
+  def name(self):
+    return 'string'
+
+  def parse(self, value):
+    return value
+
+  def python_type(self):
+    return str
+
+  def input_field_size(self):
+    return 50
+
+
+class TextType(StringType):
+  def name(self):
+    return 'Text'
+
+  def input_field(self, name, value, sample_values):
+    return '<textarea name="%s" rows="5" cols="50">%s</textarea>' % (cgi.escape(name), cgi.escape(str(value)))
+
+  def parse(self, value):
+    return datastore_types.Text(value)
+
+  def python_type(self):
+    return datastore_types.Text
+
+
+class BlobType(StringType):
+  def name(self):
+    return 'Blob'
+
+  def input_field(self, name, value, sample_values):
+    return '&lt;binary&gt;'
+
+  def format(self, value):
+    return '<binary>'
+
+  def python_type(self):
+    return datastore_types.Blob
+
+
+class TimeType(DataType):
+  _FORMAT = '%Y-%m-%d %H:%M:%S'
+
+  def format(self, value):
+    return value.strftime(TimeType._FORMAT)
+
+  def name(self):
+    return 'datetime'
+
+  def parse(self, value):
+    return datetime.datetime(*(time.strptime(value, TimeType._FORMAT)[0:6]))
+
+  def python_type(self):
+    return datetime.datetime
+
+
+class ListType(DataType):
+  def format(self, value):
+    value_file = cStringIO.StringIO()
+    try:
+      writer = csv.writer(value_file)
+      writer.writerow(value)
+      return value_file.getvalue()
+    finally:
+      value_file.close()
+
+  def name(self):
+    return 'list'
+
+  def parse(self, value):
+    value_file = cStringIO.StringIO(value)
+    try:
+      reader = csv.reader(value_file)
+      return reader.next()
+    finally:
+      value_file.close()
+
+  def python_type(self):
+    return list
+
+
+class BoolType(DataType):
+  def name(self):
+    return 'bool'
+
+  def input_field(self, name, value, sample_values):
+    selected = { None: '', False: '', True: '' };
+    selected[value] = "selected"
+    return """<select class="%s" name="%s">
+    <option %s value=''></option>
+    <option %s value='0'>False</option>
+    <option %s value='1'>True</option></select>""" % (cgi.escape(self.name()), cgi.escape(name), selected[None],
+            selected[False], selected[True])
+
+  def parse(self, value):
+    if value.lower() is 'true':
+      return True
+    if value.lower() is 'false':
+      return False
+    return bool(int(value))
+
+  def python_type(self):
+    return bool
+
+
+class NumberType(DataType):
+  def input_field_size(self):
+    return 10
+
+
+class IntType(NumberType):
+  def name(self):
+    return 'int'
+
+  def parse(self, value):
+    return int(value)
+
+  def python_type(self):
+    return int
+
+
+class LongType(NumberType):
+  def name(self):
+    return 'long'
+
+  def parse(self, value):
+    return long(value)
+
+  def python_type(self):
+    return long
+
+
+class FloatType(NumberType):
+  def name(self):
+    return 'float'
+
+  def parse(self, value):
+    return float(value)
+
+  def python_type(self):
+    return float
+
+
+class UserType(DataType):
+  def name(self):
+    return 'User'
+
+  def parse(self, value):
+    return users.User(value)
+
+  def python_type(self):
+    return users.User
+
+  def input_field_size(self):
+    return 15
+
+class ReferenceType(DataType):
+  def name(self):
+    return 'Key'
+
+  def short_format(self, value):
+    return str(value)[:8] + '...'
+
+  def parse(self, value):
+    return datastore_types.Key(value)
+
+  def python_type(self):
+    return datastore_types.Key
+
+  def input_field_size(self):
+    return 85
+
+
+class EmailType(StringType):
+  def name(self):
+    return 'Email'
+
+  def parse(self, value):
+    return datastore_types.Email(value)
+
+  def python_type(self):
+    return datastore_types.Email
+
+
+class CategoryType(StringType):
+  def name(self):
+    return 'Category'
+
+  def parse(self, value):
+    return datastore_types.Category(value)
+
+  def python_type(self):
+    return datastore_types.Category
+
+
+class LinkType(StringType):
+  def name(self):
+    return 'Link'
+
+  def parse(self, value):
+    return datastore_types.Link(value)
+
+  def python_type(self):
+    return datastore_types.Link
+
+
+class GeoPtType(DataType):
+  def name(self):
+    return 'GeoPt'
+
+  def parse(self, value):
+    return datastore_types.GeoPt(value)
+
+  def python_type(self):
+    return datastore_types.GeoPt
+
+
+class ImType(DataType):
+  def name(self):
+    return 'IM'
+
+  def parse(self, value):
+    return datastore_types.IM(value)
+
+  def python_type(self):
+    return datastore_types.IM
+
+
+class PhoneNumberType(StringType):
+  def name(self):
+    return 'PhoneNumber'
+
+  def parse(self, value):
+    return datastore_types.PhoneNumber(value)
+
+  def python_type(self):
+    return datastore_types.PhoneNumber
+
+
+class PostalAddressType(StringType):
+  def name(self):
+    return 'PostalAddress'
+
+  def parse(self, value):
+    return datastore_types.PostalAddress(value)
+
+  def python_type(self):
+    return datastore_types.PostalAddress
+
+
+class RatingType(NumberType):
+  def name(self):
+    return 'Rating'
+
+  def parse(self, value):
+    return datastore_types.Rating(value)
+
+  def python_type(self):
+    return datastore_types.Rating
+
+
+class NoneType(DataType):
+  def name(self):
+    return 'None'
+
+  def parse(self, value):
+    return None
+
+  def format(self, value):
+    return 'None'
+
+_DATA_TYPES = {
+  types.NoneType: NoneType(),
+  types.StringType: StringType(),
+  types.UnicodeType: StringType(),
+  datastore_types.Text: TextType(),
+  datastore_types.Blob: BlobType(),
+  types.BooleanType: BoolType(),
+  types.IntType: IntType(),
+  types.LongType: LongType(),
+  types.FloatType: FloatType(),
+  datetime.datetime: TimeType(),
+  users.User: UserType(),
+  datastore_types.Key: ReferenceType(),
+  types.ListType: ListType(),
+  datastore_types.Email: EmailType(),
+  datastore_types.Category: CategoryType(),
+  datastore_types.Link: LinkType(),
+  datastore_types.GeoPt: GeoPtType(),
+  datastore_types.IM: ImType(),
+  datastore_types.PhoneNumber: PhoneNumberType(),
+  datastore_types.PostalAddress: PostalAddressType(),
+  datastore_types.Rating: RatingType(),
+}
+
+_NAMED_DATA_TYPES = {}
+for data_type in _DATA_TYPES.values():
+  _NAMED_DATA_TYPES[data_type.name()] = data_type
+
+
+def main():
+  application = webapp.WSGIApplication([
+    ('.*' + DatastoreQueryHandler.PATH, DatastoreQueryHandler),
+    ('.*' + DatastoreEditHandler.PATH, DatastoreEditHandler),
+    ('.*' + DatastoreBatchEditHandler.PATH, DatastoreBatchEditHandler),
+    ('.*' + InteractivePageHandler.PATH, InteractivePageHandler),
+    ('.*' + InteractiveExecuteHandler.PATH, InteractiveExecuteHandler),
+    ('.*' + ImageHandler.PATH, ImageHandler),
+    ('.*', DefaultPageHandler),
+  ], debug=_DEBUG)
+  wsgiref.handlers.CGIHandler().run(application)
+
+
+import django
+if django.VERSION[:2] < (0, 97):
+  from django.template import defaultfilters
+  def safe(text, dummy=None):
+    return text
+  defaultfilters.register.filter("safe", safe)
+
+
+if __name__ == '__main__':
+  main()