You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@steve.apache.org by ad...@apache.org on 2015/03/29 19:40:18 UTC

svn commit: r1669940 - in /steve/steve-web: ./ bin/ src/asf/steve/ src/asf/steve/admin/ src/asf/steve/api/ src/asf/steve/backends/ src/asf/steve/commands/ src/asf/steve/static/ src/asf/steve/templates/ src/asf/steve/voter/ src/asf/steve/web/ src/asf/st...

Author: adc
Date: Sun Mar 29 17:40:18 2015
New Revision: 1669940

URL: http://svn.apache.org/r1669940
Log:
More sketch work with commands and backend plugins
pre-commit-status-crumb=881832c7-8d56-47d0-a4bc-ae455ffb5369

Added:
    steve/steve-web/src/asf/steve/backends/
    steve/steve-web/src/asf/steve/backends/__init__.py
    steve/steve-web/src/asf/steve/backends/elastic.py
    steve/steve-web/src/asf/steve/backends/file.py
    steve/steve-web/src/asf/steve/commands/
    steve/steve-web/src/asf/steve/commands/__init__.py
    steve/steve-web/src/asf/steve/commands/mkelection.py
    steve/steve-web/src/asf/steve/commands/setup.py
    steve/steve-web/src/asf/steve/util.py
    steve/steve-web/src/asf/steve/web/
    steve/steve-web/src/asf/steve/web/__init__.py
    steve/steve-web/src/asf/steve/web/admin/
      - copied from r1669774, steve/steve-web/src/asf/steve/admin/
    steve/steve-web/src/asf/steve/web/api/
      - copied from r1669774, steve/steve-web/src/asf/steve/api/
    steve/steve-web/src/asf/steve/web/static/
      - copied from r1669774, steve/steve-web/src/asf/steve/static/
    steve/steve-web/src/asf/steve/web/templates/
      - copied from r1669774, steve/steve-web/src/asf/steve/templates/
    steve/steve-web/src/asf/steve/web/voter/
      - copied from r1669774, steve/steve-web/src/asf/steve/voter/
    steve/steve-web/tests/conftest.py
    steve/steve-web/tests/data/steve.cfg
    steve/steve-web/tests/test_backends.py
    steve/steve-web/tests/test_commands.py
    steve/steve-web/tests/test_commands_mkelection.py
Removed:
    steve/steve-web/src/asf/steve/admin/
    steve/steve-web/src/asf/steve/api/
    steve/steve-web/src/asf/steve/static/
    steve/steve-web/src/asf/steve/templates/
    steve/steve-web/src/asf/steve/voter/
Modified:
    steve/steve-web/README.rst
    steve/steve-web/bin/steve.apache.wsgi
    steve/steve-web/bin/steve.uwsgi.wsgi
    steve/steve-web/requirements.txt
    steve/steve-web/setup.py
    steve/steve-web/src/asf/steve/__init__.py
    steve/steve-web/src/asf/steve/web/admin/__init__.py
    steve/steve-web/tests/test_steve.py
    steve/steve-web/uwsgi.ini

Modified: steve/steve-web/README.rst
URL: http://svn.apache.org/viewvc/steve/steve-web/README.rst?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/README.rst (original)
+++ steve/steve-web/README.rst Sun Mar 29 17:40:18 2015
@@ -4,8 +4,12 @@ Apache STeVe
 
 .. rubric:: Apache STeVe.
 
-Running Web Site
-----------------
+Running Web Site In Development Mode
+------------------------------------
+
+It's obviously handy to look at your spiffy work from a browser.  Apache STeVe
+can be run locally at port 8080.  You can change the port by editing the file
+``uwsgi.ini``.
 
 You need to have ``pip`` and ``virtialenv`` installed on your machine.
 In the local directory of ``steve/steve-web``:

Modified: steve/steve-web/bin/steve.apache.wsgi
URL: http://svn.apache.org/viewvc/steve/steve-web/bin/steve.apache.wsgi?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/bin/steve.apache.wsgi (original)
+++ steve/steve-web/bin/steve.apache.wsgi Sun Mar 29 17:40:18 2015
@@ -23,6 +23,6 @@ WSGI script to access STeVe web site.
 
 
 def application(environ, start_response):
-    from asf.steve import app
+    from asf.steve.web import app
     return app.wsgi_app(environ, start_response)
 

