changeset 605:04b90498ea47 pytest2

merged with default
author pkumar <contactprashantat@gmail.com>
date Fri, 01 Jul 2011 10:18:38 +0530
parents 6b926426e3d2 (current diff) 7a81ff51008a (diff)
children caf03e340f75
files MoinMoin/converter/_tests/test_moinwiki19_in.py MoinMoin/converter/_tests/test_moinwiki_in.py MoinMoin/items/_tests/test_Item.py MoinMoin/themes/modernized/static/img/white_clouds.png setup.py
diffstat 39 files changed, 547 insertions(+), 443 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/apps/__init__.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/apps/__init__.py	Fri Jul 01 10:18:38 2011 +0530
@@ -0,0 +1,13 @@
+# Copyright: 2011 MoinMoin:ThomasWaldmann
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - flask modules for better modularization
+
+This package contains some Flask Modules:
+
+- frontend has all usual wiki user interface code
+- feed Module for all feed-like stuff
+- admin Module for special stuff for wiki admins
+- serve Module for static file serving
+"""
--- a/MoinMoin/apps/frontend/views.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/apps/frontend/views.py	Fri Jul 01 10:18:38 2011 +0530
@@ -40,7 +40,6 @@
 from MoinMoin.items import ROWS_META, COLS, ROWS_DATA
 from MoinMoin import config, user, wikiutil
 from MoinMoin.config import CONTENTTYPE, ITEMLINKS, ITEMTRANSCLUSIONS
-from MoinMoin.util.forms import make_generator
 from MoinMoin.util import crypto
 from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
 from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, AccessDeniedError
@@ -70,6 +69,7 @@
 Crawl-delay: 20
 Disallow: /+convert/
 Disallow: /+dom/
+Disallow: /+download/
 Disallow: /+modify/
 Disallow: /+copy/
 Disallow: /+delete/
@@ -108,9 +108,40 @@
     return app.send_static_file('logos/favicon.ico')
 
 
-@frontend.route('/<itemname:item_name>', defaults=dict(rev=-1))
-@frontend.route('/+show/<int:rev>/<itemname:item_name>')
+class ValidSearch(Validator):
+    """Validator for a valid search form
+    """
+    too_short_query_msg = L_('Search query too short.')
+
+    def validate(self, element, state):
+        if element['q'].value is None:
+            # no query, nothing to search for
+            return False
+        if len(element['q'].value) < 2:
+            return self.note_error(element, state, 'too_short_query_msg')
+        return True
+
+class SearchForm(Form):
+    q = String.using(optional=False).with_properties(autofocus=True, placeholder=L_("Search Query"))
+    submit = String.using(default=L_('Search'), optional=True)
+
+    validators = [ValidSearch()]
+
+
+def _search(query):
+    return "searching not implemented yet, query: %r" % query
+
+
+@frontend.route('/<itemname:item_name>', defaults=dict(rev=-1), methods=['GET', 'POST'])
+@frontend.route('/+show/<int:rev>/<itemname:item_name>', methods=['GET', 'POST'])
 def show_item(item_name, rev):
+    # first check whether we have a valid search query:
+    search_form = SearchForm.from_flat(request.values)
+    if search_form.validate():
+        query = search_form['q'].value
+        return _search(query)
+    search_form['submit'].set_default() # XXX from_flat() kills all values
+
     flaskg.user.addTrail(item_name)
     item_displayed.send(app._get_current_object(),
                         item_name=item_name)
@@ -140,6 +171,7 @@
                               data_rendered=Markup(item._render_data()),
                               show_revision=show_revision,
                               show_navigation=show_navigation,
+                              search_form=search_form,
                              )
     return Response(content, status)
 
@@ -217,6 +249,15 @@
         abort(403)
     return item.do_get()
 
+@frontend.route('/+download/<int:rev>/<itemname:item_name>')
+@frontend.route('/+download/<itemname:item_name>', defaults=dict(rev=-1))
+def download_item(item_name, rev):
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    return item.do_get(force_attachment=True)
+
 @frontend.route('/+convert/<itemname:item_name>')
 def convert_item(item_name):
     """
@@ -243,12 +284,13 @@
         abort(403)
     return converted_item._convert(item.internal_representation())
 
+
 @frontend.route('/+modify/<itemname:item_name>', methods=['GET', 'POST'])
 def modify_item(item_name):
     """Modify the wiki item item_name.
 
     On GET, displays a form.
-    On POST, saves the new page (unless there's an error in input, or cancelled).
+    On POST, saves the new page (unless there's an error in input).
     After successful POST, redirects to the page.
     """
     contenttype = request.values.get('contenttype')
@@ -257,42 +299,9 @@
         item = Item.create(item_name, contenttype=contenttype)
     except AccessDeniedError:
         abort(403)
-    if request.method == 'GET':
-        if not flaskg.user.may.write(item_name):
-            abort(403)
-        content = item.do_modify(template_name)
-        return content
-    elif request.method == 'POST':
-        cancelled = 'button_cancel' in request.form
-        if not cancelled:
-            form = TextChaizedForm.from_flat(request.form)
-            TextCha(form).amend_form()
-            valid = form.validate()
-            if not valid:
-                data_text = request.values.get('data_text')
-                meta_text = item.meta_dict_to_text(item.meta)
-                comment = request.values.get('comment')
-                return render_template(item.template,
-                                       item_name=item_name,
-                                       gen=make_generator(),
-                                       form=form,
-                                       data_text=data_text,
-                                       meta_text=meta_text,
-                                       comment=comment,
-                                       cols=COLS,
-                                       rows_data=ROWS_DATA,
-                                       rows_meta=ROWS_META,
-                                      )
-            try:
-                item.modify()
-                item_modified.send(app._get_current_object(),
-                                   item_name=item_name)
-                if contenttype in ('application/x-twikidraw', 'application/x-anywikidraw', 'application/x-svgdraw'):
-                    # TWikiDraw/AnyWikiDraw/SvgDraw POST more than once, redirecting would break them
-                    return "OK"
-            except AccessDeniedError:
-                abort(403)
-        return redirect(url_for('frontend.show_item', item_name=item_name))
+    if not flaskg.user.may.write(item_name):
+        abort(403)
+    return item.do_modify(contenttype, template_name)
 
 
 class CommentForm(TextChaizedForm):
@@ -330,15 +339,13 @@
     elif request.method == 'POST':
         form = RevertItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        valid = form.validate()
-        if valid:
+        if form.validate():
             item.revert()
             return redirect(url_for('frontend.show_item', item_name=item_name))
     return render_template(item.revert_template,
                            item=item, item_name=item_name,
                            rev_no=rev,
                            form=form,
-                           gen=make_generator(),
                           )
 
 
@@ -355,8 +362,7 @@
     elif request.method == 'POST':
         form = CopyItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        valid = form.validate()
-        if valid:
+        if form.validate():
             target = form['target'].value
             comment = form['comment'].value
             item.copy(target, comment)
@@ -364,7 +370,6 @@
     return render_template(item.copy_template,
                            item=item, item_name=item_name,
                            form=form,
-                           gen=make_generator(),
                           )
 
 
@@ -381,8 +386,7 @@
     elif request.method == 'POST':
         form = RenameItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        valid = form.validate()
-        if valid:
+        if form.validate():
             target = form['target'].value
             comment = form['comment'].value
             item.rename(target, comment)
@@ -390,7 +394,6 @@
     return render_template(item.rename_template,
                            item=item, item_name=item_name,
                            form=form,
-                           gen=make_generator(),
                           )
 
 
@@ -406,15 +409,13 @@
     elif request.method == 'POST':
         form = DeleteItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        valid = form.validate()
-        if valid:
+        if form.validate():
             comment = form['comment'].value
             item.delete(comment)
             return redirect(url_for('frontend.show_item', item_name=item_name))
     return render_template(item.delete_template,
                            item=item, item_name=item_name,
                            form=form,
-                           gen=make_generator(),
                           )
 
 
@@ -438,8 +439,7 @@
     elif request.method == 'POST':
         form = DestroyItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        valid = form.validate()
-        if valid:
+        if form.validate():
             comment = form['comment'].value
             item.destroy(comment=comment, destroy_item=destroy_item)
             return redirect(url_for('frontend.show_item', item_name=item_name))
@@ -447,7 +447,6 @@
                            item=item, item_name=item_name,
                            rev_no=rev,
                            form=form,
-                           gen=make_generator(),
                           )
 
 
@@ -560,15 +559,6 @@
     return refs_here
 
 
-@frontend.route('/+search')
-def search():
-    return _search()
-
-
-def _search(**args):
-    return "searching for %r not implemented yet" % args
-
-
 @frontend.route('/+history/<itemname:item_name>')
 def history(item_name):
     history = flaskg.storage.history(item_name=item_name)
@@ -808,18 +798,17 @@
             form = OpenIDForm.from_flat(request.form)
             TextCha(form).amend_form()
 
-            valid = form.validate()
-            if valid:
-                    msg = user.create_user(username=form['username'].value,
-                                           password=form['password1'].value,
-                                           email=form['email'].value,
-                                           openid=form['openid'].value,
-                                          )
-                    if msg:
-                        flash(msg, "error")
-                    else:
-                        flash(_('Account created, please log in now.'), "info")
-                        return redirect(url_for('frontend.show_root'))
+            if form.validate():
+                msg = user.create_user(username=form['username'].value,
+                                       password=form['password1'].value,
+                                       email=form['email'].value,
+                                       openid=form['openid'].value,
+                                      )
+                if msg:
+                    flash(msg, "error")
+                else:
+                    flash(_('Account created, please log in now.'), "info")
+                    return redirect(url_for('frontend.show_root'))
 
     else:
         # not openid registration and no MoinAuth
@@ -835,8 +824,7 @@
             form = RegistrationForm.from_flat(request.form)
             TextCha(form).amend_form()
 
