changeset 1864:5036a7273f5e gae

merged default into gae branch
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 01 Dec 2012 15:00:46 +0100
parents 350376df6f6a (diff) 269cd70825f0 (current diff)
children 9cc2a4e2f582
files MoinMoin/apps/frontend/views.py MoinMoin/constants/keys.py setup.py wikiconfig.py
diffstat 25 files changed, 689 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sun Nov 25 10:03:46 2012 -0700
+++ b/.hgignore	Sat Dec 01 15:00:46 2012 +0100
@@ -4,6 +4,7 @@
 ^env-pypy/
 ^env-py26/
 ^env-py27/
+^support/
 ^dlc/
 ^moin.egg-info/
 ^MoinMoin/_tests/wiki/data/cache/
--- a/Makefile	Sun Nov 25 10:03:46 2012 -0700
+++ b/Makefile	Sat Dec 01 15:00:46 2012 +0100
@@ -29,6 +29,42 @@
 pylint:
 	@pylint --disable-msg=W0142,W0511,W0612,W0613,C0103,C0111,C0302,C0321,C0322 --disable-msg-cat=R MoinMoin
 
+# Automate creation of the support archive from a virtualenv site-packages directory
+support:
+	@# do NOT name it "site-packages", but "support":
+	@cp -a env/lib/python2.7/site-packages support
+	@# remove compiled code files:
+	@find support -name "*.pyc" -exec rm {} \;
+	@find support -name "*.pyo" -exec rm {} \;
+	@# documentation generation support not needed on GAE:
+	@rm -rf support/sphinx
+	@# package installers not needed/supported on GAE:
+	@rm -rf support/distribute support/pip
+	@# test support and suites not needed in production:
+	@rm -rf support/_pytest support/pytest*.py support/py.test*
+	@rm -rf support/py support/execnet support/pep8.py support/selenium
+	@rm -rf support/flask/testsuite support/werkzeug/testsuite
+	@# misc. egg and path stuff, not needed:
+	@rm -rf support/*.egg support/*.egg-info support/*.egg-link support/*.pth
+	@# we need to add a __init__.py to the namespace packages:
+	@touch support/flaskext/__init__.py support/xstatic/__init__.py support/xstatic/pkg/__init__.py
+
+# Create Dist archive with support
+supportdist:
+	@python setup.py sdist
+	@find dist -name 'moin-*.tar.gz' -exec tar -xzf {} -C dist/ \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec cp -r support/ {} \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec cp moin.py {} \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec cp app.yaml {} \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec cp wikiconfig_gae.py {} \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec sh -c "rm -rf {}/moin.egg-info {}/PKG-INFO {}/MANIFEST.in {}/setup.* {}/quickinstall* {}/Makefile" \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec bash -c 'tar czf dist/`basename {}`_with_support.tar.gz -C dist/ `basename {}`' \;
+	@find dist -mindepth 1 -maxdepth 1 -name "moin-*" -type d -exec rm -rf {} \;
+
+supporttgz:
+	@# create the support archive:
+	@tar czf moin2-support.tgz support
+
 clean: clean-devwiki clean-pyc clean-orig clean-rej
 	-rm -rf build
 
--- a/MoinMoin/app.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/app.py	Sat Dec 01 15:00:46 2012 +0100
@@ -68,6 +68,7 @@
     clock = Clock()
     clock.start('create_app total')
     app = Flask('MoinMoin')
+    app.on_gae = False  # are we running on Google App Engine?
     clock.start('create_app load config')
     if flask_config_file:
         app.config.from_pyfile(flask_config_file)
--- a/MoinMoin/apps/frontend/views.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/apps/frontend/views.py	Sat Dec 01 15:00:46 2012 +0100
@@ -78,43 +78,7 @@
 
 @frontend.route('/robots.txt')
 def robots():
-    return Response("""\
-User-agent: *
-Crawl-delay: 20
-Disallow: /+convert/
-Disallow: /+dom/
-Disallow: /+download/
-Disallow: /+modify/
-Disallow: /+content/
-Disallow: /+delete/
-Disallow: /+ajaxdelete/
-Disallow: /+ajaxdestroy/
-Disallow: /+ajaxmodify/
-Disallow: /+destroy/
-Disallow: /+rename/
-Disallow: /+revert/
-Disallow: /+index/
-Disallow: /+jfu-server/
-Disallow: /+sitemap/
-Disallow: /+similar_names/
-Disallow: /+quicklink/
-Disallow: /+subscribe/
-Disallow: /+backrefs/
-Disallow: /+wanteds/
-Disallow: /+orphans/
-Disallow: /+register
-Disallow: /+recoverpass
-Disallow: /+usersettings
-Disallow: /+login
-Disallow: /+logout
-Disallow: /+bookmark
-Disallow: /+diff/
-Disallow: /+diffraw/
-Disallow: /+search
-Disallow: /+dispatch/
-Disallow: /+admin/
-Allow: /
-""", mimetype='text/plain')
+    return app.send_static_file('robots.txt')
 
 
 @frontend.route('/favicon.ico')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/gae.py	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,81 @@
+# Copyright: 2012 MoinMoin:TarashishMishra
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - Authentication on GAE
+
+    Users could log in into moin using their google account
+
+"""
+
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin import user
+from MoinMoin.auth import BaseAuth, MultistageRedirectLogin, ContinueLogin
+from MoinMoin.constants.keys import *
+
+from werkzeug import redirect, abort
+from flask import url_for
+
+from google.appengine.api import users
+
+
+class GAEAuthMoin(BaseAuth):
+    """ authenticate on gae using google account """
+    name = 'gae'
+    login_inputs = ['special_no_input']
+    logout_possible = True
+
+    def login(self, user_obj=None, **kw):
+        u = None
+        # always revalidate auth
+        if user_obj and user_obj.auth_method == self.name:
+            user_obj = None
+        # something else authenticated before us
+        if user_obj:
+            return ContinueLogin(user_obj)
+        # get the current user from gae
+        gae_user = users.get_current_user()
+        if not gae_user:
+            # Redirect the user to the google account login, telling it to redirect back to
+            # moin's .show_root url, simulating a login there.
+            return_to = url_for(".show_root", login_submit=1)
+            return MultistageRedirectLogin(users.create_login_url(return_to))
+
+        gae_user_id = unicode(gae_user.user_id())
+        email = unicode(gae_user.email())
+        nickname = unicode(gae_user.nickname())
+        logging.debug("Current gae_user: name: {0!r}, email: {1!r}, gae_user_id: {2!r}".format(nickname, email, gae_user_id))
+        # try to get existing user with the same gae_user_id
+        users_list = user.search_users(gae_user_id=gae_user_id)
+        if users_list:
+            u = user.User(uid=users_list[0].meta[ITEMID], trusted=self.trusted, auth_method=self.name)
+            changed = False
+        else:
+            # if no user with same gae_user_id found try to get existing user with the same email
+            users_list = user.search_users(email=email)
+            if users_list:
+                u = user.User(uid=users_list[0].meta[ITEMID], trusted=self.trusted, auth_method=self.name)
+                # set gae_user_id when user is found by email
+                u.profile[GAE_USER_ID] = gae_user_id
+                changed = True
+            else:
+                # if there is no existing user with same gae_user_id or email create one
+                u = user.User(trusted=self.trusted, auth_method=self.name)
+                u.profile[GAE_USER_ID] = gae_user_id
+                u.profile[EMAIL] = email
+                u.profile[NAME] = nickname
+                changed = True
+        if u:
+            u.create_or_update(changed=changed)
+            user_obj = u
+
+        return ContinueLogin(user_obj)
+
+    def logout(self, user_obj, **kw):
+        # TODO: currently, logging out of moin logs you out of all applications that use your
+        # google account. We should fix that.
+        user_obj.logout_session()
+        abort(redirect(users.create_logout_url(url_for('.show_root'))))
--- a/MoinMoin/constants/keys.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/constants/keys.py	Sat Dec 01 15:00:46 2012 +0100
@@ -88,6 +88,7 @@
 EDIT_ROWS = "edit_rows"
 RESULTS_PER_PAGE = "results_per_page"
 DISABLED = "disabled"
+GAE_USER_ID = "gae_user_id"
 
 USEROBJ_ATTRS = [
     # User objects proxy these attributes of the UserProfile objects:
--- a/MoinMoin/converter/rst_in.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/converter/rst_in.py	Sat Dec 01 15:00:46 2012 +0100
@@ -18,7 +18,6 @@
 from __future__ import absolute_import, division
 
 import re
-import pytest
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -32,7 +31,6 @@
 from ._util import allowed_uri_scheme, decode_data, normalize_split_text
 
 #### TODO: try block (do not crash if we don't have docutils)
-pytest.importorskip('docutils')
 from docutils import nodes, utils, writers, core
 from docutils.parsers.rst import Parser
 from docutils.nodes import reference, literal_block
--- a/MoinMoin/mail/sendmail.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/mail/sendmail.py	Sat Dec 01 15:00:46 2012 +0100
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # Copyright: 2003 Juergen Hermann <jh@web.de>
 # Copyright: 2008-2009 MoinMoin:ThomasWaldmann
+# Copyright: 2012 MoinMoin:TarashishMishra
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
@@ -79,11 +80,6 @@
     :rtype: tuple
     :returns: (is_ok, Description of error or OK message)
     """
-    import smtplib, socket
-    from email.Message import Message
-    from email.Charset import Charset, QP
-    from email.Utils import formatdate, make_msgid
-
     cfg = app.cfg
     mail_from = mail_from or cfg.mail_from
 
@@ -93,98 +89,120 @@
     if not to and not cc and not bcc:
         return (1, _("No recipients, nothing to do"))
 
-    subject = subject.encode(config.charset)
-
     # Create a text/plain body using CRLF (see RFC2822)
     text = text.replace(u'\n', u'\r\n')
-    text = text.encode(config.charset)
-
-    # Create a message using config.charset and quoted printable
-    # encoding, which should be supported better by mail clients.
-    # TODO: check if its really works better for major mail clients
-    msg = Message()
-    charset = Charset(config.charset)
-    charset.header_encoding = QP
-    charset.body_encoding = QP
-    msg.set_charset(charset)
-
-    # work around a bug in python 2.4.3 and above:
-    msg.set_payload('=')
-    if msg.as_string().endswith('='):
-        text = charset.body_encode(text)
-
-    msg.set_payload(text)
-
-    address = encodeAddress(mail_from, charset)
-    msg['From'] = address
-    if to:
-        msg['To'] = ','.join(to)
-    if cc:
-        msg['CC'] = ','.join(cc)
-    msg['Date'] = formatdate()
-    msg['Message-ID'] = make_msgid()
-    msg['Subject'] = Header(subject, charset)
-    # See RFC 3834 section 5:
-    msg['Auto-Submitted'] = 'auto-generated'
-
-    if cfg.mail_sendmail:
-        if bcc:
-            # Set the BCC.  This will be stripped later by sendmail.
-            msg['BCC'] = ','.join(bcc)
-        # Set Return-Path so that it isn't set (generally incorrectly) for us.
-        msg['Return-Path'] = address
 
     # Send the message
-    if not cfg.mail_sendmail:
+    if app.on_gae:
+        from google.appengine.api import mail
         try:
-            logging.debug("trying to send mail (smtp) via smtp server '{0}'".format(cfg.mail_smarthost))
-            host, port = (cfg.mail_smarthost + ':25').split(':')[:2]
-            server = smtplib.SMTP(host, int(port))
+            logging.debug("trying to send mail (mail.EmailMessage.send() on gae)")
+            message = mail.EmailMessage(sender=mail_from,
+                                        subject=subject,
+                                        to=to,
+                                        body=text)
+            if cc:
+                message.cc = cc
+            if bcc:
+                message.bcc = bcc
+            message.send()
+        except:
+            logging.exception("gae's mail.sendmail failed with an exception.")
+            return (0, _("Mail not sent"))
+        return (1, _("Mail sent successfully"))
+    else:
+        import smtplib, socket
+        from email.Message import Message
+        from email.Charset import Charset, QP
+        from email.Utils import formatdate, make_msgid
+
+        subject = subject.encode(config.charset)
+        text = text.encode(config.charset)
+        # Create a message using config.charset and quoted printable
+        # encoding, which should be supported better by mail clients.
+        # TODO: check if its really works better for major mail clients
+        msg = Message()
+        charset = Charset(config.charset)
+        charset.header_encoding = QP
+        charset.body_encoding = QP
+        msg.set_charset(charset)
+
+        # work around a bug in python 2.4.3 and above:
+        msg.set_payload('=')
+        if msg.as_string().endswith('='):
+            text = charset.body_encode(text)
+
+        msg.set_payload(text)
+
+        address = encodeAddress(mail_from, charset)
+        msg['From'] = address
+        if to:
+            msg['To'] = ','.join(to)
+        if cc:
+            msg['CC'] = ','.join(cc)
+        msg['Date'] = formatdate()
+        msg['Message-ID'] = make_msgid()
+        msg['Subject'] = Header(subject, charset)
+        # See RFC 3834 section 5:
+        msg['Auto-Submitted'] = 'auto-generated'
+
+        if cfg.mail_sendmail:
+            if bcc:
+                # Set the BCC.  This will be stripped later by sendmail.
+                msg['BCC'] = ','.join(bcc)
+            # Set Return-Path so that it isn't set (generally incorrectly) for us.
+            msg['Return-Path'] = address
+
+        if not cfg.mail_sendmail:
             try:
-                #server.set_debuglevel(1)
-                if cfg.mail_username is not None and cfg.mail_password is not None:
-                    try: # try to do tls
-                        server.ehlo()
-                        if server.has_extn('starttls'):
-                            server.starttls()
-                            server.ehlo()
-                            logging.debug("tls connection to smtp server established")
-                    except:
-                        logging.debug("could not establish a tls connection to smtp server, continuing without tls")
-                    logging.debug("trying to log in to smtp server using account '{0}'".format(cfg.mail_username))
-                    server.login(cfg.mail_username, cfg.mail_password)
-                server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string())
-            finally:
+                logging.debug("trying to send mail (smtp) via smtp server '{0}'".format(cfg.mail_smarthost))
+                host, port = (cfg.mail_smarthost + ':25').split(':')[:2]
+                server = smtplib.SMTP(host, int(port))
                 try:
-                    server.quit()
-                except AttributeError:
-                    # in case the connection failed, SMTP has no "sock" attribute
-                    pass
-        except smtplib.SMTPException as e:
-            logging.exception("smtp mail failed with an exception.")
-            return (0, str(e))
-        except (os.error, socket.error) as e:
-            logging.exception("smtp mail failed with an exception.")
-            return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s",
-                server=cfg.mail_smarthost,
-                reason=str(e)
-            ))
-    else:
-        try:
-            logging.debug("trying to send mail (sendmail)")
-            sendmailp = os.popen(cfg.mail_sendmail, "w")
-            # msg contains everything we need, so this is a simple write
-            sendmailp.write(msg.as_string())
-            sendmail_status = sendmailp.close()
-            if sendmail_status:
-                logging.error("sendmail failed with status: {0!s}".format(sendmail_status))
-                return (0, str(sendmail_status))
-        except:
-            logging.exception("sendmail failed with an exception.")
-            return (0, _("Mail not sent"))
+                    #server.set_debuglevel(1)
+                    if cfg.mail_username is not None and cfg.mail_password is not None:
+                        try: # try to do tls
+                            server.ehlo()
+                            if server.has_extn('starttls'):
+                                server.starttls()
+                                server.ehlo()
+                                logging.debug("tls connection to smtp server established")
+                        except:
+                            logging.debug("could not establish a tls connection to smtp server, continuing without tls")
+                        logging.debug("trying to log in to smtp server using account '{0}'".format(cfg.mail_username))
+                        server.login(cfg.mail_username, cfg.mail_password)
+                    server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string())
+                finally:
+                    try:
+                        server.quit()
+                    except AttributeError:
+                        # in case the connection failed, SMTP has no "sock" attribute
+                        pass
+            except smtplib.SMTPException as e:
+                logging.exception("smtp mail failed with an exception.")
+                return (0, str(e))
+            except (os.error, socket.error) as e:
+                logging.exception("smtp mail failed with an exception.")
+                return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s",
+                    server=cfg.mail_smarthost,
+                    reason=str(e)
+                ))
+        else:
+            try:
+                logging.debug("trying to send mail (sendmail)")
+                sendmailp = os.popen(cfg.mail_sendmail, "w")
+                # msg contains everything we need, so this is a simple write
+                sendmailp.write(msg.as_string())
+                sendmail_status = sendmailp.close()
+                if sendmail_status:
+                    logging.error("sendmail failed with status: {0!s}".format(sendmail_status))
+                    return (0, str(sendmail_status))
+            except:
+                logging.exception("sendmail failed with an exception.")
+                return (0, _("Mail not sent"))
 
-    logging.debug("Mail sent successfully")
-    return (1, _("Mail sent successfully"))
+        logging.debug("Mail sent successfully")
+        return (1, _("Mail sent successfully"))
 
 
 def encodeSpamSafeEmail(email_address, obfuscation_text=''):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/static/robots.txt	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,35 @@
+User-agent: *
+Crawl-delay: 20
+Disallow: /+convert/
+Disallow: /+dom/
+Disallow: /+download/
+Disallow: /+modify/
+Disallow: /+content/
+Disallow: /+delete/
+Disallow: /+ajaxdelete/
+Disallow: /+ajaxdestroy/
+Disallow: /+ajaxmodify/
+Disallow: /+destroy/
+Disallow: /+rename/
+Disallow: /+revert/
+Disallow: /+index/
+Disallow: /+jfu-server/
+Disallow: /+sitemap/
+Disallow: /+similar_names/
+Disallow: /+quicklink/
+Disallow: /+subscribe/
+Disallow: /+backrefs/
+Disallow: /+wanteds/
+Disallow: /+orphans/
+Disallow: /+register
+Disallow: /+recoverpass
+Disallow: /+usersettings
+Disallow: /+login
+Disallow: /+logout
+Disallow: /+bookmark
+Disallow: /+diff/
+Disallow: /+diffraw/
+Disallow: /+search
+Disallow: /+dispatch/
+Disallow: /+admin/
+Allow: /
--- a/MoinMoin/storage/middleware/indexing.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/storage/middleware/indexing.py	Sat Dec 01 15:00:46 2012 +0100
@@ -93,6 +93,7 @@
 
 
 WHOOSH_FILESTORAGE = 'FileStorage'
+WHOOSH_GAE = 'DatastoreStorage'
 INDEXES = [LATEST_REVS, ALL_REVS, ]
 
 
@@ -332,6 +333,9 @@
                 params[0] += '.temp'
             from whoosh.filedb.filestore import FileStorage
             cls = FileStorage
+        elif kind == WHOOSH_GAE:
+            from whoosh.filedb.gae import DatastoreStorage
+            cls = DatastoreStorage
         else:
             raise ValueError("index_storage = {0!r} is not supported!".format(kind))
         return kind, cls, params, kw
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/storage/stores/gae.py	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,87 @@
+# Copyright: 2012 MoinMoin:GuidoVanRossum
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Google App Engine store
+
+Store into Google App Engine datastore (using NDB), one entity per k/v pair.
+"""
+
+from __future__ import absolute_import, division
+
+import cStringIO as StringIO
+import logging
+
+from google.appengine.ext import ndb
+
+from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+
+
+class _MoinDirectory(ndb.Model):
+    """Used as a parent key."""
+
+
+class _MoinValue(ndb.Model):
+    """Used to store a value.
+
+    The parent is a _MoinDirectory key (but no _MoinDirectory
+    object exists).
+    """
+
+    value = ndb.BlobProperty()
+
+
+class _Store(MutableStoreBase):
+    """Keys and uris are required to be valid key names.
+
+    (This is not an onerous requirement.)
+    """
+
+    @classmethod
+    def from_uri(cls, uri):
+        return cls(uri)
+
+    def __init__(self, path):
+        logging.info('%s(%r)', self.__class__.__name__, path)
+        self._root_key = ndb.Key(_MoinDirectory, path)
+        self._query = _MoinValue.query(ancestor=self._root_key)
+
+    def create(self):
+        """Nothing to do."""
+
+    def destroy(self):
+        self._query.map(self._destroy_key, keys_only=True)
+
+    def _destroy_key(self, key):
+        return key.delete()
+
+    def __iter__(self):
+        return self._query.iter(keys_only=True)
+
+    def _getitem(self, key):
+        ent = _MoinValue.get_by_id(key, parent=self._root_key)
+        return ent and ent.value or 'null'
+
+    def _setitem(self, key, value):
+        _MoinValue(value=value, id=key, parent=self._root_key).put()
+
+    def __delitem__(self, key):
+        ndb.Key(_MoinValue, key, parent=self._root_key).delete()
+
+
+class BytesStore(_Store, BytesMutableStoreBase):
+
+    def __getitem__(self, key):
+        return self._getitem(key)
+
+    def __setitem__(self, key, value):
+        self._setitem(key, value)
+
+
+class FileStore(_Store, FileMutableStoreBase):
+
+    def __getitem__(self, key):
+        return StringIO.StringIO(self._getitem(key))
+
+    def __setitem__(self, key, stream):
+        return self._setitem(key, stream.read())
--- a/MoinMoin/templates/layout.html	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/templates/layout.html	Sat Dec 01 15:00:46 2012 +0100
@@ -51,12 +51,14 @@
                     <a href="{{ url_for('frontend.usersettings') }}" class="moin-usersettings" rel="nofollow">{{ _('Settings') }}</a>
                 {%- endif %}
             {%- endif %}
-            {% if user.auth_method in cfg.auth_can_logout %}
-                <span class="sep"> | </span>
-                <a href="{{ url_for('frontend.logout', logout_submit=1) }}" class="moin-logout" rel="nofollow">
-                    {{ _('Logout') }}
-                </a>
+            {% set logout_url = theme_supp.logout_url() %}
+            {% if logout_url %}
+                    <span class="sep"> | </span>
+                    <a href="{{ logout_url }}" class="moin-logout" rel="nofollow">
+                        {{ _('Logout') }}
+                    </a>
             {% endif %}
+                
         {% else %}
             {% set login_url = theme_supp.login_url() %}
             {% if login_url %}
--- a/MoinMoin/themes/__init__.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/MoinMoin/themes/__init__.py	Sat Dec 01 15:00:46 2012 +0100
@@ -276,11 +276,23 @@
         """
         url = None
         if self.cfg.auth_login_inputs == ['special_no_input']:
-            url = url_for('frontend.login', login=1)
+            url = url_for('frontend.login', login_submit=1)
         if self.cfg.auth_have_login:
             url = url or url_for('frontend.login')
         return url
 
+    def logout_url(self):
+        """
+        Return URL usable for user logout
+
+        :rtype: unicode (or None, if no logout url is supported)
+        :returns: url for user logout
+        """
+        url = None
+        if flaskg.user and flaskg.user.auth_method in self.cfg.auth_can_logout:
+            url = url_for('frontend.logout', logout_submit=1)
+        return url
+
 
 def get_editor_info(meta, external=False):
     addr = meta.get(ADDRESS)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app.yaml	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,54 @@
+application: moin2-test
+version: dev
+runtime: python27
+api_version: 1
+threadsafe: true
+
+env_variables:
+  MOINCFG: ../wikiconfig_gae.py
+
+handlers:
+- url: /%2Bserve/anywikidraw
+  static_dir: support/xstatic/pkg/anywikidraw/data
+
+- url: /%2Bserve/ckeditor
+  static_dir: support/xstatic/pkg/ckeditor/data
+
+- url: /%2Bserve/jquery
+  static_dir: support/xstatic/pkg/jquery/data
+
+- url: /%2Bserve/jquery_file_upload
+  static_dir: support/xstatic/pkg/jquery_file_upload/data
+
+- url: /%2Bserve/svgedit_moin
+  static_dir: support/xstatic/pkg/svgedit_moin/data
+
+- url: /%2Bserve/svgweb
+  static_dir: support/xstatic/pkg/svgweb/data
+
+- url: /%2Bserve/twikidraw_moin
+  static_dir: support/xstatic/pkg/twikidraw_moin/data
+
+- url: /_themes/foobar
+  static_dir: MoinMoin/themes/foobar/static
+
+- url: /_themes/modernized
+  static_dir: MoinMoin/themes/modernized/static
+
+- url: /favicon.ico
+  static_files: MoinMoin/static/logos/favicon.ico
+  upload: MoinMoin/static/logos/favicon.ico
+
+- url: /robots.txt
+  static_files: MoinMoin/static/robots.txt
+  upload: MoinMoin/static/robots.txt
+
+- url: /static
+  static_dir: MoinMoin/static
+
+- url: /.*
+  script: moin.application
+
+# We must include jinja2 in the support directory, so we can use the same
+# support tgz for both GAE and standalone local installations (both do not use
+# setup.py). Thus, we do not use the jinja2 GAE could provide.
--- a/docs/admin/configure.rst	Sun Nov 25 10:03:46 2012 -0700
+++ b/docs/admin/configure.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -1202,6 +1202,17 @@
    add kt store configuration example
 
 
+gae store
+---------
+Features::
+
+* storage for use on google app engine (GAE)
+
+`uri` for `create_simple_mapping` looks like e.g.::
+
+    stores:gae:/%(nsname)s/%(kind)s
+
+
 memory store
 --------------
 Features:
--- a/docs/admin/index.rst	Sun Nov 25 10:03:46 2012 -0700
+++ b/docs/admin/index.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -28,17 +28,25 @@
 
     index_storage = kind, (p1, p2, ...), {kw1=..., kw2=..., ...}
 
-Currently, we only support the 'FileStorage' kind of index storage, which only
-has one parameter - the index directory::
+Currently, we support the following kinds of storages::
+
+FileStorage
+-----------
+Stores into a file system and just has one parameter - the index directory::
 
     index_storage = 'FileStorage', ("/path/to/moin-2.0/wiki/index", ), {}
 
-**Notes for FileStorage:**
+**Notes:**
 
 * The path MUST be absolute, writable and should be on a fast, local filesystem.
 * Moin will use `index.temp` directory as well, if you build an index at
   the `temporary location`.
 
+DatastoreStorage
+----------------
+Stores into google app engine (GAE) storage, has no parameter::
+
+    index_storage = 'DatastoreStorage', (), {}
 
 moin index script reference
 ===========================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/admin/install_gae.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,33 @@
+===============================
+Installing on Google App Engine
+===============================
+
+These are just some additional hints that only apply for GAE, see the
+standard installation docs for the generic stuff.
+
+Note: currently, you need moin-2.0 repo's "gae" branch::
+
+ hg up -C gae
+
+You do NOT need to:
+* run quickinstall or otherwise use virtualenv/pip (as the result of this
+  is not useful for GAE)
+* create an index (will be done automatically)
+* create a storage (will be done automatically)
+
+Instead, do this:
+* download http://static.moinmo.in/files/moin2-support.tgz
+* unpack it into the toplevel directory of the repo workdir (there should
+  be a "support" directory on the same level as the "MoinMoin" directory
+  after unpacking). These are all the dependencies moin needs for production
+  packaged together.
+
+Then, have a look into app.yaml and make sure that "application" is the same
+as in your GAE site settings and the env_variables MOINCFG setting is correct.
+
+Then try running the dev_appserver.py from the Google GAE SDK and point it to
+the toplevel directory (== your workdir).
+
+If that works, deploy the files using appcfg.py.
+
+Usage of dev_appserver and appcfg is documented in the GAE SDK documentation.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/admin/install_standalone.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,32 @@
+================================
+Installing a standalone MoinMoin
+================================
+
+These are just some additional hints that only apply for standalone
+installation, see the standard installation docs for the generic stuff.
+
+Note: currently, you need moin-2.0 repo's "gae" branch::
+
+ hg up -C gae
+
+You do NOT need to:
+* run quickinstall or otherwise use virtualenv/pip (as the result of this
+  is not useful for standalone operation)
+
+Instead, do this:
+* download http://static.moinmo.in/files/moin2-support.tgz
+* unpack it into the toplevel directory of the repo workdir (there should
+  be a "support" directory on the same level as the "MoinMoin" directory
+  after unpacking). These are all the dependencies moin needs for production
+  packaged together.
+
+Then try using "moin.py" from the toplevel directory instead of the "moin"
+command::
+
+    python moin.py index-create -s -i  # create an index and a storage
+    python moin.py                     # run the standalone server
+
+If you are on some POSIX OS (like Linux), this should also work::
+
+    ./moin.py index-create -s -i  # create an index and a storage
+    ./moin.py                     # run the standalone server
--- a/docs/admin/requirements.rst	Sun Nov 25 10:03:46 2012 -0700
+++ b/docs/admin/requirements.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -16,6 +16,7 @@
   debugging, development, adhoc-wikis, etc.
 * apache with mod_wsgi is recommended for bigger/busier wikis.
 * other WSGI-compatible servers or middlewares are usable
+* Google App Engine (experimental)
 * For cgi, fastcgi, scgi, ajp, etc., you can use the "flup" middleware:
   http://trac.saddi.com/flup
 * IIS with ISAPI-WSGI gateway is also compatible: http://code.google.com/p/isapi-wsgi/
@@ -27,5 +28,6 @@
 For dependency information, please see setup.py.
 
 If you use easy_install or pip or our ``quickinstall`` script, then
-dependencies are usually automatically dealt with.
+dependencies are usually automatically dealt with. Alternatively, you can
+use a support archive that contains all the dependencies.
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/devel/create_support_archive.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,29 @@
+Creating the support directory / archive
+========================================
+
+The support directory / archive contains all of MoinMoin's dependencies for
+installation scenarios where using virtualenv / pip / setup.py is not possible
+or not wanted.
+
+Usually you can just use the ready-to-use moin2-support.tgz as described in
+the GAE / standalone installation instructions.
+
+But in case you have to recreate it (e.g. to update it), here are some hints:
+
+* make sure you run some POSIX OS (like Linux), Windows is currently not
+  supported. You also need "make", "find", "tar" and "gzip" (but usually you
+  should have them).
+* make sure that you have a virtualenv "env" in the moin workdir (this is
+  the default place and name used by the quickinstall script)
+* make sure you use Python 2.7.x
+* from the repo workdir, run::
+
+    make support
+
+This will create a "support" directory with all the dependencies needed for
+running moin. For more infos about how this is done, see the Makefile.
+
+To create the moin2-support.tgz, just run additionally::
+
+    make supporttgz
+
--- a/docs/index.rst	Sun Nov 25 10:03:46 2012 -0700
+++ b/docs/index.rst	Sat Dec 01 15:00:46 2012 +0100
@@ -33,6 +33,8 @@
 
    admin/requirements
    admin/install
+   admin/install_standalone
+   admin/install_gae
    admin/serve
    admin/configure
    admin/changes
@@ -57,6 +59,7 @@
    :maxdepth: 2
 
    devel/development
+   devel/create_support_archive
 
 Autogenerated API docs
 ======================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/moin.py	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# Copyright: 2012 MoinMoin:TarashishMishra
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+""" This module serves as the entry point for GAE. The standalone server is also
+called from this module.
+"""
+
+import os
+import sys
+
+support_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'support'))
+if support_path not in sys.path:
+    sys.path.insert(0, support_path)
+server_sw = os.environ.get('SERVER_SOFTWARE', '')
+gae = server_sw.startswith('Development') or server_sw.startswith('Google')
+
+if gae:
+    from MoinMoin import log
+    log.configured = True  # TODO: without this, it crashes/hangs on GAE
+    # Hack: If there are no DatastoreFile instances assume we must create the index.
+    from whoosh.filedb.gae import DatastoreFile
+    create_index = DatastoreFile.all().get() is None
+    # Create the WSGI application object.
+    from MoinMoin.app import create_app
+    application = create_app(create_index=create_index)
+    application.on_gae = True  # GAE specific code can check this
+
+
+elif __name__ == '__main__':
+    from MoinMoin.script import main
+    main()
--- a/setup.cfg	Sun Nov 25 10:03:46 2012 -0700
+++ b/setup.cfg	Sat Dec 01 15:00:46 2012 +0100
@@ -40,7 +40,7 @@
 directory = MoinMoin/translations/
 
 [pytest]
-norecursedirs = .hg _build tmp* env* dlc wiki
+norecursedirs = .hg _build tmp* env* dlc wiki support
 minversion = 2.0
 pep8ignore =
  *.py E121 E122 E123 E124 E125 E126 E127 E128  # continuation line indentation
--- a/setup.py	Sun Nov 25 10:03:46 2012 -0700
+++ b/setup.py	Sat Dec 01 15:00:46 2012 +0100
@@ -74,7 +74,7 @@
     zip_safe=False,
     dependency_links=[
         #'https://github.com/mitsuhiko/werkzeug/tarball/master#egg=Werkzeug-0.7dev',
-        'https://bitbucket.org/thomaswaldmann/whoosh/get/2.4x.tar.gz#egg=Whoosh-2.4.99dev',
+        'https://bitbucket.org/thomaswaldmann/whoosh/get/default.tar.gz#egg=Whoosh-2.5.99dev',
         # fixed flask-themes, 0.1.3 does not work for flask 0.8.x, thus we use a faked 0.1.3.1:
         'https://bitbucket.org/thomaswaldmann/flask-themes/get/24dcc703953f.tar.gz#egg=Flask-Themes-0.1.3.1',
         'https://bitbucket.org/thomaswaldmann/emeraldtree/get/tip.tar.gz#egg=emeraldtree-0.9.1',
@@ -98,7 +98,7 @@
                              # likely due to the fixtures changes.
         'pytest-pep8<1.0.3', # coding style checker
                              # note: pytest-pep8 1.0.3 needs pytest 2.3
-        'whoosh>=2.4.0', # needed for indexed search
+        'whoosh>=2.5.0', # needed for indexed search
         'sphinx>=1.1', # needed to build the docs
         'pdfminer', # pdf -> text/plain conversion
         'XStatic>=0.0.2', # support for static file pypi packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikiconfig_gae.py	Sat Dec 01 15:00:46 2012 +0100
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+"""
+MoinMoin Wiki - Configuration
+
+Developers can use this configuration to run moin right from their mercurial workdir.
+"""
+
+import os
+
+from MoinMoin.config.default import DefaultConfig
+from MoinMoin.storage import create_simple_mapping
+from MoinMoin.util.interwiki import InterWikiMap
+from MoinMoin.auth.gae import GAEAuthMoin
+
+
+class Config(DefaultConfig):
+    # Directory containing THIS wikiconfig:
+    wikiconfig_dir = os.path.abspath(os.path.dirname(__file__))
+    # We assume this structure for a simple "unpack and run" scenario:
+    # wikiconfig.py
+    # wiki/
+    #      data/
+    #      index/
+    # contrib/
+    #      interwiki/
+    #          intermap.txt
+    # If that's not true, feel free to adjust the pathes.
+    instance_dir = os.path.join(wikiconfig_dir, 'wiki')
+    data_dir = os.path.join(instance_dir, 'data') # Note: this used to have a trailing / in the past
+    index_storage = 'DatastoreStorage', (), {}
+
+    # This provides a simple default setup for your backend configuration.
+    # 'stores:fs:...' indicates that you want to use the filesystem backend.
+    namespace_mapping, acl_mapping = create_simple_mapping(
+                            uri='stores:gae:/%(nsname)s/%(kind)s',
+                            # XXX we use rather relaxed ACLs for the development wiki:
+                            content_acl=dict(before=u'',
+                                             default=u'All:read,write,create,destroy,admin',
+                                             after=u'',
+                                             hierarchic=False, ),
+                            user_profile_acl=dict(before=u'',
+                                                  default=u'All:read,write,create,destroy,admin',
+                                                  after=u'',
+                                                  hierarchic=False, ),
+                            )
+
+    #item_root = u'Home' # front page
+
+    # for display purposes:
+    sitename = u'My MoinMoin'
+    # it is required that you set this to a unique, stable and non-empty name:
+    interwikiname = u'MyMoinMoin'
+    # Load the interwiki map from intermap.txt:
+    interwiki_map = InterWikiMap.from_file(os.path.join(wikiconfig_dir, 'contrib', 'interwiki', 'intermap.txt')).iwmap
+    # we must add entries for 'Self' and our interwikiname:
+    interwiki_map[interwikiname] = 'http://127.0.0.1:8080/'
+    interwiki_map['Self'] = 'http://127.0.0.1:8080/'
+
+    # setup static files' serving:
+    serve_files = dict(
+        docs=os.path.join(wikiconfig_dir, 'docs', '_build', 'html'),  # html docs made by sphinx
+    )
+    # see https://bitbucket.org/thomaswaldmann/xstatic for infos about xstatic:
+    from xstatic.main import XStatic
+    mod_names = ['jquery', 'jquery_file_upload',
+                 'ckeditor',
+                 'svgweb',
+                 'svgedit_moin', 'twikidraw_moin', 'anywikidraw',
+                ]
+    pkg = __import__('xstatic.pkg', fromlist=mod_names)
+    for mod_name in mod_names:
+        mod = getattr(pkg, mod_name)
+        xs = XStatic(mod, root_url='/static', provider='local', protocol='http')
+        serve_files.update([(xs.name, xs.base_dir)])
+    auth = [GAEAuthMoin(), ]
+
+MOINCFG = Config # Flask only likes uppercase stuff
+# Flask settings - see the flask documentation about their meaning
+SECRET_KEY = 'you need to change this so it is really secret'
+#DEBUG = False # use True for development only, not for public sites!
+#TESTING = False
+#SESSION_COOKIE_NAME = 'session'
+#PERMANENT_SESSION_LIFETIME = timedelta(days=31)
+#USE_X_SENDFILE = False
+#LOGGER_NAME = 'MoinMoin'
+#config for flask-cache:
+#CACHE_TYPE = 'filesystem'
+#CACHE_DIR = '/path/to/flask-cache-dir'
+
+# DEVELOPERS! Do not add your configuration items here - you could accidentally
+# commit them! Instead, create a wikiconfig_local.py file containing this:
+#
+#from wikiconfig_editme import *
+#
+# In wikiconfig_editme.py (the indirection is needed so that the auto reload
+# mechanism of the builtin server works) you do this:
+#
+#from wikiconfig import *
+#
+#class LocalConfig(Config):
+#    configuration_item_1 = 'value1'
+#
+#MOINCFG = LocalConfig
+#DEBUG = True