Modified: steve/steve-web/bin/steve.uwsgi.wsgi
URL: http://svn.apache.org/viewvc/steve/steve-web/bin/steve.uwsgi.wsgi?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/bin/steve.uwsgi.wsgi (original)
+++ steve/steve-web/bin/steve.uwsgi.wsgi Sun Mar 29 17:40:18 2015
@@ -20,7 +20,7 @@
 """
 WSGI script to access STeVe web site.
 """
-from asf.steve import app
+from asf.steve.web import app
 
 
 def application(environ, start_response):

Modified: steve/steve-web/requirements.txt
URL: http://svn.apache.org/viewvc/steve/steve-web/requirements.txt?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/requirements.txt (original)
+++ steve/steve-web/requirements.txt Sun Mar 29 17:40:18 2015
@@ -1,3 +1,5 @@
+click==3.3
+colorama==0.3.3
 Flask==0.10.1
 Flask-WTF==0.9.5
 Flask-Principal==0.4.0

Modified: steve/steve-web/setup.py
URL: http://svn.apache.org/viewvc/steve/steve-web/setup.py?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/setup.py (original)
+++ steve/steve-web/setup.py Sun Mar 29 17:40:18 2015
@@ -61,6 +61,10 @@ class Doc(Command):
                          '   %s/\n' % (mode, path))
 
 