-            valid = form.validate()
-            if valid:
+            if form.validate():
                 msg = user.create_user(username=form['username'].value,
                                        password=form['password1'].value,
                                        email=form['email'].value,
@@ -850,7 +838,6 @@
 
     return render_template(template,
                            item_name=item_name,
-                           gen=make_generator(),
                            form=form,
                           )
 
@@ -892,8 +879,7 @@
         form = PasswordLostForm.from_defaults()
     elif request.method == 'POST':
         form = PasswordLostForm.from_flat(request.form)
-        valid = form.validate()
-        if valid:
+        if form.validate():
             u = None
             username = form['username'].value
             if username:
@@ -909,7 +895,6 @@
             return redirect(url_for('frontend.show_root'))
     return render_template('lostpass.html',
                            item_name=item_name,
-                           gen=make_generator(),
                            form=form,
                           )
 
@@ -956,8 +941,7 @@
         form.update(request.values)
     elif request.method == 'POST':
         form = PasswordRecoveryForm.from_flat(request.form)
-        valid = form.validate()
-        if valid:
+        if form.validate():
             u = user.User(user.getUserId(form['username'].value))
             if u and u.valid and u.apply_recovery_token(form['token'].value, form['password1'].value):
                 flash(_("Your password has been changed, you can log in now."), "info")
@@ -966,7 +950,6 @@
             return redirect(url_for('frontend.show_root'))
     return render_template('recoverpass.html',
                            item_name=item_name,
-                           gen=make_generator(),
                            form=form,
                           )
 
@@ -1030,8 +1013,7 @@
                 flash(hint, "info")
     elif request.method == 'POST':
         form = LoginForm.from_flat(request.form)
-        valid = form.validate()
-        if valid:
+        if form.validate():
             # we have a logged-in, valid user
             return redirect(url_for('frontend.show_root'))
         # flash the error messages (if any)
@@ -1040,7 +1022,6 @@
     return render_template('login.html',
                            item_name=item_name,
                            login_inputs=app.cfg.auth_login_inputs,
-                           gen=make_generator(),
                            form=form,
                           )
 
@@ -1166,8 +1147,7 @@
         form['submit'].set_default() # XXX from_object() kills all values
     elif request.method == 'POST':
         form = FormClass.from_flat(request.form)
-        valid = form.validate()
-        if valid:
+        if form.validate():
             # successfully modified everything
             success = True
             if part == 'password':
@@ -1201,7 +1181,6 @@
     return render_template('usersettings.html',
                            item_name=item_name,
                            part=part,
-                           gen=make_generator(),
                            form=form,
                           )
 
--- a/MoinMoin/config/default.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/config/default.py	Fri Jul 01 10:18:38 2011 +0530
@@ -352,7 +352,7 @@
     ('item_views', [
         # (endpointname, label, check_item_exists
         ('frontend.show_item', L_('Show'), L_('Show'), False, ),
-        ('frontend.get_item', L_('Download'), L_('Download'), True, ),
+        ('frontend.download_item', L_('Download'), L_('Download'), True, ),
         ('frontend.history', L_('History'), L_('Revision History'), True, ),
         # note: when rendering a non-existing item, you'll be offered to
         # create it (in the content area), so we do not offer "Modify":
--- a/MoinMoin/converter/_tests/test_moinwiki19_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/_tests/test_moinwiki19_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -27,6 +27,13 @@
                 '<page><body><p><a xlink:href="wiki://Self/FrontPage">FrontPage</a></p></body></page>'),
             (u'http://moinmo.in/',
                 '<page><body><p><a xlink:href="http://moinmo.in/">http://moinmo.in/</a></p></body></page>'),
+            # email tests
+            (u'mailto:foo@bar.baz',
+                '<page><body><p><a xlink:href="mailto:foo@bar.baz">mailto:foo@bar.baz</a></p></body></page>'),
+            (u'foo@bar.baz',
+                '<page><body><p><a xlink:href="mailto:foo@bar.baz">foo@bar.baz</a></p></body></page>'),
+            (u'foo@bar', # 1.9 requires domain
+                '<page><body><p>foo@bar</p></body></page>'),
         ]
         for i in data:
             yield (self.do, ) + i
--- a/MoinMoin/converter/_tests/test_moinwiki_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/_tests/test_moinwiki_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -274,6 +274,35 @@
         ]
         for i in data:
             yield (self.do, ) + i
+    def test_interwiki(self):
+        data = [
+            (u'[[MoinMoin:RecentChanges]]',
+                '<page><body><p><a xlink:href="wiki://MoinMoin/RecentChanges">RecentChanges</a></p></body></page>'),
+            (u'[[MoinMoin:RecentChanges|changes]]',
+                '<page><body><p><a xlink:href="wiki://MoinMoin/RecentChanges">changes</a></p></body></page>'),
+            (u'[[MoinMoin:Foo/Bar.Baz]]',
+                '<page><body><p><a xlink:href="wiki://MoinMoin/Foo/Bar.Baz">Foo/Bar.Baz</a></p></body></page>'),
+            (u'[[MoinMoin:Blank In Page Name|blank in page name]]',
+                '<page><body><p><a xlink:href="wiki://MoinMoin/Blank%20In%20Page%20Name">blank in page name</a></p></body></page>'),
+            (u'[[InvalidWikiName:RecentChanges]]',
+                '<page><body><p><a xlink:href="wiki.local:InvalidWikiName:RecentChanges">InvalidWikiName:RecentChanges</a></p></body></page>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def test_email(self):
+        data = [
+            (u'[[mailto:root]]',
+                '<page><body><p><a xlink:href="mailto:root">mailto:root</a></p></body></page>'),
+            (u'[[mailto:foo@bar.baz]]',
+                '<page><body><p><a xlink:href="mailto:foo@bar.baz">mailto:foo@bar.baz</a></p></body></page>'),
+            (u'[[mailto:foo@bar.baz|write me]]',
+                '<page><body><p><a xlink:href="mailto:foo@bar.baz">write me</a></p></body></page>'),
+            (u'[[mailto:foo.bar_baz@bar.baz]]', # . and _ are special characters commonly allowed by email systems
+                '<page><body><p><a xlink:href="mailto:foo.bar_baz@bar.baz">mailto:foo.bar_baz@bar.baz</a></p></body></page>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
 
     def serialize(self, elem, **options):
         from StringIO import StringIO
--- a/MoinMoin/converter/audio_video_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/audio_video_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -26,11 +26,11 @@
     def __init__(self, input_type):
         self.input_type = input_type
 
-    def __call__(self, content):
-        item_name = content # we just give the name of the item in the content
+    def __call__(self, rev):
+        item_name = rev.item.name
         attrib = {
             moin_page.type_: unicode(self.input_type),
-            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get'),
+            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev=%d' % rev.revno),
         }
         return moin_page.object_(attrib=attrib, children=[u'Your Browser does not support HTML5 audio/video element.', ])
 
--- a/MoinMoin/converter/everything.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/everything.py	Fri Jul 01 10:18:38 2011 +0530
@@ -21,10 +21,10 @@
     def _factory(cls, input, output, **kw):
         return cls()
 
-    def __call__(self, content):
-        item_name = content # we just give the name of the item in the content
+    def __call__(self, rev):
+        item_name = rev.item.name
         attrib = {
-            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get'),
+            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev=%d' % rev.revno),
         }
         return moin_page.a(attrib=attrib, children=["Download %s." % item_name])
 
--- a/MoinMoin/converter/html_out.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/html_out.py	Fri Jul 01 10:18:38 2011 +0530
@@ -344,13 +344,9 @@
         else:
             # Nothing else worked...try using <object>
             return "object"
+
     def visit_moinpage_object(self, elem):
         href = elem.get(xlink.href, None)
-        if href:
-            if isinstance(href, unicode): # XXX sometimes we get Iri, sometimes unicode - bug?
-                h = href
-            else: # Iri
-                h = href.path[-1] # XXX BUG Iri doesn't have a path if we access the root page (eg. http://google.de doesn't have a path)
         attrib = {}
         mimetype = Type(_type=elem.get(moin_page.type_, 'application/x-nonexistent'))
         # Get the object type
--- a/MoinMoin/converter/image_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/image_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -24,11 +24,11 @@
     def __init__(self, input_type):
         self.input_type = input_type
 
-    def __call__(self, content):
-        item_name = content # we just give the name of the item in the content
+    def __call__(self, rev):
+        item_name = rev.item.name
         attrib = {
             moin_page.type_: unicode(self.input_type),
-            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get'),
+            xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev=%d' % rev.revno),
         }
         return moin_page.object_(attrib=attrib, children=[item_name, ])
 
--- a/MoinMoin/converter/link.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/link.py	Fri Jul 01 10:18:38 2011 +0530
@@ -105,14 +105,15 @@
         if links == 'extern':
             return cls(url_root=url_root)
 
-    def _get_do(self, query):
+    def _get_do_rev(self, query):
         """
-        get 'do' value from query string and remove do=value from querystring
+        get 'do' and 'rev' values from query string and remove them from querystring
 
         Note: we can't use url_decode/url_encode from e.g. werkzeug because
               url_encode quotes the qs values (and Iri code will quote them again)
         """
         do = None
+        revno = None
         separator = '&'
         result = []
         if query:
@@ -126,22 +127,27 @@
                 if k == 'do':
                     do = v
                     continue # we remove do=xxx from qs
+                if k == 'rev':
+                    revno = v
+                    continue # we remove rev=n from qs
                 result.append(u'%s=%s' % (k, v))
         if result:
             query = separator.join(result)
         else:
             query = None
-        return do, query
+        if revno is not None:
+            revno = int(revno)
+        return do, revno, query
 
     def handle_wiki_links(self, elem, input):
-        do, query = self._get_do(input.query)
+        do, revno, query = self._get_do_rev(input.query)
         link = Iri(query=query, fragment=input.fragment)
 
         if input.authority and input.authority.host:
             # interwiki link
             wikitag, wikiurl, wikitail, err = resolve_interwiki(unicode(input.authority.host), unicode(input.path[1:]))
             if not err:
-                elem.set(html.class_, 'interwiki')
+                elem.set(html.class_, 'moin-interwiki')
                 if do is not None:
                     # this will only work for wikis with compatible URL design
                     # for other wikis, don't use do=... in your interwiki links
@@ -156,16 +162,18 @@
 
         if not input.authority or err:
             # local wiki link
+            path = input.path[1:]
+            if revno is not None:
+                path = IriPath('%d/' % revno) + path
             if do is not None:
-                link.path = IriPath('+' + do + '/') + input.path[1:]
-            else:
-                link.path = input.path[1:]
+                path = IriPath('+%s/' % do) + path
+            link.path = path
             base = self.url_root
 
         elem.set(self._tag_xlink_href, base + link)
 
     def handle_wikilocal_links(self, elem, input, page):
-        do, query = self._get_do(input.query)
+        do, revno, query = self._get_do_rev(input.query)
         link = Iri(query=query, fragment=input.fragment)
 
         if input.path:
@@ -177,10 +185,11 @@
         else:
             path = page.path[1:]
 
+        if revno is not None:
+            path = IriPath('%d/' % revno) + path
         if do is not None:
-            link.path = IriPath('+' + do + '/') + path
-        else:
-            link.path = path
+            path = IriPath('+%s/') + path
+        link.path = path
         output = self.url_root + link
 
         elem.set(self._tag_xlink_href, output)
--- a/MoinMoin/converter/moinwiki19_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/moinwiki19_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -130,8 +130,7 @@
                 )
             )
             (?P<url_target>
-                # TODO: config.url_schemas
-                (http|https|ftp|nntp|news|mailto|telnet|file|irc):
+                (%(url_schemas)s):
                 \S+?
             )
             (
@@ -145,7 +144,7 @@
                 )
             )
         )
