diff -r 261778de26ff -r 620f9b141567 thirdparty/google_appengine/google/appengine/ext/admin/__init__.py --- /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, '', '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 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 '' % (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 '' % (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 '' % (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 '<binary>' + + def format(self, value): + return '' + + 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 """""" % (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()