+with open('requirements.txt') as f:
+    required = f.read().splitlines()
+
+
 setup(
     name='steve-site',
     version=VERSION,
@@ -79,8 +83,20 @@ setup(
     zip_safe=False,
     platforms='any',
 
+    install_requires=required,
+
     tests_require=['tox'],
 
+    entry_points='''
+    [console_scripts]
+    setup = asf.steve.commands.setup:main
+    mkelection = asf.steve.commands.mkelection:main
+
+    [steve_backend.plugins]
+    file-be = asf.steve.backends.file:FileBackend
+    elastic-storage-be = asf.steve.backends.elastic:ElasticStorageBackend
+    ''',
+
     classifiers=[
         'Intended Audience :: Developers',
         'Intended Audience :: System Administrators',

Modified: steve/steve-web/src/asf/steve/__init__.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/__init__.py?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/src/asf/steve/__init__.py (original)
+++ steve/steve-web/src/asf/steve/__init__.py Sun Mar 29 17:40:18 2015
@@ -16,146 +16,5 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-import logging
 
-import flask
-from flask.ext import principal
-
-from asf.steve.api import api
-from asf.steve.admin import admin
-from asf.steve.voter import voter
-
-app = flask.Flask(__name__)
-app.config.from_envvar('STEVE_FLASK_CONFIG')
-
-app.register_blueprint(api, url_prefix='/api')
-app.register_blueprint(admin, url_prefix='/admin')
-app.register_blueprint(voter, url_prefix='/voter')
-
-log = logging.getLogger(__name__)
-
-logging.basicConfig(level=logging.DEBUG)
-
-
-authenticated_permission = principal.Permission(principal.RoleNeed('authenticated'))
-
-
-@app.route('/')
-def hello_world():
-    return flask.render_template('index.html')
-
-
-@app.before_request
-def before_request():
-    flask.g.login_allowed = flask.current_app.debug or flask.request.scheme == 'https'
-
-
-@app.route('/login', methods=['POST'])
-def login():
-    if not flask.g.login_allowed:
-        flask.abort(403)
-
-    username = flask.request.form.get('username')
-    password = flask.request.form.get('password')
-    if ldap_data.check_user_password(username, password):
-        identity = principal.Identity(username, auth_type='ldap')
-        for p in committers.get_committer(username, username, password).projects:
-            identity.provides.add(principal.RoleNeed(p))
-        principal.identity_changed.send(app, identity=identity)
-        flask.flash(u'Signed in as ' + username, 'success')
-    else:
-        flask.flash(u'Invalid username or password', 'danger')
-    return flask.redirect(flask.request.referrer)
-
-
-@app.route('/logout')
-@authenticated_permission.require()
-def logout():
-    principal.identity_changed.send(app, identity=principal.AnonymousIdentity())
-    flask.flash(u'You have been signed out', 'success')
-    return flask.redirect(flask.url_for('hello_world'))
-
-
-is_authenticated = principal.Permission(principal.RoleNeed('authenticated'))
-login_required = is_authenticated.require(401)
-
-principals = principal.Principal(app, skip_static=True)
-
-
-@principals.identity_loader
-def session_identity_loader():
-    if all(key in flask.session for key in ('identity.id', 'identity.auth_type', 'identity.projects')):
-        identity_id = flask.session['identity.id']
-        identity_auth_type = flask.session['identity.auth_type']
-        identity_projects = flask.session['identity.projects']
-
-        if identity_id:
-            identity = principal.Identity(identity_id, identity_auth_type)
-            for p in identity_projects.split(','):
-                identity.provides.add(principal.RoleNeed(p))
-        else:
-            identity = principal.AnonymousIdentity()
-        return identity
-    else:
-        return principal.AnonymousIdentity()
-
-
-@principals.identity_saver
-def session_identity_saver(identity):
-    if identity is None or not identity.is_authenticated:
-        flask.session.pop('identity.id', None)
-        flask.session.pop('identity.auth_type', None)
-    else:
-        flask.session['identity.id'] = identity.id
-        flask.session['identity.auth_type'] = identity.auth_type
-        flask.session['identity.projects'] = ','.join([role.value for role in identity.provides])
-    flask.session.modified = True
-
-
-@principal.identity_changed.connect
-def on_identity_changed(sender, identity):
-    if identity is None:
-        return
-
-    identity.is_authenticated = not isinstance(identity, principal.AnonymousIdentity)
-    if identity.is_authenticated:
-        user = person.Person(identity.id, fault_in_via_ldap=True)
-        identity.person = user
-        identity.provides.add(principal.RoleNeed('authenticated'))
-    else:
-        identity.person = None
-
-
-@principal.identity_loaded.connect
-def on_identity_loaded(sender, identity):
-    if identity is None:
-        return
-
-    identity.is_authenticated = not isinstance(identity, principal.AnonymousIdentity)
-    if identity.is_authenticated:
-        user = person.Person(identity.id, fault_in_via_ldap=False)
-        identity.person = user
-        identity.provides.add(principal.RoleNeed('authenticated'))
-    else:
-        identity.person = None
-
-
-@app.errorhandler(404)
-def not_found_handler(error):
-    return flask.render_template('not_found.html'), 404
-
-
-@app.errorhandler(500)
-def error_handler(error):
-    return flask.render_template('error.html'), 500
-
-
-@app.errorhandler(401)
-@app.errorhandler(403)
-@app.errorhandler(principal.PermissionDenied)
-def forbidden_handler(error):
-    return flask.render_template('forbidden.html'), 403
-
-
-if __name__ == '__main__':
-    app.run(debug=True)
+ELECTION_ENTRY_POINT = 'steve_election.plugins'

Added: steve/steve-web/src/asf/steve/backends/__init__.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/backends/__init__.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/backends/__init__.py (added)
+++ steve/steve-web/src/asf/steve/backends/__init__.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,54 @@
+#
+# 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.
+#
+import pkg_resources
+
+from asf.steve import util
+
+BACKEND_ENTRY_POINT = 'steve_backend.plugins'
+
+
+def load_plugins():
+    """ Load the set of backend plugins whose registered entry point is steve_backend.plugins
+    :return dict: a dictionary of backend plugins indexed by their name
+    """
+    plugins = {}
+
+    for entry_point in pkg_resources.iter_entry_points(BACKEND_ENTRY_POINT):
+        plugins[entry_point.name] = entry_point.load(require=False)
+
+    return plugins
+
+
+def load_plugin(plugin_name, configurations, plugins=None):
+    """ Load and initialize a plugin from configurations
+    :param str plugin_name: the name of the plugin to load
+    :param ConfigParser.RawConfigParser configurations: the configurations to initialze the plugin with
+    :param dict plugins: an optional dictionary of plugin classes
+    :return: an initialized backend plugin
+    """
+    plugins = plugins or load_plugins()
+    plugin_class = plugins[plugin_name]
+
+    if plugin_name not in configurations.sections():
+        raise ValueError('Section %s not in configurations' % plugin_name)
+
+    plugin_properties = util.properties_from_section(configurations, plugin_name)
+    plugin = plugin_class(**plugin_properties)
+
+    return plugin
\ No newline at end of file

Added: steve/steve-web/src/asf/steve/backends/elastic.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/backends/elastic.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/backends/elastic.py (added)
+++ steve/steve-web/src/asf/steve/backends/elastic.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+class ElasticStorageBackend(object):
+    pass
\ No newline at end of file

Added: steve/steve-web/src/asf/steve/backends/file.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/backends/file.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/backends/file.py (added)
+++ steve/steve-web/src/asf/steve/backends/file.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+import hashlib
+import json
+import os
+import random
+import time
+
+
+class FileBackend(object):
+    def __init__(self, home_dir):
+        self.home_dir = home_dir
+
+    def create_election(self, eid, title, owner, monitors, starts, ends, is_open):
+        """ Create an election
+        :param str eid: Election ID
+        :param str title: the title (name) of the election
+        :param str owner: the owner of this election
+        :param list monitors: email addresses to use for monitoring
+        :param str starts: the start date of the election
+        :param str ends: the end date of the election
+        :param bool is_open: flag that indicates if election is open to the public or not
+        """
+        election_hash = hashlib.sha512("%f-stv-%s" % (time.time(), os.environ['REMOTE_ADDR'] if 'REMOTE_ADDR' in os.environ else random.randint(1, 99999999999))).hexdigest(),
+
+        base_data = {
+            'id': eid,
+            'title': title,
+            'owner': owner,
+            'monitors': monitors,
+            'starts': starts,
+            'ends': ends,
+            'hash': election_hash,
+            'open': is_open
+        }
+
+        election_path = os.path.join(self.home_dir, 'issues', eid)
+        os.mkdir(election_path)
+
+        with open(os.path.join(election_path, 'basedata.json'), 'w') as f:
+            f.write(json.dumps(base_data, sort_keys=True, indent=1))
+            f.close()
+
+        with open(os.path.join(election_path, 'voters.json'), 'w') as f:
+            f.write("{}")
+            f.close()

Added: steve/steve-web/src/asf/steve/commands/__init__.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/commands/__init__.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/commands/__init__.py (added)
+++ steve/steve-web/src/asf/steve/commands/__init__.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,41 @@
+#
+# 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.
+#
+import ConfigParser
+import os
+
+import click
+
+
+class Config(click.ParamType):
+    name = 'cfg'
+
+    def convert(self, value, param, ctx):
+        if isinstance(value, basestring):
+            cfg_file = value
+
+            if not os.path.exists(cfg_file):
+                self.fail('Cannot find file %s' % cfg_file, param, ctx)
+
+            value = ConfigParser.RawConfigParser()
+            try:
+                value.read(cfg_file)
+            except Exception as e:
+                raise click.ClickException('Cannot load file %s: %s' % (cfg_file, str(e)))
+
+        return value

Added: steve/steve-web/src/asf/steve/commands/mkelection.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/commands/mkelection.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/commands/mkelection.py (added)
+++ steve/steve-web/src/asf/steve/commands/mkelection.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,56 @@
+#
+# 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.
+#
+import random
+import time
+
+import click
+
+from asf.steve import backends
+from asf.steve.commands import Config
+
+
+CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
+
+
+@click.command(context_settings=CONTEXT_SETTINGS)
+@click.argument('cfg_file', type=Config())
+@click.option('--id', '-i', 'eid', type=str, nargs=1, help='Election ID: If defined, attempt to create an election using this as the election ID')
+@click.option('--owner', '-o', required=True, type=str, nargs=1, help='Sets the owner of this election, as according to steve.cfg')
+@click.option('--title', '-t', required=True, type=str, nargs=1, help='Sets the title (name) of the election')
+@click.option('--monitor', '-m', 'monitors', type=str, nargs=1, multiple=True, help='Email address to use for monitoring')
+@click.option('--public/--private', 'public', default=False, help='If set, create the election as a public (open) election where anyone can vote')
+@click.option('--backend', '-b', default='file-be', type=str, nargs=1, envvar='STEVE_BACKEND', help='Name of backend plugin to store election')
+def main(cfg_file, eid, owner, title, monitors, public, backend):
+    """ Create an election """
+
+    if not eid:
+        eid = ('%08x' % int(time.time() * random.randint(1, 999999999999)))[0:8]
+        click.echo('Created election id %s' % eid)
+
+    if not cfg_file.has_section('general'):
+        raise click.ClickException('Cannot find [general] section')
+
+    if not cfg_file.has_option('karma', owner):
+        raise click.ClickException("Sorry, I could not find '%s' in the karma list in steve.cfg!" % owner)
+
+    click.echo('Using backend %s' % backend)
+    be_plugin = backends.load_plugin(backend, cfg_file)
+
+    be_plugin.create_election(eid, title, owner, monitors, '', '', public)
+
+if __name__ == '__main__':
+    main()

Added: steve/steve-web/src/asf/steve/commands/setup.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/commands/setup.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/commands/setup.py (added)
+++ steve/steve-web/src/asf/steve/commands/setup.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,51 @@
+#
+# 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.
+#
+import os
+
+import click
+
+from asf.steve.commands import Config
+
+CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
+
+
+@click.command(context_settings=CONTEXT_SETTINGS)
+@click.argument('cfg_file', type=Config())
+def main(cfg_file):
+    """ Setup the a STeVe directory as configured by a STeVe configuration file """
+
+    if not cfg_file.has_section('general'):
+        raise click.ClickException('Cannot find [general] section')
+
+    home_dir = cfg_file.get('general', 'homedir')
+
+    if os.path.isdir(home_dir):
+        issues_dir = os.path.join(home_dir, 'issues')
+
+        if not os.path.exists(issues_dir):
+            try:
+                os.mkdir(issues_dir)
+            except Exception as e:
+                raise click.ClickException('Could not create dir: %s: %s' % (issues_dir, str(e)))
+        elif not os.path.isdir(issues_dir):
+            raise click.ClickException('Issues dir %s is not a directory' % issues_dir)
+    else:
+        raise click.ClickException('Home dir %s is not a directory or does not exist' % home_dir)
+
+
+if __name__ == '__main__':
+    main()

Added: steve/steve-web/src/asf/steve/util.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/util.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/util.py (added)
+++ steve/steve-web/src/asf/steve/util.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def properties_from_section(configurations, section_name):
+    """ Extract the properties of a section
+    :param ConfigParser.RawConfigParser configurations: the configurations from which to extract section properties
+    :param section_name: the name of the section
+    :return: the extracted properties of a section
+    """
+    section_properties = {}
+
+    for option_name in configurations.options(section_name):
+        section_properties[option_name] = configurations.get(section_name, option_name)
+
+    return section_properties

Added: steve/steve-web/src/asf/steve/web/__init__.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/web/__init__.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/src/asf/steve/web/__init__.py (added)
+++ steve/steve-web/src/asf/steve/web/__init__.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,162 @@
+#
+# 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.
+#
+import logging
+
+import flask
+from flask.ext import principal
+
+from asf.steve.web.admin import admin
+from asf.steve.web.api import api
+from asf.steve.web.voter import voter
+
+
+app = flask.Flask(__name__)
+app.config.from_envvar('STEVE_FLASK_CONFIG')
+
+app.register_blueprint(api, url_prefix='/api')
+app.register_blueprint(admin, url_prefix='/admin')
+app.register_blueprint(voter, url_prefix='/voter')
+
+log = logging.getLogger(__name__)
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+authenticated_permission = principal.Permission(principal.RoleNeed('authenticated'))
+
+
+@app.route('/')
+def hello_world():
+    return flask.render_template('index.html')
+
+
+@app.before_request
+def before_request():
+    flask.g.login_allowed = flask.current_app.debug or flask.request.scheme == 'https'
+
+
+@app.route('/login', methods=['POST'])
+def login():
+    if not flask.g.login_allowed:
+        flask.abort(403)
+
+    username = flask.request.form.get('username')
+    password = flask.request.form.get('password')
+    if ldap_data.check_user_password(username, password):
+        identity = principal.Identity(username, auth_type='ldap')
+        for p in committers.get_committer(username, username, password).projects:
+            identity.provides.add(principal.RoleNeed(p))
+        principal.identity_changed.send(app, identity=identity)
+        flask.flash(u'Signed in as ' + username, 'success')
+    else:
+        flask.flash(u'Invalid username or password', 'danger')
+    return flask.redirect(flask.request.referrer)
+
+
+@app.route('/logout')
+@authenticated_permission.require()
+def logout():
+    principal.identity_changed.send(app, identity=principal.AnonymousIdentity())
+    flask.flash(u'You have been signed out', 'success')
+    return flask.redirect(flask.url_for('hello_world'))
+
+
+is_authenticated = principal.Permission(principal.RoleNeed('authenticated'))
+login_required = is_authenticated.require(401)
+
+principals = principal.Principal(app, skip_static=True)
+
+
+@principals.identity_loader
+def session_identity_loader():
+    if all(key in flask.session for key in ('identity.id', 'identity.auth_type', 'identity.projects')):
+        identity_id = flask.session['identity.id']
+        identity_auth_type = flask.session['identity.auth_type']
+        identity_projects = flask.session['identity.projects']
+
+        if identity_id:
+            identity = principal.Identity(identity_id, identity_auth_type)
+            for p in identity_projects.split(','):
+                identity.provides.add(principal.RoleNeed(p))
+        else:
+            identity = principal.AnonymousIdentity()
+        return identity
+    else:
+        return principal.AnonymousIdentity()
+
+
+@principals.identity_saver
+def session_identity_saver(identity):
+    if identity is None or not identity.is_authenticated:
+        flask.session.pop('identity.id', None)
+        flask.session.pop('identity.auth_type', None)
+    else:
+        flask.session['identity.id'] = identity.id
+        flask.session['identity.auth_type'] = identity.auth_type
+        flask.session['identity.projects'] = ','.join([role.value for role in identity.provides])
+    flask.session.modified = True
+
+
+@principal.identity_changed.connect
+def on_identity_changed(sender, identity):
+    if identity is None:
+        return
+
+    identity.is_authenticated = not isinstance(identity, principal.AnonymousIdentity)
+    if identity.is_authenticated:
+        user = person.Person(identity.id, fault_in_via_ldap=True)
+        identity.person = user
+        identity.provides.add(principal.RoleNeed('authenticated'))
+    else:
+        identity.person = None
+
+
+@principal.identity_loaded.connect
+def on_identity_loaded(sender, identity):
+    if identity is None:
+        return
+
+    identity.is_authenticated = not isinstance(identity, principal.AnonymousIdentity)
+    if identity.is_authenticated:
+        user = person.Person(identity.id, fault_in_via_ldap=False)
+        identity.person = user
+        identity.provides.add(principal.RoleNeed('authenticated'))
+    else:
+        identity.person = None
+
+
+@app.errorhandler(404)
+def not_found_handler(error):
+    return flask.render_template('not_found.html'), 404
+
+
+@app.errorhandler(500)
+def error_handler(error):
+    return flask.render_template('error.html'), 500
+
+
+@app.errorhandler(401)
+@app.errorhandler(403)
+@app.errorhandler(principal.PermissionDenied)
+def forbidden_handler(error):
+    return flask.render_template('forbidden.html'), 403
+
+
+if __name__ == '__main__':
+    app.run(debug=True)

Modified: steve/steve-web/src/asf/steve/web/admin/__init__.py
URL: http://svn.apache.org/viewvc/steve/steve-web/src/asf/steve/web/admin/__init__.py?rev=1669940&r1=1669774&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/src/asf/steve/web/admin/__init__.py (original)
+++ steve/steve-web/src/asf/steve/web/admin/__init__.py Sun Mar 29 17:40:18 2015
@@ -21,7 +21,8 @@ import logging
 import flask
 from flask.ext import principal
 
-from asf.steve.admin import forms
+from asf.steve.web.admin import forms
+
 
 admin = flask.Blueprint('admin', __name__, template_folder='templates')
 

Added: steve/steve-web/tests/conftest.py
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/conftest.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/tests/conftest.py (added)
+++ steve/steve-web/tests/conftest.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,40 @@
+#
+# 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.
+#
+import ConfigParser
+import os
+
+import pytest
+
+
+@pytest.fixture()
+def data_path():
+    return os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
+
+
+@pytest.fixture()
+def steve_cfg_path(data_path):
+    return os.path.join(data_path, 'steve.cfg')
+
+
+@pytest.fixture()
+def steve_cfg(steve_cfg_path):
+    configurations = ConfigParser.RawConfigParser()
+    configurations.read(steve_cfg_path)
+
+    return configurations

Added: steve/steve-web/tests/data/steve.cfg
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/data/steve.cfg?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/tests/data/steve.cfg (added)
+++ steve/steve-web/tests/data/steve.cfg Sun Mar 29 17:40:18 2015
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+[general]
+homedir=/var/tmp
+
+[karma]
+adc=d
+
+[file-be]
+home_dir=/var/tmp

Added: steve/steve-web/tests/test_backends.py
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/test_backends.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/tests/test_backends.py (added)
+++ steve/steve-web/tests/test_backends.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,40 @@
+#
+# 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.
+#
+import pytest
+
+from asf.steve import backends
+
+
+def test_load_plugins():
+    plugins = backends.load_plugins()
+
+    assert 'file-be' in plugins
+    assert 'elastic-storage-be' in plugins
+
+
+def test_load_plugin(steve_cfg):
+    backends.load_plugin('file-be', steve_cfg)
+
+    # plugin fubar just does not exist
+    with pytest.raises(KeyError):
+        backends.load_plugin('fubar', steve_cfg)
+
+    # plugin elastic-storage-be exists but does not have config
+    with pytest.raises(ValueError):
+        backends.load_plugin('elastic-storage-be', steve_cfg)

Added: steve/steve-web/tests/test_commands.py
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/test_commands.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/tests/test_commands.py (added)
+++ steve/steve-web/tests/test_commands.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,32 @@
+#
+# 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.
+#
+import os
+from ConfigParser import RawConfigParser
+
+from asf.steve import commands
+
+
+def test_config_parameter_type(steve_cfg_path):
+    parameter = commands.Config()
+
+    value = parameter.convert(steve_cfg_path, None, None)
+
+    assert isinstance(value, RawConfigParser)
+
+    assert value == parameter.convert(value, None, None)

Added: steve/steve-web/tests/test_commands_mkelection.py
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/test_commands_mkelection.py?rev=1669940&view=auto
==============================================================================
--- steve/steve-web/tests/test_commands_mkelection.py (added)
+++ steve/steve-web/tests/test_commands_mkelection.py Sun Mar 29 17:40:18 2015
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+from click.testing import CliRunner
+
+from asf.steve.commands import mkelection, setup
+
+
+def test_mkelection(steve_cfg_path):
+    runner = CliRunner()
+
+    result = runner.invoke(setup.main, [steve_cfg_path])
+    assert result.exit_code == 0
+
+    result = runner.invoke(mkelection.main, [steve_cfg_path, '-o', 'adc', '-t', 'foo', '-m', 'a@apache.org', '-m', 'b@apache.org'])
+    assert result.exit_code == 0
+    assert 'Created election id' in result.output
+    assert 'Using backend file-be' in result.output

Modified: steve/steve-web/tests/test_steve.py
URL: http://svn.apache.org/viewvc/steve/steve-web/tests/test_steve.py?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/tests/test_steve.py (original)
+++ steve/steve-web/tests/test_steve.py Sun Mar 29 17:40:18 2015
@@ -7,7 +7,7 @@
 # "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
+# 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
@@ -16,7 +16,23 @@
 # specific language governing permissions and limitations
 # under the License.
 #
+from asf.steve import util
 
 
-def test_steve():
-    pass
\ No newline at end of file
+class MockPlugin(object):
+    def __init__(self, homedir):
+        self.homedir = homedir
+
+
+def test_properties_from_section(steve_cfg):
+    properties = util.properties_from_section(steve_cfg, 'general')
+    assert 'homedir' in properties
+
+
+def test_steve_cfg(steve_cfg):
+    properties = util.properties_from_section(steve_cfg, 'general')
+
+    plugin = MockPlugin(**properties)
+
+    assert plugin
+    assert plugin.homedir == '/var/tmp'

Modified: steve/steve-web/uwsgi.ini
URL: http://svn.apache.org/viewvc/steve/steve-web/uwsgi.ini?rev=1669940&r1=1669939&r2=1669940&view=diff
==============================================================================
--- steve/steve-web/uwsgi.ini (original)
+++ steve/steve-web/uwsgi.ini Sun Mar 29 17:40:18 2015
@@ -1,6 +1,6 @@
 [uwsgi]
 http = 127.0.0.1:8080
-env = STEVE_FLASK_CONFIG=../../../tests/data/steve-flask.properties
+env = STEVE_FLASK_CONFIG=../../../../tests/data/steve-flask.properties
 wsgi-file = ./bin/steve.uwsgi.wsgi
 callable = app
 pythonpath = src