-    """
+    """ % dict(url_schemas='|'.join(config.url_schemas))
 
     def inline_url_repl(self, stack, url, url_target):
         url = Iri(url_target)
@@ -165,3 +164,4 @@
 default_registry.register(ConverterFormat19.factory, Type('text/x.moin.wiki;format=1.9'), type_moin_document)
 default_registry.register(ConverterFormat19.factory, Type('x-moin/format;name=wiki;format=1.9'), type_moin_document)
 
+
--- a/MoinMoin/converter/moinwiki_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/moinwiki_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -21,7 +21,7 @@
 from MoinMoin import config
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.tree import html, moin_page, xlink, xinclude
-
+from MoinMoin.util.interwiki import resolve_interwiki
 from ._args import Arguments
 from ._args_wiki import parse as parse_arguments
 from ._wiki_macro import ConverterMacro
@@ -748,11 +748,16 @@
             \s*
             (
                 (?P<link_url>
-                    [a-zA-Z0-9+.-]+
-                    ://
+                    (%(url_schemas)s):
                     [^|]+?
                 )
                 |
+                (
+                    (?P<link_interwiki_site>[A-Z][a-zA-Z]+)
+                    :
+                    (?P<link_interwiki_item>[^|]+) # accept any item name; will verify link_interwiki_site below
+                )
+                |
                 (?P<link_item> [^|]+? )
             )
             \s*
@@ -770,11 +775,29 @@
             )?
             \]\]
         )
-    """
+    """ % dict(url_schemas='|'.join(config.url_schemas))
 
     def inline_link_repl(self, stack, link, link_url=None, link_item=None,
-            link_text=None, link_args=None):
+            link_text=None, link_args=None,
+            link_interwiki_site=None, link_interwiki_item=None):
         """Handle all kinds of links."""
+        if link_interwiki_site:
+            err = resolve_interwiki(link_interwiki_site, link_interwiki_item)[3]
+            if not err:
+                link = Iri(scheme='wiki',
+                        authority=link_interwiki_site,
+                        path='/' + link_interwiki_item)
+                element = moin_page.a(attrib={xlink.href: link})
+                stack.push(element)
+                if link_text:
+                    self.parse_inline(link_text, stack, self.inlinedesc_re)
+                else:
+                    stack.top_append(link_interwiki_item)
+                stack.pop()
+                return
+            else:
+                # assume local language uses ":" inside of words, set link_item and continue
+                link_item = '%s:%s' % (link_interwiki_site, link_interwiki_item)
         if link_args:
             link_args = parse_arguments(link_args) # XXX needs different parsing
             query = url_encode(link_args.keyword, charset=config.charset, encode_keys=True)
@@ -1052,6 +1075,7 @@
         inline_macro,
         inline_nowiki,
         inline_emphstrong,
+        inline_object,
     )
     inlinedesc_re = re.compile('|'.join(inlinedesc), re.X | re.U)
 
@@ -1112,3 +1136,4 @@
 default_registry.register(Converter.factory, type_moin_wiki, type_moin_document)
 default_registry.register(Converter.factory, Type('x-moin/format;name=wiki'), type_moin_document)
 
+
--- a/MoinMoin/converter/nonexistent_in.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/converter/nonexistent_in.py	Fri Jul 01 10:18:38 2011 +0530
@@ -22,8 +22,8 @@
     def _factory(cls, input, output, **kw):
         return cls()
 
-    def __call__(self, content):
-        item_name = content # we just give the name of the item in the content
+    def __call__(self, rev):
+        item_name = rev.item.name
         attrib = {
             xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=modify'),
         }
--- a/MoinMoin/items/__init__.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/items/__init__.py	Fri Jul 01 10:18:38 2011 +0530
@@ -1,5 +1,5 @@
 # Copyright: 2009 MoinMoin:ThomasWaldmann
-# Copyright: 2009 MoinMoin:ReimarBauer
+# Copyright: 2009-2011 MoinMoin:ReimarBauer
 # Copyright: 2009 MoinMoin:ChristopherDenter
 # Copyright: 2008,2009 MoinMoin:BastianBlank
 # Copyright: 2010 MoinMoin:ValentinJaniaut
@@ -22,8 +22,12 @@
 from StringIO import StringIO
 from array import array
 
+from flatland import Form, String, Integer, Boolean, Enum
+from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted
+from MoinMoin.util.forms import FileStorage
+
 from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
-from MoinMoin.util.forms import make_generator
+from MoinMoin.signalling import item_modified
 from MoinMoin.util.mimetype import MimeType
 from MoinMoin.util.mime import Type, type_moin_document
 from MoinMoin.util.tree import moin_page, html, xlink, docbook
@@ -48,7 +52,8 @@
 from flask import current_app as app
 from flask import g as flaskg
 
-from flask import request, url_for, Response, abort, escape
+from flask import request, url_for, flash, Response, redirect, abort, escape
+
 from werkzeug import is_resource_modified
 from jinja2 import Markup
 
@@ -214,7 +219,7 @@
         return ''
 
     def feed_input_conv(self):
