You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by he...@apache.org on 2015/06/09 23:08:24 UTC

[1/8] allura git commit: [#6057] ticket:788 Validate options in default app admin controller

Repository: allura
Updated Branches:
  refs/heads/master 3f2f1ab93 -> 672649fb8


[#6057] ticket:788 Validate options in default app admin controller


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/3852cd26
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/3852cd26
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/3852cd26

Branch: refs/heads/master
Commit: 3852cd26008351e4e00ab0099c2172470870bc91
Parents: 72decc8
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jun 5 09:09:17 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:05 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py | 6 ++++++
 1 file changed, 6 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/3852cd26/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index c52f1c7..a9d3ff0 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -32,6 +32,7 @@ from bson import ObjectId
 from bson.errors import InvalidId
 from formencode import validators as V
 from webob import exc
+from formencode import validators as fev
 
 from ming.orm import session
 from ming.utils import LazyProperty
@@ -854,6 +855,11 @@ class DefaultAdminController(BaseController):
                     val = asbool(val or False)
                 elif opt.ming_type == int:
                     val = asint(val or 0)
+                try:
+                    val = opt.validate(val)
+                except fev.Invalid as e:
+                    flash(u'{}: {}'.format(opt.name, str(e)), 'error')
+                    continue
                 self.app.config.options[opt.name] = val
             if is_admin:
                 # possibly moving admin mount point


[8/8] allura git commit: [#6057] ticket:792 Get rid of help text for mount points

Posted by he...@apache.org.
[#6057] ticket:792 Get rid of help text for mount points


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/672649fb
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/672649fb
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/672649fb

Branch: refs/heads/master
Commit: 672649fb81687247ffb43a6d7b578f69a5e53790
Parents: 3090de7
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Jun 9 14:10:51 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:07 2015 +0000

----------------------------------------------------------------------
 .../ext/admin/templates/project_tools.html      | 21 +-------------------
 Allura/allura/public/nf/js/project_tools.js     |  9 ---------
 2 files changed, 1 insertion(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/672649fb/Allura/allura/ext/admin/templates/project_tools.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/project_tools.html b/Allura/allura/ext/admin/templates/project_tools.html
index 057af51..38705ed 100644
--- a/Allura/allura/ext/admin/templates/project_tools.html
+++ b/Allura/allura/ext/admin/templates/project_tools.html
@@ -31,7 +31,7 @@
   <div class="nested-grid-container">
     {% for tool in installable_tools %}
       <span class="tcenter grid-4 installable_tool">
-        <a class="install_trig" data-tool="{{ tool['name'] }}" data-relaxed-mount-points="{{ 'true' if tool.app.relaxed_mount_points else 'false' }}">
+        <a class="install_trig" data-tool="{{ tool['name'] }}">
           <span class="tool_title">{{ tool['app'].tool_label }}{{' (%s)' % tool.app.status if tool.app.status != 'production' else ''}}</span><br />
           <img src="{{ g.theme.app_icon_url(tool['name'], 32) or 'unk.png' }}" alt="">
         </a>
@@ -55,25 +55,6 @@
         <input id="id_url_input" type="text" name="new.mount_point" title="The url for this tool relative to {{ full_url }} " class="new_mount_point">
        <p><span id="full-url-preview" data-url="{{full_url}}"></span></p>
     </div>
-    <div class="grid-13">
-      <small>
-        * The mount point is the name of the tool as it will appear in a URL.
-        <span class="mount-point-name-rules tool">
-          A valid mount point for this tool must begin with a letter, contain only lowercase letters,
-          numbers, and dashes, and be from 1-63 characters in length.
-        </span>
-        <span class="mount-point-name-rules tool-relaxed">
-          A valid mount point for this tool must begin with a letter or number, contain only
-          letters, numbers, dashes, underscores, periods, and plus signs, and be
-          from 1-63 characters in length.
-        </span>
-        <span class="mount-point-name-rules subproject">
-          A valid subproject mount point must begin with a letter, contain only lowercase letters,
-          numbers, and dashes, and be from 3-15 characters in length.
-        </span>
-      </small>
-    </div>
-    <div class="grid-13">&nbsp;</div>
     <div id="install_options">{# js inserts form fields here #}</div>
     <div class="grid-13">&nbsp;</div>
     <hr>

http://git-wip-us.apache.org/repos/asf/allura/blob/672649fb/Allura/allura/public/nf/js/project_tools.js
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/js/project_tools.js b/Allura/allura/public/nf/js/project_tools.js
index b7c4e0c..a690937 100644
--- a/Allura/allura/public/nf/js/project_tools.js
+++ b/Allura/allura/public/nf/js/project_tools.js
@@ -33,8 +33,6 @@
     install_popup.append(install_form.show());
     $('a.install_trig').click(function () {
         var datatool = $(this).data('tool');
-        var relaxed_mount_points = $(this).data('relaxed-mount-points');
-        install_form.find('.mount-point-name-rules').hide();
         if (datatool) {
             var tool = defaults[datatool];
             install_options.html('Loading options...');
@@ -45,18 +43,11 @@
             new_ep_name.val(datatool);
             new_mount_point.val(tool.default_mount);
             new_mount_label.val(tool.default_label);
-            if (relaxed_mount_points) {
-              install_form.find('.mount-point-name-rules.tool-relaxed').show();
-            }
-            else {
-              install_form.find('.mount-point-name-rules.tool').show();
-            }
         } else {
             install_tool_label.html("Subproject");
             new_ep_name.val('');
             new_mount_point.val('');
             new_mount_label.val('');
-            install_form.find('.mount-point-name-rules.subproject').show();
         }
     });
     // Edit popup


[4/8] allura git commit: [#6057] ticket:792 Allow ConfigOption to specify help text

Posted by he...@apache.org.
[#6057] ticket:792 Allow ConfigOption to specify help text


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/3090de78
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/3090de78
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/3090de78

Branch: refs/heads/master
Commit: 3090de782e6e901cf8063977fbe86574dcb01176
Parents: bc3c028
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Jun 9 14:01:56 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:06 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                                      | 9 +++++----
 Allura/allura/templates/app_admin_options.html            | 3 +++
 Allura/allura/templates/app_admin_options_on_install.html | 5 ++++-
 ForgeLink/forgelink/link_main.py                          | 2 ++
 4 files changed, 14 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/3090de78/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 2683e36..62b82fe 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -57,12 +57,13 @@ class ConfigOption(object):
 
     """
 
-    def __init__(self, name, ming_type, default, label=None, validator=None, extra_attrs=None):
-        """Create a new ConfigOption.
-
-        """
+    def __init__(self, name, ming_type, default,
+                 label=None, help_text=None, validator=None,
+                 extra_attrs=None):
+        """Create a new ConfigOption."""
         self.name, self.ming_type, self._default, self.label = (
             name, ming_type, default, label or name)
+        self.help_text = help_text
         self.validator = validator
         self.extra_attrs = extra_attrs
 

http://git-wip-us.apache.org/repos/asf/allura/blob/3090de78/Allura/allura/templates/app_admin_options.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_options.html b/Allura/allura/templates/app_admin_options.html
index bf013c4..d818a56 100644
--- a/Allura/allura/templates/app_admin_options.html
+++ b/Allura/allura/templates/app_admin_options.html
@@ -30,6 +30,9 @@
         {% else %}
             <input {{o.render_attrs()}} id="{{o.name}}" name="{{o.name}}" value="{{app.config.options.get(o.name, o.default)}}">
         {% endif %}
+        {% if o.help_text %}
+          <div class="grid-9"><small>{{o.help_text}}</small></div>
+        {% endif %}
       {% endif %}
     </div>
   {% else %}

http://git-wip-us.apache.org/repos/asf/allura/blob/3090de78/Allura/allura/templates/app_admin_options_on_install.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_options_on_install.html b/Allura/allura/templates/app_admin_options_on_install.html
index 218dd61..16b62b6 100644
--- a/Allura/allura/templates/app_admin_options_on_install.html
+++ b/Allura/allura/templates/app_admin_options_on_install.html
@@ -17,7 +17,7 @@
        under the License.
 -#}
 {% for o in options %}
-  <label class="grid-13" for="{{o.name}}">{{o.label|capitalize}}</label>
+  <label class="grid-13" for="{{o.name}}">{{o.label}}</label>
   <div class="grid-13">
     {% if o.ming_type.__name__ == 'bool' %}
       <input {{o.render_attrs()}}id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if o.default}}>
@@ -25,4 +25,7 @@
       <input {{o.render_attrs()}} id="{{o.name}}" name="{{o.name}}" value="{{o.default or ''}}">
     {% endif %}
   </div>
+  {% if o.help_text %}
+    <div class="grid-13"><small>{{o.help_text}}</small></div>
+  {% endif %}
 {% endfor %}

http://git-wip-us.apache.org/repos/asf/allura/blob/3090de78/ForgeLink/forgelink/link_main.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/link_main.py b/ForgeLink/forgelink/link_main.py
index ec32974..735816d 100644
--- a/ForgeLink/forgelink/link_main.py
+++ b/ForgeLink/forgelink/link_main.py
@@ -51,6 +51,8 @@ class ForgeLinkApp(Application):
     config_options = Application.config_options + [
         ConfigOption(
             'url', str, None,
+            label='External Url',
+            help_text='URL to which you wish to link',
             validator=fev.URL(not_empty=True, add_http=True),
             extra_attrs={'type': 'url'}),
     ]


[2/8] allura git commit: [#6057] ticket:788 Handle extra options on tool install

Posted by he...@apache.org.
[#6057] ticket:788 Handle extra options on tool install


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/72decc8f
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/72decc8f
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/72decc8f

Branch: refs/heads/master
Commit: 72decc8f1ac5ee4d2600dbfd3f6fbcd199e4337b
Parents: 0f559d6
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jun 5 08:52:53 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:05 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                  |  7 ++++++-
 Allura/allura/ext/admin/admin_main.py | 12 +++++++++++-
 Allura/allura/model/project.py        | 15 ++++++++++++---
 ForgeLink/forgelink/link_main.py      |  4 +++-
 4 files changed, 32 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/72decc8f/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 4d96a8f..c52f1c7 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -55,12 +55,13 @@ class ConfigOption(object):
 
     """
 
-    def __init__(self, name, ming_type, default, label=None):
+    def __init__(self, name, ming_type, default, label=None, validator=None):
         """Create a new ConfigOption.
 
         """
         self.name, self.ming_type, self._default, self.label = (
             name, ming_type, default, label or name)
+        self.validator = validator
 
     @property
     def default(self):
@@ -71,6 +72,10 @@ class ConfigOption(object):
             return self._default()
         return self._default
 
+    def validate(self, value):
+        if self.validator:
+            return self.validator.to_python(value)
+
 
 class SitemapEntry(object):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/72decc8f/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 4ec1568..46cdf9c 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -687,8 +687,18 @@ class ProjectAdminController(BaseController):
                 h.log_action(log, 'install tool').info(
                     'install tool %s', mount_point,
                     meta=dict(tool_type=ep_name, mount_point=mount_point, mount_label=new['mount_label']))
+                App = g.entry_points['tool'][ep_name]
+                # pass only options which app expects
+                config_on_install = {
+                    k: v for (k, v) in kw.iteritems()
+                    if k in [o.name for o in App.options_on_install()]
+                }
                 return c.project.install_app(
-                    ep_name, mount_point, mount_label=new['mount_label'], ordinal=new['ordinal'])
+                    ep_name,
+                    mount_point,
+                    mount_label=new['mount_label'],
+                    ordinal=new['ordinal'],
+                    **config_on_install)
         g.post_event('project_updated')
 
     @h.vardec

http://git-wip-us.apache.org/repos/asf/allura/blob/72decc8f/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index db460a1..e280d63 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -725,6 +725,12 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
             except fe.Invalid as e:
                 raise exceptions.ToolError(str(e))
 
+    def _validate_tool_option(self, opt, value):
+        try:
+            return opt.validate(value)
+        except fe.Invalid as e:
+            raise exceptions.ToolError(u'{}: {}'.format(opt.name, str(e)))
+
     def install_app(self, ep_name, mount_point=None, mount_label=None, ordinal=None, **override_options):
         App = g.entry_points['tool'][ep_name]
         mount_point = self._mount_point_for_install(App, mount_point)
@@ -733,10 +739,13 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
                           [-1]['ordinal']) + 1
         options = App.default_options()
         options['mount_point'] = mount_point
-        options[
-            'mount_label'] = mount_label or App.default_mount_label or mount_point
+        options['mount_label'] = mount_label or App.default_mount_label or mount_point
         options['ordinal'] = int(ordinal)
-        options.update(override_options)
+        options_on_install = {o.name: o for o in App.options_on_install()}
+        for o, v in override_options.iteritems():
+            if o in options_on_install:
+                v = self._validate_tool_option(options_on_install[o], v)
+            options[o] = v
         cfg = AppConfig(
             project_id=self._id,
             tool_name=ep_name.lower(),

http://git-wip-us.apache.org/repos/asf/allura/blob/72decc8f/ForgeLink/forgelink/link_main.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/link_main.py b/ForgeLink/forgelink/link_main.py
index 87fbdbb..cd43d3f 100644
--- a/ForgeLink/forgelink/link_main.py
+++ b/ForgeLink/forgelink/link_main.py
@@ -49,7 +49,9 @@ class ForgeLinkApp(Application):
         'read': 'View link.',
     }
     config_options = Application.config_options + [
-        ConfigOption('url', str, None)
+        ConfigOption(
+            'url', str, None,
+            validator=fev.URL(not_empty=True, add_http=True)),
     ]
     config_on_install = ['url']
     searchable = True


[3/8] allura git commit: [#6057] ticket:788 Show extra options on tool installation

Posted by he...@apache.org.
[#6057] ticket:788 Show extra options on tool installation


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/0f559d61
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/0f559d61
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/0f559d61

Branch: refs/heads/master
Commit: 0f559d61ee7dda1c2d5aeb088de0d1b765eeedf5
Parents: 3f2f1ab
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jun 5 08:02:18 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:05 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                            | 14 ++++++++++
 Allura/allura/ext/admin/admin_main.py           |  9 +++++++
 .../ext/admin/templates/project_tools.html      |  4 +++
 Allura/allura/public/nf/js/project_tools.js     |  5 ++++
 .../templates/app_admin_options_on_install.html | 28 ++++++++++++++++++++
 ForgeLink/forgelink/link_main.py                |  1 +
 6 files changed, 61 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 3e226db..4d96a8f 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -220,6 +220,8 @@ class Application(object):
         /<neighborhood>/<project>/<admin>/<app>/. Default is a
         :class:`DefaultAdminController` instance.
     :cvar dict icons: Mapping of icon sizes to application-specific icon paths.
+    :cvar list config_on_install: :class:`ConfigOption` names that should be
+        configured by user during app installation
     """
 
     __version__ = None
@@ -227,6 +229,7 @@ class Application(object):
         ConfigOption('mount_point', str, 'app'),
         ConfigOption('mount_label', str, 'app'),
         ConfigOption('ordinal', int, '0')]
+    config_on_install = []
     status_map = ['production', 'beta', 'alpha', 'user']
     status = 'production'
     script_name = None
@@ -481,6 +484,17 @@ class Application(object):
             (co.name, co.default)
             for co in cls.config_options)
 
+    @classmethod
+    def options_on_install(cls):
+        """
+        Return a list of :class:`config_options <ConfigOption>` which should be
+        configured by user during app installation.
+
+        :rtype: list
+        """
+        return [o for o in cls.config_options
+                if o.name in cls.config_on_install]
+
     def install(self, project):
         'Whatever logic is required to initially set up a tool'
         # Create the discussion object

http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index b51c816..4ec1568 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -293,6 +293,15 @@ class ProjectAdminController(BaseController):
             flash('Invalid threshold', 'error')
         redirect('tools?limit=%s&page=%s' % (limit, page))
 
+    @expose('jinja:allura:templates/app_admin_options_on_install.html')
+    def options_on_install(self, tool_name):
+        """Renders form fields for options configuration on install"""
+        tool = [t['app'] for t in AdminApp.installable_tools_for(c.project)
+                if t['name'].lower() == tool_name.lower()]
+        if len(tool) == 1:
+            return {'options': tool[0].options_on_install()}
+        raise exc.HTTPNotFound
+
     @expose()
     @require_post()
     def update_labels(self, labels=None, **kw):

http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/Allura/allura/ext/admin/templates/project_tools.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/project_tools.html b/Allura/allura/ext/admin/templates/project_tools.html
index ca372ce..057af51 100644
--- a/Allura/allura/ext/admin/templates/project_tools.html
+++ b/Allura/allura/ext/admin/templates/project_tools.html
@@ -73,6 +73,9 @@
         </span>
       </small>
     </div>
+    <div class="grid-13">&nbsp;</div>
+    <div id="install_options">{# js inserts form fields here #}</div>
+    <div class="grid-13">&nbsp;</div>
     <hr>
     <div class="grid-13">&nbsp;</div>
     <div class="grid-13">
@@ -183,6 +186,7 @@
         '{{tool.name}}':{'default_label':'{{tool.app.default_mount_label}}','default_mount':'{{tool.app.default_mount_point}}'}{% if not loop.last %},{% endif %}
         {% endfor %}
     };
+    var install_options_url = "{{ c.project.url().rstrip('/') }}/admin/options_on_install";
 </script>
 <script>
 /* Real-time preview of a Url Path (aka mount point). */

http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/Allura/allura/public/nf/js/project_tools.js
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/js/project_tools.js b/Allura/allura/public/nf/js/project_tools.js
index 4e4ab46..b7c4e0c 100644
--- a/Allura/allura/public/nf/js/project_tools.js
+++ b/Allura/allura/public/nf/js/project_tools.js
@@ -29,6 +29,7 @@
     var new_mount_label = install_form.find('input.new_mount_label');
     var install_tool_label = $('#install_tool_label');
     var mount_point_rule_names = $('#install_form .mount-point-rule-names');
+    var install_options = $('#install_options');
     install_popup.append(install_form.show());
     $('a.install_trig').click(function () {
         var datatool = $(this).data('tool');
@@ -36,6 +37,10 @@
         install_form.find('.mount-point-name-rules').hide();
         if (datatool) {
             var tool = defaults[datatool];
+            install_options.html('Loading options...');
+            $.get(install_options_url, {tool_name: datatool}, function (data) {
+                install_options.html(data);
+            });
             install_tool_label.html(tool.default_label);
             new_ep_name.val(datatool);
             new_mount_point.val(tool.default_mount);

http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/Allura/allura/templates/app_admin_options_on_install.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_options_on_install.html b/Allura/allura/templates/app_admin_options_on_install.html
new file mode 100644
index 0000000..bc0d8ea
--- /dev/null
+++ b/Allura/allura/templates/app_admin_options_on_install.html
@@ -0,0 +1,28 @@
+{#-
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+-#}
+{% for o in options %}
+  <label class="grid-13" for="{{o.name}}">{{o.label|capitalize}}</label>
+  <div class="grid-13">
+    {% if o.ming_type.__name__ == 'bool' %}
+      <input id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if o.default}}>
+    {% else %}
+      <input id="{{o.name}}" name="{{o.name}}" value="{{o.default or ''}}">
+    {% endif %}
+  </div>
+{% endfor %}

http://git-wip-us.apache.org/repos/asf/allura/blob/0f559d61/ForgeLink/forgelink/link_main.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/link_main.py b/ForgeLink/forgelink/link_main.py
index ef1c510..87fbdbb 100644
--- a/ForgeLink/forgelink/link_main.py
+++ b/ForgeLink/forgelink/link_main.py
@@ -51,6 +51,7 @@ class ForgeLinkApp(Application):
     config_options = Application.config_options + [
         ConfigOption('url', str, None)
     ]
+    config_on_install = ['url']
     searchable = True
     exportable = True
     tool_label = 'External Link'


[7/8] allura git commit: [#6057] ticket:788 Add/fix tests

Posted by he...@apache.org.
[#6057] ticket:788 Add/fix tests


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/a0b3aed2
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/a0b3aed2
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/a0b3aed2

Branch: refs/heads/master
Commit: a0b3aed29a4a7e9657838f584f49e668f54bfd41
Parents: 1ac85b8
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jun 5 12:19:37 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:06 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                            |  1 +
 Allura/allura/model/project.py                  |  6 +--
 Allura/allura/tests/functional/test_admin.py    | 10 ++++
 Allura/allura/tests/model/test_project.py       | 17 ++++++
 Allura/allura/tests/test_app.py                 | 35 +++++++++++++
 .../forgelink/tests/functional/test_root.py     | 54 ++++++++++++++++++++
 6 files changed, 120 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index a9d3ff0..58aa49d 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -76,6 +76,7 @@ class ConfigOption(object):
     def validate(self, value):
         if self.validator:
             return self.validator.to_python(value)
+        return value
 
 
 class SitemapEntry(object):

http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index e280d63..2ad3be4 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -742,10 +742,10 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         options['mount_label'] = mount_label or App.default_mount_label or mount_point
         options['ordinal'] = int(ordinal)
         options_on_install = {o.name: o for o in App.options_on_install()}
-        for o, v in override_options.iteritems():
+        for o, val in override_options.iteritems():
             if o in options_on_install:
-                v = self._validate_tool_option(options_on_install[o], v)
-            options[o] = v
+                val = self._validate_tool_option(options_on_install[o], val)
+            options[o] = val
         cfg = AppConfig(
             project_id=self._id,
             tool_name=ep_name.lower(),

http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/Allura/allura/tests/functional/test_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 819a92a..d239591 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -358,6 +358,16 @@ class TestProjectAdmin(TestController):
             assert 'error' in self.webflash(r)
             assert 'limit exceeded' in self.webflash(r)
 
+    def test_tool_options_on_install(self):
+        self.app.get('/admin/options_on_install', status=404)
+        self.app.get('/admin/options_on_install?tool_name=fake', status=404)
+        r = self.app.get('/admin/options_on_install?tool_name=tickets')
+        assert_equals(r.body, '')
+        opts = ['EnableVoting']
+        with mock.patch.object(ForgeTrackerApp, 'config_on_install', new=opts):
+            r = self.app.get('/admin/options_on_install?tool_name=tickets')
+            assert_in(u'<input id="EnableVoting" name="EnableVoting" type="checkbox">', r)
+
     def test_grouping_threshold(self):
         r = self.app.get('/admin/tools')
         grouping_threshold = r.html.find(

http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/Allura/allura/tests/model/test_project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/model/test_project.py b/Allura/allura/tests/model/test_project.py
index d8a754f..2fe3dc8 100644
--- a/Allura/allura/tests/model/test_project.py
+++ b/Allura/allura/tests/model/test_project.py
@@ -24,6 +24,7 @@ from nose import with_setup
 from nose.tools import assert_equals, assert_in
 from pylons import tmpl_context as c
 from ming.orm.ormsession import ThreadLocalORMSession
+from formencode import validators as fev
 
 from allura import model as M
 from allura.lib import helpers as h
@@ -96,6 +97,22 @@ def test_project():
     c.app.config.breadcrumbs()
 
 
+@with_setup(setUp)
+def test_install_app_validates_options():
+    from forgetracker.tracker_main import ForgeTrackerApp
+    name = 'TicketMonitoringEmail'
+    opt = [o for o in ForgeTrackerApp.config_options if o.name == name][0]
+    opt.validator = fev.Email(not_empty=True)
+    with patch.object(ForgeTrackerApp, 'config_on_install', new=[opt.name]):
+        for v in [None, '', 'bad@email']:
+            with td.raises(ToolError):
+                c.project.install_app('Tickets', 'test-tickets', **{name: v})
+            assert_equals(c.project.app_instance('test-tickets'), None)
+        c.project.install_app('Tickets', 'test-tickets', **{name: 'e@e.com'})
+        app = c.project.app_instance('test-tickets')
+        assert_equals(app.config.options[name], 'e@e.com')
+
+
 def test_project_index():
     project, idx = c.project, c.project.index()
     assert 'id' in idx

http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/Allura/allura/tests/test_app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index a31f112..2642a95 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -18,6 +18,8 @@
 from pylons import tmpl_context as c
 import mock
 from ming.base import Object
+from nose.tools import assert_equal, assert_raises
+from formencode import validators as fev
 
 from alluratest.controller import setup_unit_test
 from allura import app
@@ -52,6 +54,39 @@ def test_config_options():
     assert options[1].default == 'MyTestValue'
 
 
+def test_config_option_without_validator():
+    opt = app.ConfigOption('test1', str, None)
+    assert_equal(opt.validate(None), None)
+    assert_equal(opt.validate(''), '')
+    assert_equal(opt.validate('val'), 'val')
+
+
+def test_config_option_with_validator():
+    v = fev.NotEmpty()
+    opt = app.ConfigOption('test1', str, None, validator=v)
+    assert_equal(opt.validate('val'), 'val')
+    assert_raises(fev.Invalid, opt.validate, None)
+    assert_raises(fev.Invalid, opt.validate, '')
+
+
+def test_options_on_install_default():
+    a = app.Application(c.project, c.app.config)
+    assert_equal(a.options_on_install(), [])
+
+
+def test_options_on_install():
+    opts = [app.ConfigOption('url', str, None),
+            app.ConfigOption('private', bool, None)]
+    class TestApp(app.Application):
+        config_options = app.Application.config_options + opts + [
+            app.ConfigOption('not_on_install', str, None),
+        ]
+        config_on_install = ['url', 'private']
+
+    a = TestApp(c.project, c.app.config)
+    assert_equal(a.options_on_install(), opts)
+
+
 def test_sitemap():
     sm = app.SitemapEntry('test', '')[
         app.SitemapEntry('a', 'a/'),

http://git-wip-us.apache.org/repos/asf/allura/blob/a0b3aed2/ForgeLink/forgelink/tests/functional/test_root.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/tests/functional/test_root.py b/ForgeLink/forgelink/tests/functional/test_root.py
index 021b91f..1372be8 100644
--- a/ForgeLink/forgelink/tests/functional/test_root.py
+++ b/ForgeLink/forgelink/tests/functional/test_root.py
@@ -15,6 +15,7 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+import json
 from nose.tools import assert_equal
 
 from allura import model as M
@@ -61,3 +62,56 @@ class TestRootController(TestController):
         response = self.app.get('/link/help')
         # HACK: support for remote redirects is limited in follow()
         assert 'http://www.google.de/search?q=help' in response
+
+
+class TestConfigOptions(TestController):
+
+    @property
+    def project(self):
+        return M.Project.query.get(shortname='test')
+
+    def assert_url(self, mount_point, val):
+        app = self.project.app_instance(mount_point)
+        assert_equal(app.config.options['url'], val)
+
+    def test_sets_url_on_install(self):
+        r = self.app.post('/p/test/admin/update_mounts', params={
+            'new.install': 'install',
+            'new.ep_name': 'link',
+            'new.ordinal': '1',
+            'new.mount_point': 'link-google',
+            'new.mount_label': 'Google',
+            'url': 'google.com'})
+        self.assert_url('link-google', 'http://google.com')
+
+    def test_validates_url_on_install(self):
+        r = self.app.post('/p/test/admin/update_mounts', params={
+            'new.install': 'install',
+            'new.ep_name': 'link',
+            'new.ordinal': '1',
+            'new.mount_point': 'link-google',
+            'new.mount_label': 'Google',
+            'url': 'invalid url'})
+        flash = json.loads(self.webflash(r))
+        assert_equal(flash['status'], 'error')
+        assert_equal(flash['message'], 'ToolError: url: That is not a valid URL')
+        app = self.project.app_instance('link-google')
+        assert_equal(app, None)
+
+    @td.with_link
+    def test_sets_url_on_config(self):
+        self.assert_url('link', None)
+        params = {'url': 'https://allura.apache.org'}
+        r = self.app.post('/p/test/admin/link/configure', params=params)
+        assert_equal(self.webflash(r), '')
+        self.assert_url('link', 'https://allura.apache.org')
+
+    @td.with_link
+    def test_validates_url_on_config(self):
+        self.assert_url('link', None)
+        params = {'url': 'invalid link'}
+        r = self.app.post('/p/test/admin/link/configure', params=params)
+        flash = json.loads(self.webflash(r))
+        assert_equal(flash['status'], 'error')
+        assert_equal(flash['message'], 'url: That is not a valid URL')
+        self.assert_url('link', None)


[6/8] allura git commit: [#6057] ticket:788 Remove unnecessary ForgeLink app admin controller

Posted by he...@apache.org.
[#6057] ticket:788 Remove unnecessary ForgeLink app admin controller


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/1ac85b82
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/1ac85b82
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/1ac85b82

Branch: refs/heads/master
Commit: 1ac85b82a7d4b77cbff630f0a7de9188bc947044
Parents: 3852cd2
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jun 5 09:09:58 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:06 2015 +0000

----------------------------------------------------------------------
 ForgeLink/forgelink/link_main.py                | 33 +--------
 .../forgelink/templates/app_admin_options.html  | 77 --------------------
 .../forgelink/tests/functional/test_root.py     | 44 -----------
 3 files changed, 4 insertions(+), 150 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1ac85b82/ForgeLink/forgelink/link_main.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/link_main.py b/ForgeLink/forgelink/link_main.py
index cd43d3f..b50c22e 100644
--- a/ForgeLink/forgelink/link_main.py
+++ b/ForgeLink/forgelink/link_main.py
@@ -15,20 +15,20 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-#-*- python -*-
+# -*- python -*-
 import logging
 import json
 
 # Non-stdlib imports
-from tg import expose, redirect, flash, jsonify
+from tg import expose, jsonify
 from pylons import tmpl_context as c
 from pylons import request
 from formencode import validators as fev
 
 # Pyforge-specific imports
-from allura.app import Application, ConfigOption, SitemapEntry, DefaultAdminController
+from allura.app import Application, ConfigOption, SitemapEntry
 from allura.lib import helpers as h
-from allura.lib.security import require_access, has_access
+from allura.lib.security import require_access
 from allura.lib.utils import permanent_redirect
 from allura import model as M
 from allura.controllers import BaseController
@@ -69,7 +69,6 @@ class ForgeLinkApp(Application):
     def __init__(self, project, config):
         Application.__init__(self, project, config)
         self.root = RootController()
-        self.admin = LinkAdminController(self)
         self.api_root = RootRestController(self)
 
     @property
@@ -126,30 +125,6 @@ class RootController(BaseController):
         return dict()
 
 
-class LinkAdminController(DefaultAdminController):
-
-    @expose()
-    def index(self, **kw):
-        flash('External link URL updated.')
-        redirect(c.project.url() + 'admin/tools')
-
-    @expose('jinja:forgelink:templates/app_admin_options.html')
-    def options(self):
-        return dict(
-            app=self.app,
-            allow_config=has_access(self.app, 'configure')())
-
-    @expose('json:')
-    def set_url(self, **kw):
-        validator = fev.URL(not_empty=True, add_http=True)
-        try:
-            url = validator.to_python(kw.get('url'))
-        except fev.Invalid as e:
-            return {'status': 'error', 'errors': {'url': e.msg}}
-        self.app.config.options['url'] = url
-        return {'status': 'ok'}
-
-
 class RootRestController(BaseController, AppRestControllerMixin):
 
     def __init__(self, app):

http://git-wip-us.apache.org/repos/asf/allura/blob/1ac85b82/ForgeLink/forgelink/templates/app_admin_options.html
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/templates/app_admin_options.html b/ForgeLink/forgelink/templates/app_admin_options.html
deleted file mode 100644
index a62dfa7..0000000
--- a/ForgeLink/forgelink/templates/app_admin_options.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{#-
-       Licensed to the Apache Software Foundation (ASF) under one
-       or more contributor license agreements.  See the NOTICE file
-       distributed with this work for additional information
-       regarding copyright ownership.  The ASF licenses this file
-       to you under the Apache License, Version 2.0 (the
-       "License"); you may not use this file except in compliance
-       with the License.  You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-       Unless required by applicable law or agreed to in writing,
-       software distributed under the License is distributed on an
-       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-       KIND, either express or implied.  See the License for the
-       specific language governing permissions and limitations
-       under the License.
--#}
-<!DOCTYPE html>
-{% import 'allura:templates/jinja_master/lib.html' as lib with context %}
-<form id="set-url-form" method="post" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/set_url">
-  <label for="url" class="grid-4">url</label>
-  <div class="grid-9">
-    {% if not allow_config %}
-      {{app.config.options.get('url', '')}}
-    {% else %}
-      <input id="url" name="url" value="{{app.config.options.get('url', '')}}">
-    {% endif %}
-  </div>
-  <div class="grid-4">&nbsp;</div><div id='url-error' class='grid-9'></div>
-  {% if allow_config %}
-    <div class="grid-13">&nbsp;</div>
-    <hr>
-    <div class="grid-13">&nbsp;</div>
-    <div class="grid-13">
-      <input type="submit" value="Save" id="save" />
-      <a href="#" class="close">Cancel</a>
-    </div>
-  {% endif %}
-  {{lib.csrf_token()}}
-</form>
-
-<script type="text/javascript">
-$(function() {
-  $('#save').on('click', function(e) {
-    e.preventDefault();
-    var form = $('#set-url-form');
-    var button = $(this);
-    var input = form.find('input[name="url"]')
-    var url_error = $('#url-error');
-    var url = input.val();
-    var csrf = $.cookie('_session_id');
-    button.prop('disabled', true);
-    input.prop('disabled', true);
-    url_error.text('');
-    $.post(
-      form.attr('action'),
-      {'_session_id': csrf, 'url': url},
-      function(data) {
-        if (data['status'] == 'ok') {
-          location.reload();
-        } else {
-          url_error.text(data['errors']['url']);
-        }
-        button.prop('disabled', false);
-        input.prop('disabled', false);
-      });
-  });
-});
-</script>
-
-<style type="text/css">
-#url-error {
-  margin-left: 15px;
-  color: #f33;
-}
-</style>

http://git-wip-us.apache.org/repos/asf/allura/blob/1ac85b82/ForgeLink/forgelink/tests/functional/test_root.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/tests/functional/test_root.py b/ForgeLink/forgelink/tests/functional/test_root.py
index e4fc656..021b91f 100644
--- a/ForgeLink/forgelink/tests/functional/test_root.py
+++ b/ForgeLink/forgelink/tests/functional/test_root.py
@@ -61,47 +61,3 @@ class TestRootController(TestController):
         response = self.app.get('/link/help')
         # HACK: support for remote redirects is limited in follow()
         assert 'http://www.google.de/search?q=help' in response
-
-    @td.with_link
-    def test_set_url_validation(self):
-        r = self.app.post('/p/test/admin/link/set_url', {})
-        expected = {'status': 'error',
-                    'errors': {'url': u'Please enter a value'}}
-        assert_equal(r.json, expected)
-
-        r = self.app.post('/p/test/admin/link/set_url', {'url': ''})
-        expected = {'status': 'error',
-                    'errors': {'url': u'Please enter a value'}}
-        assert_equal(r.json, expected)
-
-        r = self.app.post('/p/test/admin/link/set_url', {'url': 'bad url'})
-        expected = {'status': 'error',
-                    'errors': {'url': u'That is not a valid URL'}}
-        assert_equal(r.json, expected)
-
-        p = M.Project.query.get(shortname='test')
-        link = p.app_instance('link')
-        assert_equal(link.config.options.get('url'), None)
-
-    @td.with_link
-    def test_set_url(self):
-        data = {'url': 'http://example.com'}  # http
-        r = self.app.post('/p/test/admin/link/set_url', data)
-        assert_equal(r.json, {'status': 'ok'})
-        p = M.Project.query.get(shortname='test')
-        link = p.app_instance('link')
-        assert_equal(link.config.options.get('url'), 'http://example.com')
-
-        data = {'url': 'https://google.com'}  # https
-        r = self.app.post('/p/test/admin/link/set_url', data)
-        assert_equal(r.json, {'status': 'ok'})
-        p = M.Project.query.get(shortname='test')
-        link = p.app_instance('link')
-        assert_equal(link.config.options.get('url'), 'https://google.com')
-
-        data = {'url': 'lmgtfy.com'}  # http is added if not provided
-        r = self.app.post('/p/test/admin/link/set_url', data)
-        assert_equal(r.json, {'status': 'ok'})
-        p = M.Project.query.get(shortname='test')
-        link = p.app_instance('link')
-        assert_equal(link.config.options.get('url'), 'http://lmgtfy.com')


[5/8] allura git commit: [#6057] ticket:792 Allow ConfigOption to specify extra attrs for html input tag

Posted by he...@apache.org.
[#6057] ticket:792 Allow ConfigOption to specify extra attrs for html input tag

and use it to pass type='url' to ForgeLink's url options


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/bc3c028c
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/bc3c028c
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/bc3c028c

Branch: refs/heads/master
Commit: bc3c028c7cc04430aaf0a1d81c8c194ed65301cb
Parents: a0b3aed
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Jun 9 13:41:25 2015 +0000
Committer: Heith Seewald <hs...@slashdotmedia.com>
Committed: Tue Jun 9 21:06:06 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                                      | 8 +++++++-
 Allura/allura/templates/app_admin_options.html            | 4 ++--
 Allura/allura/templates/app_admin_options_on_install.html | 4 ++--
 Allura/allura/tests/test_app.py                           | 5 +++++
 ForgeLink/forgelink/link_main.py                          | 3 ++-
 5 files changed, 18 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/bc3c028c/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 58aa49d..2683e36 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -36,6 +36,7 @@ from formencode import validators as fev
 
 from ming.orm import session
 from ming.utils import LazyProperty
+import ew.jinja2_ew as ew
 
 from allura.lib import helpers as h
 from allura.lib.security import has_access, require_access
@@ -56,13 +57,14 @@ class ConfigOption(object):
 
     """
 
-    def __init__(self, name, ming_type, default, label=None, validator=None):
+    def __init__(self, name, ming_type, default, label=None, validator=None, extra_attrs=None):
         """Create a new ConfigOption.
 
         """
         self.name, self.ming_type, self._default, self.label = (
             name, ming_type, default, label or name)
         self.validator = validator
+        self.extra_attrs = extra_attrs
 
     @property
     def default(self):
@@ -78,6 +80,10 @@ class ConfigOption(object):
             return self.validator.to_python(value)
         return value
 
+    def render_attrs(self):
+        """Return extra_attrs formatted in a way that allows inserting into html tag"""
+        return ew._Jinja2Widget().j2_attrs(self.extra_attrs or {})
+
 
 class SitemapEntry(object):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/bc3c028c/Allura/allura/templates/app_admin_options.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_options.html b/Allura/allura/templates/app_admin_options.html
index 8924823..bf013c4 100644
--- a/Allura/allura/templates/app_admin_options.html
+++ b/Allura/allura/templates/app_admin_options.html
@@ -26,9 +26,9 @@
            {{app.config.options.get(o.name, o.default)}}
       {% else%}
         {% if o.ming_type.__name__ == 'bool' %}
-            <input id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if app.config.options.get(o.name, o.default)}}>
+            <input {{o.render_attrs()}} id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if app.config.options.get(o.name, o.default)}}>
         {% else %}
-            <input id="{{o.name}}" name="{{o.name}}" value="{{app.config.options.get(o.name, o.default)}}">
+            <input {{o.render_attrs()}} id="{{o.name}}" name="{{o.name}}" value="{{app.config.options.get(o.name, o.default)}}">
         {% endif %}
       {% endif %}
     </div>

http://git-wip-us.apache.org/repos/asf/allura/blob/bc3c028c/Allura/allura/templates/app_admin_options_on_install.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_options_on_install.html b/Allura/allura/templates/app_admin_options_on_install.html
index bc0d8ea..218dd61 100644
--- a/Allura/allura/templates/app_admin_options_on_install.html
+++ b/Allura/allura/templates/app_admin_options_on_install.html
@@ -20,9 +20,9 @@
   <label class="grid-13" for="{{o.name}}">{{o.label|capitalize}}</label>
   <div class="grid-13">
     {% if o.ming_type.__name__ == 'bool' %}
-      <input id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if o.default}}>
+      <input {{o.render_attrs()}}id="{{o.name}}" name="{{o.name}}" type="checkbox"{{ ' checked="checked"' if o.default}}>
     {% else %}
-      <input id="{{o.name}}" name="{{o.name}}" value="{{o.default or ''}}">
+      <input {{o.render_attrs()}} id="{{o.name}}" name="{{o.name}}" value="{{o.default or ''}}">
     {% endif %}
   </div>
 {% endfor %}

http://git-wip-us.apache.org/repos/asf/allura/blob/bc3c028c/Allura/allura/tests/test_app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index 2642a95..230b5f9 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -54,6 +54,11 @@ def test_config_options():
     assert options[1].default == 'MyTestValue'
 
 
+def test_config_options_render_attrs():
+    opt = app.ConfigOption('test1', str, None, extra_attrs={'type': 'url'})
+    assert_equal(opt.render_attrs(), 'type="url"')
+
+
 def test_config_option_without_validator():
     opt = app.ConfigOption('test1', str, None)
     assert_equal(opt.validate(None), None)

http://git-wip-us.apache.org/repos/asf/allura/blob/bc3c028c/ForgeLink/forgelink/link_main.py
----------------------------------------------------------------------
diff --git a/ForgeLink/forgelink/link_main.py b/ForgeLink/forgelink/link_main.py
index b50c22e..ec32974 100644
--- a/ForgeLink/forgelink/link_main.py
+++ b/ForgeLink/forgelink/link_main.py
@@ -51,7 +51,8 @@ class ForgeLinkApp(Application):
     config_options = Application.config_options + [
         ConfigOption(
             'url', str, None,
-            validator=fev.URL(not_empty=True, add_http=True)),
+            validator=fev.URL(not_empty=True, add_http=True),
+            extra_attrs={'type': 'url'}),
     ]
     config_on_install = ['url']
     searchable = True