-        return self.name
+        return self.rev
 
     def internal_representation(self, converters=['smiley']):
         """
@@ -413,31 +418,44 @@
 
     def modify(self):
         # called from modify UI/POST
+        meta = data = contenttype_guessed = None
+        contenttype_qs = request.values.get('contenttype')
         data_file = request.files.get('data_file')
-        contenttype = request.values.get('contenttype', 'text/plain;charset=utf-8')
-        if data_file and data_file.filename:
-            # user selected a file to upload
+        if data_file and data_file.filename: # XXX is this the right way to check if there was a file uploaded?
             data = data_file.stream
-            contenttype = MimeType(filename=data_file.filename).content_type()
-        else:
-            # take text from textarea
-            data = request.form.get('data_text', u'') # we get unicode from the form
-            if data:
+            # this is likely a guess by the browser, based on the filename
+            contenttype_guessed = data_file.content_type # comes from form multipart data
+        if data is None:
+            # no file upload, try taking stuff from textarea
+            data = request.form.get('data_text')
+            if data is not None:
+                # there was a data_text field with (possibly empty) content
+                assert isinstance(data, unicode) # we get unicode from the form
                 data = self.data_form_to_internal(data)
                 data = self.data_internal_to_storage(data)
-                contenttype = 'text/plain;charset=utf-8' # XXX is there a way to get the charset of the form?
-            else:
-                data = '' # could've been u'' also!
-                contenttype = None
-        meta_text = request.form.get('meta_text', '')
-        try:
+                # we know it is text and utf-8 - XXX is there a way to get the charset of the form?
+                contenttype_guessed = 'text/plain;charset=utf-8'
+        # data might be None here, if we have a form with just the data_file field, no file was uploaded
+        # and no data_text field. this can happen if just metadata of a non-text item is edited.
+
+        meta_text = request.form.get('meta_text')
+        if meta_text is not None:
+            # there was a meta_text field with (possibly empty) content
+            # Note: if you get crashes here, please see the ValidJSON validator
+            # to catch invalid json issues early.
             meta = self.meta_text_to_dict(meta_text)
-        except ValueError:
-            meta = {} # XXX maybe rather validate and reject invalid json
+        if meta is None:
+            # no form metadata - reuse some stuff from previous metadata?
+            meta = {}
+
+        if contenttype_qs:
+            # we use querystring param to FORCE content type
+            meta[CONTENTTYPE] = contenttype_qs
+
         comment = request.form.get('comment')
-        return self._save(meta, data, contenttype=contenttype, comment=comment)
+        return self._save(meta, data, contenttype_guessed=contenttype_guessed, comment=comment)
 
-    def _save(self, meta, data, name=None, action=u'SAVE', contenttype=None, comment=u''):
+    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u''):
         if name is None:
             name = self.name
         backend = flaskg.storage
@@ -448,11 +466,11 @@
         try:
             currentrev = storage_item.get_revision(-1)
             rev_no = currentrev.revno
-            if contenttype is None:
-                # if we didn't get contenttype info, thus reusing the one from current rev:
-                contenttype = currentrev.get(CONTENTTYPE)
+            contenttype_current = currentrev.get(CONTENTTYPE)
         except NoSuchRevisionError:
+            currentrev = None
             rev_no = -1
+            contenttype_current = None
         new_rev_no = rev_no + 1
         newrev = storage_item.create_revision(new_rev_no)
         for k, v in meta.iteritems():
@@ -467,22 +485,29 @@
             newrev[NAME_OLD] = oldname
         newrev[NAME] = name
 
+        if data is None:
+            if currentrev is not None:
+                # we don't have (new) data, just copy the old one.
+                # a valid usecase of this is to just edit metadata.
+                data = currentrev
+            else:
+                data = ''
         size = self._write_stream(data, newrev)
-        timestamp = time.time()
+
         # XXX if meta is from old revision, and user did not give a non-empty
         # XXX comment, re-using the old rev's comment is wrong behaviour:
         comment = unicode(comment or meta.get(COMMENT, ''))
         if comment:
             newrev[COMMENT] = comment
-        # allow override by form- / qs-given contenttype:
-        contenttype = request.values.get('contenttype', contenttype)
-        # allow override by give metadata:
-        assert contenttype is not None
-        newrev[CONTENTTYPE] = unicode(meta.get(CONTENTTYPE, contenttype))
+
+        if CONTENTTYPE not in newrev:
+            # make sure we have CONTENTTYPE
+            newrev[CONTENTTYPE] = unicode(contenttype_current or contenttype_guessed or 'application/octet-stream')
+
         newrev[ACTION] = unicode(action)
         self.before_revision_commit(newrev, data)
         storage_item.commit()
-        # XXX Event ?
+        item_modified.send(app._get_current_object(), item_name=name)
         return new_rev_no, size
 
     def before_revision_commit(self, newrev, data):
@@ -608,13 +633,13 @@
         ]),
     ]
 
-    def do_get(self):
+    def do_get(self, force_attachment=False):
         abort(404)
 
     def _convert(self):
         abort(404)
 
-    def do_modify(self, template_name):
+    def do_modify(self, contenttype, template_name):
         # XXX think about and add item template support
         return render_template('modify_show_type_selection.html',
                                item_name=self.name,
@@ -623,6 +648,18 @@
 
 item_registry.register(NonExistent._factory, Type('application/x-nonexistent'))
 
+class ValidJSON(Validator):
+    """Validator for JSON
+    """
+    invalid_json_msg = L_('Invalid JSON.')
+
+    def validate(self, element, state):
+        try:
+            json.loads(element.value)
+        except:
+            return self.note_error(element, state, 'invalid_json_msg')
+        return True
+
 
 class Binary(Item):
     """ An arbitrary binary item, fallback class for every item mimetype. """
@@ -653,20 +690,36 @@
         items = [item.name for item in item_iterator]
         return sorted(items)
 
-    def do_modify(self, template_name):
+    def do_modify(self, contenttype, template_name):
         # XXX think about and add item template support
         #if template_name is None and isinstance(self.rev, DummyRev):
         #    return self._do_modify_show_templates()
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
+        from MoinMoin.apps.frontend.views import CommentForm
+        class ModifyForm(CommentForm):
+            rev = Integer.using(optional=False)
+            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
+            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+
+        if request.method == 'GET':
+            form = ModifyForm.from_defaults()
+            TextCha(form).amend_form()
+            form['meta_text'] = self.meta_dict_to_text(self.meta)
+            form['rev'] = self.rev.revno if self.rev.revno is not None else -1
+        elif request.method == 'POST':
+            form = ModifyForm.from_flat(request.form.items() + request.files.items())
+            TextCha(form).amend_form()
+            if form.validate():
+                try:
+                    self.modify() # XXX
+                except AccessDeniedError:
+                    abort(403)
+                else:
+                    return redirect(url_for('frontend.show_item', item_name=self.name))
         return render_template(self.template,
                                item_name=self.name,
-                               rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               meta_text=self.meta_dict_to_text(self.meta),
+                               rows_meta=str(ROWS_META), cols=str(COLS),
                                help=self.modify_help,
                                form=form,
-                               gen=make_generator(),
                               )
 
     copy_template = 'copy.html'
@@ -690,45 +743,39 @@
         return _("Impossible to convert the data to the contenttype: %(contenttype)s",
                  contenttype=request.values.get('contenttype'))
 
-    def do_get(self):
+    def do_get(self, force_attachment=False):
         hash = self.rev.get(HASH_ALGORITHM)
         if is_resource_modified(request.environ, hash): # use hash as etag
-            return self._do_get_modified(hash)
+            return self._do_get_modified(hash, force_attachment=force_attachment)
         else:
             return Response(status=304)
 
-    def _do_get_modified(self, hash):
+    def _do_get_modified(self, hash, force_attachment=False):
         member = request.values.get('member')
-        return self._do_get(hash, member)
+        return self._do_get(hash, member, force_attachment=force_attachment)
 
-    def _do_get(self, hash, member=None):
-        filename = None
+    def _do_get(self, hash, member=None, force_attachment=False):
         if member: # content = file contained within a archive item revision
             path, filename = os.path.split(member)
             mt = MimeType(filename=filename)
-            content_disposition = mt.content_disposition(app.cfg)
-            content_type = mt.content_type()
             content_length = None
             file_to_send = self.get_member(member)
         else: # content = item revision
             rev = self.rev
+            filename = rev.item.name
             try:
                 mimestr = rev[CONTENTTYPE]
             except KeyError:
-                mt = MimeType(filename=rev.item.name)
+                mt = MimeType(filename=filename)
             else:
                 mt = MimeType(mimestr=mimestr)
-            content_disposition = mt.content_disposition(app.cfg)
-            content_type = mt.content_type()
             content_length = rev[SIZE]
             file_to_send = rev
-
-        # TODO: handle content_disposition is not None
-        # Important: empty filename keeps flask from trying to autodetect filename,
-        # as this would not work for us, because our file's are not necessarily fs files.
+        content_type = mt.content_type()
+        as_attachment = force_attachment or mt.as_attachment(app.cfg)
         return send_file(file=file_to_send,
                          mimetype=content_type,
-                         as_attachment=False, attachment_filename=filename,
+                         as_attachment=as_attachment, attachment_filename=filename,
                          cache_timeout=10, # wiki data can change rapidly
                          add_etags=True, etag=hash, conditional=True)
 
@@ -806,14 +853,15 @@
             # everything we expected has been added to the tar file, save the container as revision
             meta = {CONTENTTYPE: self.contenttype}
             data = open(temp_fname, 'rb')
-            self._save(meta, data, name=self.name, action=u'SAVE', contenttype=self.contenttype, comment='')
+            self._save(meta, data, name=self.name, action=u'SAVE', comment='')
             data.close()
             os.remove(temp_fname)
 
 
 class ApplicationXTar(TarMixin, Application):
-    def feed_input_conv(self):
-        return self.rev
+    """
+    Tar items
+    """
 
 item_registry.register(ApplicationXTar._factory, Type('application/x-tar'))
 item_registry.register(ApplicationXTar._factory, Type('application/x-gtar'))
@@ -847,8 +895,9 @@
 
 
 class ApplicationZip(ZipMixin, Application):
-    def feed_input_conv(self):
-        return self.rev
+    """
+    Zip items
+    """
 
 item_registry.register(ApplicationZip._factory, Type('application/zip'))
 
@@ -946,7 +995,7 @@
         outfile.close()
         return content_type, data
 
-    def _do_get_modified(self, hash):
+    def _do_get_modified(self, hash, force_attachment=False):
         try:
             width = int(request.values.get('w'))
         except (TypeError, ValueError):
@@ -980,7 +1029,7 @@
                 headers, data = c
             return Response(data, headers=headers)
         else:
-            return self._do_get(hash)
+            return self._do_get(hash, force_attachment=force_attachment)
 
     def _render_data_diff(self, oldrev, newrev):
         if PIL is None:
@@ -998,7 +1047,7 @@
         c = app.cache.get(cid)
         if c is None:
             if PIL is None:
-                abort(404)
+                abort(404) # TODO render user friendly error image
 
             content_type = newrev[CONTENTTYPE]
             if content_type == 'image/jpeg':
@@ -1010,17 +1059,21 @@
             else:
                 raise ValueError("content_type %r not supported" % content_type)
 
-            oldimage = PILImage.open(oldrev)
-            newimage = PILImage.open(newrev)
-            oldimage.load()
-            newimage.load()
-            diffimage = PILdiff(newimage, oldimage)
-            outfile = StringIO()
-            diffimage.save(outfile, output_type)
-            data = outfile.getvalue()
-            outfile.close()
-            headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
-            app.cache.set(cid, (headers, data))
+            try:
+                oldimage = PILImage.open(oldrev)
+                newimage = PILImage.open(newrev)
+                oldimage.load()
+                newimage.load()
+                diffimage = PILdiff(newimage, oldimage)
+                outfile = StringIO()
+                diffimage.save(outfile, output_type)
+                data = outfile.getvalue()
+                outfile.close()
+                headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
+                app.cache.set(cid, (headers, data))
+            except (IOError, ValueError) as err:
+                logging.exception("error during PILdiff: %s", err.message)
+                abort(404) # TODO render user friendly error image
         else:
             # XXX TODO check ACL behaviour
             headers, data = c
@@ -1093,27 +1146,44 @@
         doc = html_conv(doc)
         return conv_serialize(doc, {html.namespace: ''})
 
-    def do_modify(self, template_name):
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
-        if template_name is None and isinstance(self.rev, DummyRev):
-            return self._do_modify_show_templates()
-        if template_name:
-            item = Item.create(template_name)
-            data_text = self.data_storage_to_internal(item.data)
-        else:
-            data_text = self.data_storage_to_internal(self.data)
-        meta_text = self.meta_dict_to_text(self.meta)
+    def do_modify(self, contenttype, template_name):
+        # XXX think about and add item template support
+        #if template_name is None and isinstance(self.rev, DummyRev):
+        #    return self._do_modify_show_templates()
+        from MoinMoin.apps.frontend.views import CommentForm
+        class ModifyForm(CommentForm):
+            rev = Integer.using(optional=False)
+            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
+            data_text = String.using(optional=True).with_properties(placeholder=L_("Type your text here"))
+            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+
+        if request.method == 'GET':
+            if template_name is None and isinstance(self.rev, DummyRev):
+                return self._do_modify_show_templates()
+            form = ModifyForm.from_defaults()
+            TextCha(form).amend_form()
+            if template_name:
+                item = Item.create(template_name)
+                form['data_text'] = self.data_storage_to_internal(item.data)
+            else:
+                form['data_text'] = self.data_storage_to_internal(self.data)
+            form['meta_text'] = self.meta_dict_to_text(self.meta)
+            form['rev'] = self.rev.revno if self.rev.revno is not None else -1
+        elif request.method == 'POST':
+            form = ModifyForm.from_flat(request.form.items() + request.files.items())
+            TextCha(form).amend_form()
+            if form.validate():
+                try:
+                    self.modify() # XXX
+                except AccessDeniedError:
+                    abort(403)
+                else:
+                    return redirect(url_for('frontend.show_item', item_name=self.name))
         return render_template(self.template,
                                item_name=self.name,
-                               rows_data=ROWS_DATA, rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               data_text=data_text,
-                               meta_text=meta_text,
-                               lang='en', direction='ltr',
+                               rows_data=str(ROWS_DATA), rows_meta=str(ROWS_META), cols=str(COLS),
                                help=self.modify_help,
                                form=form,
-                               gen=make_generator(),
                               )
 
 item_registry.register(Text._factory, Type('text/*'))
@@ -1190,29 +1260,6 @@
     """
     template = "modify_text_html.html"
 
-    def do_modify(self, template_name):
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
-        if template_name is None and isinstance(self.rev, DummyRev):
-            return self._do_modify_show_templates()
-        if template_name:
-            item = Item.create(template_name)
-            data_text = self.data_storage_to_internal(item.data)
-        else:
-            data_text = self.data_storage_to_internal(self.data)
-        meta_text = self.meta_dict_to_text(self.meta)
-        return render_template(self.template,
-                               item_name=self.name,
-                               rows_data=ROWS_DATA, rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               data_text=data_text,
-                               meta_text=meta_text,
-                               lang='en', direction='ltr',
-                               help=self.modify_help,
-                               form=form,
-                               gen=make_generator(),
-                              )
-
 item_registry.register(HTML._factory, Type('text/html'))
 
 
@@ -1246,8 +1293,8 @@
 
         # We determine the different parameters for the reply
         mt = MimeType(mimestr='application/docbook+xml;charset=utf-8')
-        content_disposition = mt.content_disposition(app.cfg)
         content_type = mt.content_type()
+        as_attachment = mt.as_attachment(app.cfg)
         # After creation of the StringIO, we are at the end of the file
         # so position is the size the file.
         # and then we should move it back at the beginning of the file
@@ -1257,7 +1304,7 @@
         # as this would not work for us, because our file's are not necessarily fs files.
         return send_file(file=file_to_send,
                          mimetype=content_type,
-                         as_attachment=False, attachment_filename=None,
+                         as_attachment=as_attachment, attachment_filename=None,
                          cache_timeout=10, # wiki data can change rapidly
                          add_etags=False, etag=None, conditional=True)
 
@@ -1295,21 +1342,37 @@
         self.put_member('drawing' + ext, filecontent, content_length,
                         expected_members=set(['drawing.draw', 'drawing.map', 'drawing.png']))
 
-    def do_modify(self, template_name):
-        """
-        Fills params into the template for initialzing of the the java applet.
-        The applet is called for doing modifications.
-        """
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
+    def do_modify(self, contenttype, template_name):
+        # XXX think about and add item template support
+        #if template_name is None and isinstance(self.rev, DummyRev):
+        #    return self._do_modify_show_templates()
+        from MoinMoin.apps.frontend.views import CommentForm
+        class ModifyForm(CommentForm):
+            rev = Integer.using(optional=False)
+            # XXX as the "saving" POSTs come from TWikiDraw (not the form), editing meta_text doesn't work
+            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
+            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+
+        if request.method == 'GET':
+            form = ModifyForm.from_defaults()
+            TextCha(form).amend_form()
+            # XXX currently this is rather pointless, as the form does not get POSTed:
+            form['meta_text'] = self.meta_dict_to_text(self.meta)
+            form['rev'] = self.rev.revno if self.rev.revno is not None else -1
+        elif request.method == 'POST':
+            # this POST comes directly from TWikiDraw (not from Browser), thus no validation
+            try:
+                self.modify() # XXX
+            except AccessDeniedError:
+                abort(403)
+            else:
+                # TWikiDraw POSTs more than once, redirecting would break them
+                return "OK"
         return render_template(self.template,
                                item_name=self.name,
-                               rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               meta_text=self.meta_dict_to_text(self.meta),
+                               rows_meta=str(ROWS_META), cols=str(COLS),
                                help=self.modify_help,
                                form=form,
-                               gen=make_generator(),
                               )
 
     def _render_data(self):
@@ -1371,23 +1434,42 @@
         self.put_member('drawing' + ext, filecontent, content_length,
                         expected_members=set(['drawing.svg', 'drawing.map', 'drawing.png']))
 
-    def do_modify(self, template_name):
-        """
-        Fills params into the template for initialzing of the the java applet.
-        The applet is called for doing modifications.
-        """
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
-        drawing_exists = 'drawing.svg' in self.list_members()
+    def do_modify(self, contenttype, template_name):
+        # XXX think about and add item template support
+        #if template_name is None and isinstance(self.rev, DummyRev):
+        #    return self._do_modify_show_templates()
+        from MoinMoin.apps.frontend.views import CommentForm
+        class ModifyForm(CommentForm):
+            rev = Integer.using(optional=False)
+            # XXX as the "saving" POSTs come from AnyWikiDraw (not the form), editing meta_text doesn't work
+            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
+            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+
+        if request.method == 'GET':
+            form = ModifyForm.from_defaults()
+            TextCha(form).amend_form()
+            # XXX currently this is rather pointless, as the form does not get POSTed:
+            form['meta_text'] = self.meta_dict_to_text(self.meta)
+            form['rev'] = self.rev.revno if self.rev.revno is not None else -1
+        elif request.method == 'POST':
+            # this POST comes directly from AnyWikiDraw (not from Browser), thus no validation
+            try:
+                self.modify() # XXX
+            except AccessDeniedError:
+                abort(403)
+            else:
+                # AnyWikiDraw POSTs more than once, redirecting would break them
+                return "OK"
+        try:
+            drawing_exists = 'drawing.svg' in self.list_members()
+        except:
+            drawing_exists = False
         return render_template(self.template,
                                item_name=self.name,
-                               rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               meta_text=self.meta_dict_to_text(self.meta),
+                               rows_meta=str(ROWS_META), cols=str(COLS),
                                help=self.modify_help,
                                drawing_exists=drawing_exists,
                                form=form,
-                               gen=make_generator(),
                               )
 
     def _render_data(self):
@@ -1428,32 +1510,49 @@
 
     def modify(self):
         # called from modify UI/POST
-        file_upload = request.values.get('data')
+        png_upload = request.values.get('png_data')
+        svg_upload = request.values.get('filepath')
         filename = request.form['filename']
-        filecontent = file_upload.decode('base_64')
-        basepath, basename = os.path.split(filename)
-        basename, ext = os.path.splitext(basename)
+        png_content = png_upload.decode('base_64')
+        png_content = base64.urlsafe_b64decode(png_content.split(',')[1])
+        svg_content = svg_upload.decode('base_64')
         content_length = None
-
-        if ext == '.png':
-            filecontent = base64.urlsafe_b64decode(filecontent.split(',')[1])
-        self.put_member(filename, filecontent, content_length,
+        self.put_member("drawing.svg", svg_content, content_length,
+                        expected_members=set(['drawing.svg', 'drawing.png']))
+        self.put_member("drawing.png", png_content, content_length,
                         expected_members=set(['drawing.svg', 'drawing.png']))
 
-    def do_modify(self, template_name):
-        """
-        Fills params into the template for initializing of the applet.
-        """
-        form = TextChaizedForm.from_defaults()
-        TextCha(form).amend_form()
+    def do_modify(self, contenttype, template_name):
+        # XXX think about and add item template support
+        #if template_name is None and isinstance(self.rev, DummyRev):
+        #    return self._do_modify_show_templates()
+        from MoinMoin.apps.frontend.views import CommentForm
+        class ModifyForm(CommentForm):
+            rev = Integer.using(optional=False)
+            # XXX as the "saving" POSTs come from SvgDraw (not the form), editing meta_text doesn't work
+            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
+            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+
+        if request.method == 'GET':
+            form = ModifyForm.from_defaults()
+            TextCha(form).amend_form()
+            # XXX currently this is rather pointless, as the form does not get POSTed:
+            form['meta_text'] = self.meta_dict_to_text(self.meta)
+            form['rev'] = self.rev.revno if self.rev.revno is not None else -1
+        elif request.method == 'POST':
+            # this POST comes directly from SvgDraw (not from Browser), thus no validation
+            try:
+                self.modify() # XXX
+            except AccessDeniedError:
+                abort(403)
+            else:
+                # SvgDraw POSTs more than once, redirecting would break them
+                return "OK"
         return render_template(self.template,
                                item_name=self.name,
-                               rows_meta=ROWS_META, cols=COLS,
-                               revno=0,
-                               meta_text=self.meta_dict_to_text(self.meta),
+                               rows_meta=str(ROWS_META), cols=str(COLS),
                                help=self.modify_help,
                                form=form,
-                               gen=make_generator(),
                               )
 
     def _render_data(self):
--- a/MoinMoin/items/_tests/test_Item.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/items/_tests/test_Item.py	Fri Jul 01 10:18:38 2011 +0530
@@ -46,12 +46,12 @@
         name = u'NewItem'
         contenttype = 'text/plain;charset=utf-8'
         data = 'foobar'
-        meta = dict(foo='bar')
+        meta = {'foo': 'bar', CONTENTTYPE: contenttype}
         comment = u'saved it'
         become_trusted()
         item = Item.create(name)
         # save rev 0
-        item._save(meta, data, contenttype=contenttype, comment=comment)
+        item._save(meta, data, comment=comment)
         # check save result
         item = Item.create(name)
         saved_meta, saved_data = dict(item.meta), item.data
@@ -63,7 +63,7 @@
         data = rev1_data = data * 10000
         comment = comment + u' again'
         # save rev 1
-        item._save(meta, data, contenttype=contenttype, comment=comment)
+        item._save(meta, data, comment=comment)
         # check save result
         item = Item.create(name)
         saved_meta, saved_data = dict(item.meta), item.data
@@ -75,7 +75,7 @@
         data = ''
         comment = 'saved empty data'
         # save rev 2 (auto delete)
-        item._save(meta, data, contenttype=contenttype, comment=comment)
+        item._save(meta, data, comment=comment)
         # check save result
         item = Item.create(name)
         saved_meta, saved_data = dict(item.meta), item.data
@@ -93,7 +93,7 @@
         basename = u'Foo'
         for name in ['', '/ab', '/cd/ef', '/gh', '/ij/kl', ]:
             item = Item.create(basename + name)
-            item._save({}, "foo", contenttype='text/plain;charset=utf-8')
+            item._save({CONTENTTYPE: 'text/plain;charset=utf-8'}, "foo")
 
         # check index
         baseitem = Item.create(basename)
--- a/MoinMoin/static/js/common.js	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/static/js/common.js	Fri Jul 01 10:18:38 2011 +0530
@@ -4,43 +4,6 @@
 
 var QUICKLINKS_EXPAND = ">>>";
 var QUICKLINKS_COLLAPSE = "<<<";
-// We keep here the state of the search box
-searchIsDisabled = false;
-
-function searchChange(e) {
-    // Update search buttons status according to search box content.
-    // Ignore empty or whitespace search term.
-    var value = e.value.replace(/\s+/, '');
-    if (value == '' || searchIsDisabled) {
-        searchSetDisabled(true);
-    } else {
-        searchSetDisabled(false);
-    }
-}
-
-function searchSetDisabled(flag) {
-    // Enable or disable search
-    document.getElementById('moin-fullsearch').disabled = flag;
-    document.getElementById('moin-titlesearch').disabled = flag;
-}
-
-function searchFocus(e) {
-    // Update search input content on focus
-    if (e.value == search_hint) {
-        e.value = '';
-        e.className = '';
-        searchIsDisabled = false;
-    }
-}
-
-function searchBlur(e) {
-    // Update search input content on blur
-    if (e.value == '') {
-        e.value = search_hint;
-        e.className = 'disabled';
-        searchIsDisabled = true;
-    }
-}
 
 // use this instead of assigning to window.onload directly:
 function addLoadEvent(func) {
@@ -786,28 +749,6 @@
         this.selectedIndex = 0;
     });
 
-    // Functions related to search form
-    var e = document.getElementById('moin-searchinput');
-    searchChange(e);
-    searchBlur(e);
-
-    $('#moin-searchinput').blur(function(){
-        searchBlur(this);
-    });
-
-    $('#moin-searchinput').focus(function(){
-        searchFocus(this);
-    });
-
-    $('#moin-searchinput').change(function(){
-        searchChange(this);
-    });
-
-    $('#moin-searchinput').keyup(function(){
-        searchChange(this);
-    });
-
-
 });
 
 // Insert Zero-Width-Space characters into long text strings of textNode elements.
--- a/MoinMoin/storage/_tests/test_middleware_acl.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/storage/_tests/test_middleware_acl.py	Fri Jul 01 10:18:38 2011 +0530
@@ -45,7 +45,7 @@
 
     def test_noaccess(self):
         name = u"noaccess"
-        self.create_item_acl(name, "All:")
+        self.create_item_acl(name, u"All:")
         assert py.test.raises(AccessDeniedError, self.get_item, name)
 
     def test_create_item(self):
@@ -56,7 +56,7 @@
         backend = flaskg.storage
         assert py.test.raises(AccessDeniedError, backend.create_item, u"I will never exist")
 
-        item = self.create_item_acl(u"i will exist!", "All:read,write")
+        item = self.create_item_acl(u"i will exist!", u"All:read,write")
         rev = item.create_revision(1)
         data = "my very existent data"
         rev.write(data)
@@ -65,7 +65,7 @@
 
     def test_read_access_allowed(self):
         name = u"readaccessallowed"
-        self.create_item_acl(name, "All:read")
+        self.create_item_acl(name, u"All:read")
         # Should simply pass...
         item = self.get_item(name)
 
@@ -75,12 +75,12 @@
 
     def test_write_after_create(self):
         name = u"writeaftercreate"
-        item = self.create_item_acl(name, "All:")
+        item = self.create_item_acl(name, u"All:")
         assert py.test.raises(AccessDeniedError, item.create_revision, 1)
 
     def test_modify_without_acl_change(self):
         name = u"copy_without_acl_change"
-        acl = "All:read,write"
+        acl = u"All:read,write"
         self.create_item_acl(name, acl)
         item = self.get_item(name)
         rev = item.create_revision(1)
@@ -90,15 +90,16 @@
 
     def test_copy_with_acl_change(self):
         name = u"copy_with_acl_change"
-        acl = "All:read,write"
+        acl = u"All:read,write"
         self.create_item_acl(name, acl)
         item = self.get_item(name)
         rev = item.create_revision(1)
-        py.test.raises(AccessDeniedError, rev.__setitem__, ACL, acl + ",write")
+        # without admin rights it is disallowed to change ACL
+        py.test.raises(AccessDeniedError, rev.__setitem__, ACL, acl + u",destroy")
 
     def test_write_without_read(self):
         name = u"write_but_not_read"
-        acl = "All:write"
+        acl = u"All:write"
         item = flaskg.storage.create_item(name)
         rev = item.create_revision(0)
         rev[ACL] = acl
--- a/MoinMoin/storage/backends/acl.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/storage/backends/acl.py	Fri Jul 01 10:18:38 2011 +0530
@@ -168,14 +168,15 @@
             item = self.backend.get_item(itemname)
             # we always use the ACLs set on the latest revision:
             current_rev = item.get_revision(-1)
-            acls = current_rev[ACL]
+            acl = current_rev[ACL]
+            if not isinstance(acl, unicode):
+                raise TypeError("%s metadata has unsupported type: %r" % (ACL, acl))
+            acls = [acl, ]
         except (NoSuchItemError, NoSuchRevisionError, KeyError):
             # do not use default acl here
             acls = []
-        if not isinstance(acls, (tuple, list)):
-            acls = (acls, )
         default = self.default.default
-        return ContentACL(self.cfg, acls, default=default, valid=self.valid)
+        return ContentACL(self.cfg, tuple(acls), default=default, valid=self.valid)
 
     def _may(self, itemname, right, username=None):
         """ Check if username may have <right> access on item <itemname>.
@@ -482,7 +483,7 @@
                 last_rev = self._item.get_revision(-1)
                 last_acl = last_rev[ACL]
             except (NoSuchRevisionError, KeyError):
-                last_acl = ''
+                last_acl = u''
 
             acl_changed = value != last_acl
 
--- a/MoinMoin/storage/backends/sqla.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/storage/backends/sqla.py	Fri Jul 01 10:18:38 2011 +0530
@@ -600,6 +600,7 @@
         self._revno = revno
         self.setup(item._backend)
         self._item = item
+        self._item_id = item.id
 
     def __del__(self):
         # XXX XXX XXX DO NOT RELY ON THIS
--- a/MoinMoin/templates/base.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/base.html	Fri Jul 01 10:18:38 2011 +0530
@@ -50,16 +50,20 @@
     <script src="{{ url_for('serve.files', name='jquery', filename='jquery.min.js') }}"></script>
     <script src="{{ url_for('serve.files', name='svgweb', filename='svg.js') }}"></script>
     <script src="{{ url_for('.static', filename='js/common.js') }}"></script>
+    <script type="text/x-mathjax-config">
+  MathJax.Hub.Config({
+    extensions: ["tex2jax.js"],
+    jax: ["input/TeX","output/HTML-CSS"],
+    tex2jax: {inlineMath: [["$","$"],["\\(","\\)"]]}
+  });
+</script>
+<script src="{{ url_for('serve.files', name='mathjax', filename='MathJax.js') }}"> </script>
+ 
     {{ scripts }}
     <!--[if lt IE 9]>
         {# TODO: use a local copy later #}
         <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
     <![endif]-->
-    <script>
-        <!--
-        var search_hint = " {{ _('Search') }}";
-        //-->
-    </script>
     {% endblock %}
 {% endblock %}
 </head>
--- a/MoinMoin/templates/history.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/history.html	Fri Jul 01 10:18:38 2011 +0530
@@ -34,7 +34,7 @@
                 <td class="moin-wordbreak">{{ rev.comment }}</td>
                 <td><a href="{{ url_for('frontend.show_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('show') }}</a></td>
                 <td><a href="{{ url_for('frontend.show_item_meta', item_name=rev.item.name, rev=rev.revno) }}">{{ _('meta') }}</a></td>
-                <td><a href="{{ url_for('frontend.get_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('download') }}</a></td>
+                <td><a href="{{ url_for('frontend.download_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('download') }}</a></td>
                 <td><a href="{{ url_for('frontend.highlight_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('highlight') }}</a></td>
                 <td><a href="{{ url_for('frontend.revert_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('revert') }}</a></td>
                 <td><a href="{{ url_for('frontend.destroy_item', item_name=rev.item.name, rev=rev.revno) }}">{{ _('destroy') }}</a></td>
--- a/MoinMoin/templates/itemviews.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/itemviews.html	Fri Jul 01 10:18:38 2011 +0530
@@ -3,8 +3,8 @@
     {% for endpoint, label, title, check_exists in cfg.item_views if not endpoint in cfg.endpoints_excluded %}
         {% if (not check_exists or check_exists and exists) and endpoint in [
                'frontend.show_item', 'frontend.index', 'frontend.index2',
-               'frontend.highlight_item', 'frontend.show_item_meta', 'frontend.get_item',
-               'frontend.get_item', 'frontend.history', 'frontend.backrefs', 'frontend.sitemap',
+               'frontend.highlight_item', 'frontend.show_item_meta', 'frontend.download_item',
+               'frontend.history', 'frontend.backrefs', 'frontend.sitemap',
                'frontend.similar_names',
                'frontend.modify_item',
                'frontend.copy_item', 'frontend.rename_item', 'frontend.delete_item', 'frontend.destroy_item',
--- a/MoinMoin/templates/layout.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/layout.html	Fri Jul 01 10:18:38 2011 +0530
@@ -1,3 +1,4 @@
+{% import "forms.html" as forms %}
 {% extends "base.html" %}
 
 {% import "snippets.html" as snippets with context %}
@@ -17,16 +18,15 @@
 
 <div id="moin-header">
 {% block header %}
-    <form id="moin-searchform" method="get" action="{{ url_for('frontend.search') }}">
+    {% if search_form %} 
+    {{ gen.form.open(search_form, id='moin-searchform', method='get', action=url_for('frontend.show_item', item_name=item_name)) }}
         <div>
-            <input type="hidden" name="do" value="fullsearch" />
-            <input type="hidden" name="context" value="180" />
-            <label for="moin-searchinput">{{ _('Search') }}</label>
-            <input id="moin-searchinput" type="search" name="value" value="{{ request.values.get('value', '') }}" size="20" />
-            <input id="moin-titlesearch" name="titlesearch" type="submit" value="{{ _('Titles') }}" />
-            <input id="moin-fullsearch" name="fullsearch" type="submit" value="{{ _('Text') }}" />
+            {{ gen.input(search_form['q'], type='search', id='moin-search-query', size='30') }}
+            {{ gen.input(search_form['submit'], type='submit') }}
+            {{ forms.render_errors(search_form) }}
         </div>
-    </form>
+    {{ gen.form.close() }}
+    {% endif %} 
 
     {% if logo %}
     <div id="moin-logo">
--- a/MoinMoin/templates/modify_applet.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/modify_applet.html	Fri Jul 01 10:18:38 2011 +0530
@@ -1,22 +1,18 @@
+{% import "forms.html" as forms %}
 {% extends theme("layout.html") %}
 {% block content %}
 <h1>{{ _("Modifying %(item_name)s", item_name=item_name) }}</h1>
 <div class="moin-form">
-<form action="" method="POST" enctype="multipart/form-data">
-<input type="hidden" name="action" value="modify" />
-<input type="hidden" name="rev" value="{{ revno }}" />
+{{ gen.form.open(form, method='post', action='', enctype='multipart/form-data') }}
+{{ forms.render_errors(form) }}
+{{ gen.input(form['rev'], type='hidden') }}
 {% block extra_form %}{% endblock %}
 {% block data_editor %}{% endblock %}
-    <dl>
-        <dt>
-            <label for="data_file">{{ _("Upload file:") }}</label>
-         </dt>
-         <dd>
-            <input type="file" id="data_file" name="data_file" />
-        </dd>
-    </dl>
+{{ forms.render_field(gen, form['data_file'], 'file') }}
 <pre>{{ help }}</pre>
-<textarea name="meta_text" lang="en" dir="ltr" rows="{{ rows_meta }}" cols="{{ cols }}">{{ meta_text }}</textarea>
-</form>
+{{ gen.textarea(form['meta_text'], lang='en', dir='ltr', rows=rows_meta, cols=cols) }}
+<br />
+{{ forms.render_errors(form['meta_text']) }}
+{{ gen.form.close() }}
 </div>
 {% endblock %}
--- a/MoinMoin/templates/modify_binary.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/modify_binary.html	Fri Jul 01 10:18:38 2011 +0530
@@ -1,10 +1,7 @@
 {% import "forms.html" as forms %}
 {% extends "modify_applet.html" %}
 {% block extra_form %}
-<input class="button" type="submit" name="button_save" value="{{ _("Save") }}" />
-<input class="button" type="submit" name="button_cancel" value="{{ _("Cancel") }}" />
+{{ gen.input(form['submit'], class='button', type='submit') }}
 {{ forms.render_textcha(gen, form) }}
-<br />
-<label for="comment">{{ _("Comment:") }}</label><input type="text" id="comment" name="comment" size="80" maxlength="200" value="{{ comment }}" />
-<br />
+{{ forms.render_field(gen, form['comment'], 'text') }}
 {% endblock %}
--- a/MoinMoin/templates/modify_svg-edit.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/modify_svg-edit.html	Fri Jul 01 10:18:38 2011 +0530
@@ -1,7 +1,7 @@
 {% extends "modify_applet.html" %}
 {% block data_editor %}
 <p>
-<object data="{{ url_for('serve.files', name='svgedit', filename='svg-editor.html') }}?paramurl={{ url_for('frontend.get_item', item_name=item_name, member='drawing.svg') }}&amp;savepath={{ url_for('frontend.modify_item', item_name=item_name) }}&amp;viewpath={{ url_for('frontend.show_item', item_name=item_name) }}" width="100%" height="600">
+<object data="{{ url_for('serve.files', name='svgedit', filename='editor/svg-editor.html') }}?paramurl={{ url_for('frontend.get_item', item_name=item_name, member='drawing.svg') }}" width="100%" height="600">
 </object>
 </p>
 <br />
--- a/MoinMoin/templates/modify_text.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/modify_text.html	Fri Jul 01 10:18:38 2011 +0530
@@ -1,5 +1,5 @@
 {% extends "modify_binary.html" %}
 {% block data_editor %}
-<textarea name="data_text" lang="{{ lang }}" dir="{{ direction }}" rows="{{ rows_data }}" cols="{{ cols }}">{{ data_text }}</textarea>
+{{ gen.textarea(form['data_text'], lang=lang, dir=direction, rows=rows_data, cols=cols) }}
 <br />
 {% endblock %}
--- a/MoinMoin/templates/modify_text_html.html	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/templates/modify_text_html.html	Fri Jul 01 10:18:38 2011 +0530
@@ -8,8 +8,6 @@
 
 {% block data_editor %}
 <p>
-    <textarea class="ckeditor" id="data_text" name="data_text" rows="{{ rows_data }}" cols="{{ cols }}">{{
-        data_text
-    }}</textarea>
+{{ gen.textarea(form['data_text'], class='ckeditor', lang=lang, dir=direction, rows=rows_data, cols=cols) }}
 </p>
 {% endblock %}
--- a/MoinMoin/themes/__init__.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/themes/__init__.py	Fri Jul 01 10:18:38 2011 +0530
@@ -23,6 +23,7 @@
 from MoinMoin.config import USERID, ADDRESS, HOSTNAME
 from MoinMoin.util.interwiki import split_interwiki, resolve_interwiki, join_wiki, getInterwikiHome
 from MoinMoin.util.crypto import cache_key
+from MoinMoin.util.forms import make_generator
 
 
 def get_current_theme():
@@ -392,5 +393,6 @@
                             'cfg': app.cfg,
                             'item_name': 'handlers need to give it',
                             'get_editor_info': lambda rev: get_editor_info(rev),
+                            'gen': make_generator(),
                             })
 
--- a/MoinMoin/themes/modernized/static/css/common.css	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/themes/modernized/static/css/common.css	Fri Jul 01 10:18:38 2011 +0530
@@ -69,9 +69,7 @@
 
 /* dead links */
 a.moin-nonexistent:visited,
-a.moin-nonexistent,
-a.moin-badinterwiki:visited,
-a.moin-badinterwiki { color: #444; }
+a.moin-nonexistent { color: #444; }
 
 /* lists */
 ol, ul { margin-left: 3em; }
@@ -345,7 +343,6 @@
 a.moin-ircs:before { content: url(../img/moin-telnet.png); margin: 0 0.2em; }
 a.moin-mailto:before { content: url(../img/moin-email.png); margin: 0 0.2em; }
 a.moin-attachment:before { content: url(../img/moin-attach.png); margin: 0 0.2em; }
-a.moin-badinterwiki:before { content: url(../img/moin-inter.png); margin: 0 0.2em; }
 a.moin-interwiki:before { content: url(../img/moin-inter.png); margin: 0 0.2em; }
 a.moin-action:before { content: url(../img/moin-action.png); margin: 0 0.2em; }
 
@@ -462,16 +459,17 @@
 
 /* moin-header */
 #moin-header { margin: 0px; padding: 0px;
-            background: url(../img/white_clouds.png); /* background: #E6EAF0; */
+            background: url(../img/white-clouds.jpg); /* background: #E6EAF0; */
             line-height: 1.12em; }
 
 /* moin-header searchform */
-#moin-searchform { margin: 8px .5em; padding: 0; font-size: 0.82em; float: right;  text-align: right; }
+#moin-searchform { margin: 8px .5em; padding: 0; font-size: 0.82em; float: right;  text-align: left; }
 #moin-searchform input { font-size: 100%; vertical-align: middle;
             background-color: #F3F7FD; /* same as body bg col */
             border: 1px solid #A4B9DF; }
-#moin-searchform label { display:none; }
 #moin-searchform div { margin: 0; }
+#moin-searchform ul { border: 1px solid #A4B9DF; margin: 0; padding: 0; background-color: #F3F7FD; }
+#moin-searchform li { list-style:none; }
 
 /* moin-header moin-logo -- logos may be text only */
 #moin-logo { float: left; margin: 5px 10px; padding: 0; font-size: 1.4em; line-height: 1em; font-weight: bold; }
@@ -561,11 +559,11 @@
 html { font-family: Times, serif; font-size: 12pt; width: 100%; }
 body, #moin-page, #moin-page, #moin-content-data { margin: 0; padding: 0; }
 
-a, a:visited, a.nonexistent,
-a.badinterwiki { color: black !important; text-decoration: none !important; }
+a, a:visited,
+a.nonexistent { color: black !important; text-decoration: none !important; }
 
-a.interwiki:before, a.badinterwiki:before { content: attr(title) ":"; }
-a.interwiki img, a.badinterwiki img { display: none; }
+a.interwiki:before { content: attr(title) ":"; }
+a.interwiki img { display: none; }
 
 pre { font-size: 10pt; }
 .footnotes div { width: 5em; border-top: 1pt solid gray; }
Binary file MoinMoin/themes/modernized/static/img/white-clouds.jpg has changed
Binary file MoinMoin/themes/modernized/static/img/white_clouds.png has changed
--- a/MoinMoin/util/forms.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/util/forms.py	Fri Jul 01 10:18:38 2011 +0530
@@ -85,3 +85,18 @@
                               error_filter,
                               required_filter, placeholder_filter, autofocus_filter])
 
+
+# other flatland stuff
+
+from flatland import AdaptationError, Scalar
+import werkzeug
+
+
+class FileStorage(Scalar):
+    """Schema element for Werkzeug FileStorage instances."""
+
+    def adapt(self, value):
+        if not isinstance(value, (type(None), werkzeug.FileStorage)):
+            raise AdaptationError
+        return value
+
--- a/MoinMoin/util/mimetype.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/util/mimetype.py	Fri Jul 01 10:18:38 2011 +0530
@@ -163,21 +163,12 @@
         """ return a string major/minor only, no params """
         return "%s/%s" % (self.major, self.minor)
 
-    def content_disposition(self, cfg):
+    def as_attachment(self, cfg):
         # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
         # we just let the user store them to disk ('attachment').
         # For safe files, we directly show them inline (this also works better for IE).
         mime_type = self.mime_type()
-        dangerous = mime_type in cfg.mimetypes_xss_protect
-        content_disposition = dangerous and 'attachment' or 'inline'
-        filename = self.filename
-        if filename is not None:
-            # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
-            # There is no solution that is compatible to IE except stripping non-ascii chars
-            if isinstance(filename, unicode):
-                filename = filename.encode(config.charset)
-            content_disposition += '; filename="%s"' % filename
-        return content_disposition
+        return mime_type in cfg.mimetypes_xss_protect
 
     def module_name(self):
         """ convert this mimetype to a string useable as python module name,
--- a/MoinMoin/wikiutil.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/MoinMoin/wikiutil.py	Fri Jul 01 10:18:38 2011 +0530
@@ -320,15 +320,12 @@
             pass
 
 
-def file_headers(filename=None,
-                 content_type=None, content_length=None, content_disposition=None):
+def file_headers(filename=None, content_type=None, content_length=None):
         """
         Compute http headers for sending a file
 
-        :param filename: filename for content-disposition header and for autodetecting
-                         content_type (unicode, default: None)
+        :param filename: filename for autodetecting content_type (unicode, default: None)
         :param content_type: content-type header value (str, default: autodetect from filename)
-        :param content_disposition: type for content-disposition header (str, default: None)
         :param content_length: for content-length header (int, default:None)
         """
         if filename:
@@ -349,9 +346,5 @@
         headers = [('Content-Type', content_type)]
         if content_length is not None:
             headers.append(('Content-Length', str(content_length)))
-        if content_disposition is None and mt is not None:
-            content_disposition = mt.content_disposition(app.cfg)
-        if content_disposition:
-            headers.append(('Content-Disposition', content_disposition))
         return headers
 
--- a/quickinstall	Thu Jun 30 23:33:58 2011 +0530
+++ b/quickinstall	Fri Jul 01 10:18:38 2011 +0530
@@ -2,7 +2,7 @@
 # create a virtual environment in directory $DIR/
 # needs: curl or wget, unzip, gzip, tar, pip
 
-DIR=env
+DIR=envxs
 PYTHON=python
 
 virtualenv --no-site-packages --python $PYTHON $DIR
@@ -35,15 +35,15 @@
 download $DIR/svgedit.tgz http://static.moinmo.in/files/packages/svg-edit.tar.gz
 tar xz -C $DIR/ -f $DIR/svgedit.tgz
 
+download $DIR/mathjax.tgz http://static.moinmo.in/files/packages/mathjax.tar.gz
+tar xz -C $DIR/ -f $DIR/mathjax.tgz
+
 mkdir $DIR/jquery.fu
 download $DIR/jquery.fu/jquery.fileupload.js https://www.github.com/blueimp/jQuery-File-Upload/raw/master/jquery.fileupload.js
 download $DIR/jquery.fu/jquery.fileupload-ui.js https://www.github.com/blueimp/jQuery-File-Upload/raw/master/jquery.fileupload-ui.js
 download $DIR/jquery.fu/jquery.fileupload-ui.css https://www.github.com/blueimp/jQuery-File-Upload/raw/master/jquery.fileupload-ui.css
 download $DIR/jquery.fu/pbar-ani.gif https://www.github.com/blueimp/jQuery-File-Upload/raw/master/pbar-ani.gif
 
-mkdir $DIR/jquery
-download $DIR/jquery/jquery.min.js http://code.jquery.com/jquery-1.4.4.min.js
-
 download $DIR/svgweb.zip http://svgweb.googlecode.com/files/svgweb-2010-08-10-Owlephant-1.zip
 unzip -q -o -d $DIR/ $DIR/svgweb.zip
 
@@ -56,6 +56,8 @@
 
 # first install babel, moin's setup.py will emit a warning if it is not there
 pip install babel
+# first install XStatic, XStatic-jQuery's setup.py will fail if it is not there
+pip install XStatic
 
 # "install" moin2 from repo to the env, this will also install required python
 # packages from pypi. we do this LAST, so that breakage is better visible.
--- a/quickinstall.bat	Thu Jun 30 23:33:58 2011 +0530
+++ b/quickinstall.bat	Fri Jul 01 10:18:38 2011 +0530
@@ -28,9 +28,6 @@
 7za x env/svg-edit.tar.gz -y -oenv\
 7za x env/svg-edit.tar -y -oenv\
 
-mkdir env\jquery
-wget -nc http://code.jquery.com/jquery-1.4.4.min.js -Oenv/jquery/jquery.min.js
-
 wget -nc http://svgweb.googlecode.com/files/svgweb-2010-08-10-Owlephant-1.zip -Penv/
 7za x env/svgweb-2010-08-10-Owlephant-1.zip -y -oenv\
 
@@ -43,6 +40,9 @@
 echo Installing babel first ...
 pip install babel
 
+echo Installing XStatic first ...
+pip install XStatic
+
 echo Installing all required python packages from pypi ...
 pip install -e .
 
--- a/setup.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/setup.py	Fri Jul 01 10:18:38 2011 +0530
@@ -76,7 +76,7 @@
     install_requires=[
         'blinker>=1.1', # event signalling (e.g. for change notification trigger)
         'docutils>=0.6', # reST markup processing
-        'Flask>=0.6', # micro framework
+        'Flask>=0.6,<0.7', # micro framework
         'Flask-Babel>=0.6', # i18n support
         'Flask-Cache>=0.3.2', # caching support
         'Flask-Script>=0.3', # scripting support
@@ -90,6 +90,7 @@
         'Werkzeug==0.6.2', # use this if 0.7dev fails
         'pytest', # py.test 1.3.4 is needed by unit tests
         'sphinx', # needed to build the docs
+        'XStatic-jQuery',
     ],
     # optional features and their list of requirements
     extras_require = {
--- a/wikiconfig.py	Thu Jun 30 23:33:58 2011 +0530
+++ b/wikiconfig.py	Fri Jul 01 10:18:38 2011 +0530
@@ -57,14 +57,20 @@
         docs = os.path.join(wikiconfig_dir, 'docs', '_build', 'html'),
         # see "quickinstall" script about how to get those files there
         ckeditor = os.path.join(wikiconfig_dir, env_dir, 'ckeditor'),
-        jquery = os.path.join(wikiconfig_dir, env_dir, 'jquery'),
         svgweb = os.path.join(wikiconfig_dir, env_dir, 'svgweb', 'src'),
         anywikidraw = os.path.join(wikiconfig_dir, env_dir, 'AnyWikiDraw', 'anywikidraw', 'moinmoin'),
         twikidraw = os.path.join(wikiconfig_dir, env_dir, 'TWikiDrawPlugin'),
         svgedit = os.path.join(wikiconfig_dir, env_dir, 'svg-edit'),
+        mathjax = os.path.join(wikiconfig_dir, env_dir, 'MathJax'),
         fileupload = os.path.join(wikiconfig_dir, env_dir, 'jquery.fu'),
     )
 
+    # we slowly migrate all stuff from above (old) method, to xstatic (new) method,
+    # see https://bitbucket.org/thomaswaldmann/xstatic for details:
+    from xstatic.pkg.jquery import JQuery
+    j = JQuery(root_url='/static', provider='local', protocol='http')
+    serve_files.update([(j.name, j.get_mapping()[1])])
+
     # ^^^ DON'T TOUCH THIS EXCEPT IF YOU KNOW WHAT YOU DO ^^^
 
     #item_root = u'Home' # change to some better value