You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2013/02/25 22:56:17 UTC
[16/50] [abbrv] git commit: [#5658] Admin UI for posting a task
[#5658] Admin UI for posting a task
Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/550d4f9c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/550d4f9c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/550d4f9c
Branch: refs/heads/si/5453
Commit: 550d4f9c4624cff82b488d51beb46c5e1d2bd11d
Parents: 7f209c9
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Feb 20 19:39:52 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Thu Feb 21 16:32:23 2013 +0000
----------------------------------------------------------------------
Allura/allura/controllers/site_admin.py | 41 ++++++-
Allura/allura/lib/validators.py | 77 +++++++++++
Allura/allura/templates/site_admin_task_list.html | 2 +
Allura/allura/templates/site_admin_task_new.html | 112 ++++++++++++++++
Allura/allura/tests/functional/test_site_admin.py | 42 ++++++-
Allura/allura/tests/test_validators.py | 104 ++++++++++++++
6 files changed, 375 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/controllers/site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/site_admin.py b/Allura/allura/controllers/site_admin.py
index a4d5f64..7bb2c5d 100644
--- a/Allura/allura/controllers/site_admin.py
+++ b/Allura/allura/controllers/site_admin.py
@@ -1,9 +1,10 @@
import re
+import json
import logging
from datetime import datetime, timedelta
from collections import defaultdict
-from tg import expose, validate, flash, config, request
+from tg import expose, validate, flash, config, redirect
from tg.decorators import with_trailing_slash, without_trailing_slash
from ming.orm import session
import pymongo
@@ -11,9 +12,11 @@ import bson
import tg
from pylons import tmpl_context as c, app_globals as g
from pylons import request
-from formencode import validators
+from formencode import validators, Invalid
from allura.lib import helpers as h
+from allura.lib import validators as v
+from allura.lib.decorators import require_post
from allura.lib.security import require_access
from allura.lib.widgets import form_fields as ffw
from allura import model as M
@@ -303,3 +306,37 @@ class TaskManagerController(object):
task.app_config = M.AppConfig.query.get(_id=task.context.app_config_id)
task.user = M.User.query.get(_id=task.context.user_id)
return dict(task=task)
+
+ @expose('jinja:allura:templates/site_admin_task_new.html')
+ @without_trailing_slash
+ def new(self, **kw):
+ """Render the New Task form"""
+ return dict(
+ form_errors=c.form_errors or {},
+ form_values=c.form_values or {},
+ )
+
+ @expose()
+ @require_post()
+ @validate(v.CreateTaskSchema(), error_handler=new)
+ def create(self, task, task_args=None, user=None, path=None):
+ """Post a new task"""
+ args = task_args.get("args", ())
+ kw = task_args.get("kwargs", {})
+ config_dict = path
+ if user:
+ config_dict['user'] = user
+ with h.push_config(c, **config_dict):
+ task = task.post(*args, **kw)
+ redirect('view', task_id=task._id)
+
+ @expose('json:')
+ def task_doc(self, task_name):
+ """Return a task's docstring"""
+ error, doc = None, None
+ try:
+ task = v.TaskValidator.to_python(task_name)
+ doc = task.__doc__ or 'No doc string available'
+ except Invalid as e:
+ error = str(e)
+ return dict(doc=doc, error=error)
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/lib/validators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index b7ff459..de6bf51 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -55,7 +55,69 @@ class MaxBytesValidator(fev.FancyValidator):
def from_python(self, value, state):
return h.really_unicode(value or '')
+class TaskValidator(fev.FancyValidator):
+ def _to_python(self, value, state):
+ try:
+ mod, func = value.rsplit('.', 1)
+ except ValueError:
+ raise fe.Invalid('Invalid task name. Please provide the full '
+ 'dotted path to the python callable.', value, state)
+ try:
+ mod = __import__(mod, fromlist=[str(func)])
+ except ImportError:
+ raise fe.Invalid('Could not import "%s"' % value, value, state)
+
+ try:
+ task = getattr(mod, func)
+ except AttributeError:
+ raise fe.Invalid('Module has no attribute "%s"' % func, value, state)
+
+ if not hasattr(task, 'post'):
+ raise fe.Invalid('"%s" is not a task.' % value, value, state)
+ return task
+
+class UserValidator(fev.FancyValidator):
+ def _to_python(self, value, state):
+ from allura import model as M
+ user = M.User.by_username(value)
+ if not user:
+ raise fe.Invalid('Invalid username', value, state)
+ return user
+
+class PathValidator(fev.FancyValidator):
+ def _to_python(self, value, state):
+ from allura import model as M
+
+ parts = value.strip('/').split('/')
+ if len(parts) < 2:
+ raise fe.Invalid("You must specify at least a neighborhood and "
+ "project, i.e. '/nbhd/project'", value, state)
+ elif len(parts) == 2:
+ nbhd_name, project_name, app_name = parts[0], parts[1], None
+ elif len(parts) > 2:
+ nbhd_name, project_name, app_name = parts[0], parts[1], parts[2]
+
+ path_parts = {}
+ nbhd_url_prefix = '/%s/' % nbhd_name
+ nbhd = M.Neighborhood.query.get(url_prefix=nbhd_url_prefix)
+ if not nbhd:
+ raise fe.Invalid('Invalid neighborhood: %s' % nbhd_url_prefix, value, state)
+
+ project = M.Project.query.get(shortname=project_name, neighborhood_id=nbhd._id)
+ if not project:
+ raise fe.Invalid('Invalid project: %s' % project_name, value, state)
+
+ path_parts['project'] = project
+ if app_name:
+ app = project.app_instance(app_name)
+ if not app:
+ raise fe.Invalid('Invalid app mount point: %s' % app_name, value, state)
+ path_parts['app'] = app
+
+ return path_parts
+
class JsonValidator(fev.FancyValidator):
+ """Validates a string as JSON and returns the original string"""
def _to_python(self, value, state):
try:
json.loads(value)
@@ -63,6 +125,21 @@ class JsonValidator(fev.FancyValidator):
raise fe.Invalid('Invalid JSON: ' + str(e), value, state)
return value
+class JsonConverter(fev.FancyValidator):
+ """Deserializes a string to JSON and returns a Python object"""
+ def _to_python(self, value, state):
+ try:
+ obj = json.loads(value)
+ except ValueError, e:
+ raise fe.Invalid('Invalid JSON: ' + str(e), value, state)
+ return obj
+
+class CreateTaskSchema(fe.Schema):
+ task = TaskValidator(not_empty=True, strip=True)
+ task_args = JsonConverter(if_missing=dict(args=[], kwargs={}))
+ user = UserValidator(strip=True, if_missing=None)
+ path = PathValidator(strip=True, if_missing={})
+
class DateValidator(fev.FancyValidator):
def _to_python(self, value, state):
value = convertDate(value)
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/templates/site_admin_task_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/site_admin_task_list.html b/Allura/allura/templates/site_admin_task_list.html
index 5862a9a..5539a94 100644
--- a/Allura/allura/templates/site_admin_task_list.html
+++ b/Allura/allura/templates/site_admin_task_list.html
@@ -52,6 +52,8 @@
<input type="submit" />
<input type="hidden" name="page_num" value="{{ page_num }}" />
<input type="hidden" name="minutes" value="{{ minutes }}" />
+
+ <a href="task_manager/new">Create a new task</a>
</form>
{{ _paging() }}
<div class="paging-window">
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/templates/site_admin_task_new.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/site_admin_task_new.html b/Allura/allura/templates/site_admin_task_new.html
new file mode 100644
index 0000000..45cb0f6
--- /dev/null
+++ b/Allura/allura/templates/site_admin_task_new.html
@@ -0,0 +1,112 @@
+{% set page="task_manager" %}
+{% set sidebar_rel = '../' %}
+{% extends 'allura:templates/site_admin.html' %}
+
+{% block extra_css %}
+<style>
+ form {
+ margin: 1em;
+ }
+ form > div {
+ margin-bottom: 1em;
+ }
+ form > div > *{
+ display: inline-block;
+ vertical-align: top;
+ }
+ form > div input,
+ form > div textarea,
+ form > .input {
+ display: block;
+ width: 300px;
+ }
+ form > div label {
+ width: 100px;
+ }
+ .error {
+ width: 300px;
+ background: none;
+ border: none;
+ color: #f00;
+ margin: 0;
+ padding: 0 0 0 .8em;
+ }
+ pre.doc {
+ clear: left;
+ }
+ .note {
+ font-size: small;
+ font-style: italic;
+ }
+</style>
+{% endblock %}
+
+{% macro error(field) %}
+ {% if form_errors.get(field) %}
+ <span class="error">{{form_errors.get(field)}}</span>
+ {% endif %}
+{% endmacro %}
+
+{% block content %}
+<h2>New Task</h2>
+<form method="POST" action="create" id="newtask">
+ <div>
+ <label>Task Name *</label>
+ <div class="input">
+ <input name="task" value="{{form_values.get('task', '')}}" />
+ <span class="note">Dotted python path to task callable</span>
+ </div>
+ {{error('task')}}
+ </div>
+ <div>
+ <label>c.user</label>
+ <div class="input">
+ <input name="user" value="{{form_values.get('user', '')}}" />
+ <span class="note">Username</span>
+ </div>
+ {{error('user')}}
+ </div>
+ <div>
+ <label>c.project/c.app</label>
+ <div class="input">
+ <input name="path" value="{{form_values.get('path', '')}}" />
+ <span class="note">e.g. /p/allura or /p/allura/git</span>
+ </div>
+ {{error('path')}}
+ </div>
+ <div>
+ <label>Task args/kwargs</label>
+ <div class="input">
+ <textarea name="task_args" rows="4">{{form_values.get('task_args', '{\n "args": [],\n "kwargs": {}\n}')}}</textarea>
+ </div>
+ {{error('task_args')}}
+ </div>
+
+ <input type="submit" /><br/>
+
+ <pre class="doc"></pre>
+</form>
+{% endblock %}
+
+{% block extra_js %}
+<script>
+ $(function() {
+ var $task = $('input[name=task]');
+ $task.blur(function() {
+ $.get("task_doc", {task_name: $task.val()}, function(data) {
+ $task.parent().siblings('.error').remove();
+ $('pre.doc').empty();
+ if (data.error) {
+ $task.parent().after('<span class="error">' + data.error + '</span>');
+ }
+ if (data.doc) {
+ $('pre.doc').html(data.doc);
+ }
+ });
+ });
+ if ($task.val().trim() !== '') {
+ $task.blur();
+ }
+ });
+</script>
+{% endblock %}
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/tests/functional/test_site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index 6483b0f..b5f2d10 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -1,6 +1,8 @@
+import json
+
from allura import model as M
from allura.tests import TestController
-
+from allura.lib.decorators import task
class TestSiteAdmin(TestController):
@@ -87,3 +89,41 @@ class TestSiteAdmin(TestController):
r = self.app.get(url, extra_environ=dict(username='*anonymous'), status=302)
r = self.app.get(url)
assert 'math.ceil' in r, r
+
+ def test_task_new(self):
+ r = self.app.get('/nf/admin/task_manager/new')
+ assert 'New Task' in r, r
+
+ def test_task_create(self):
+ project = M.Project.query.get(shortname='test')
+ app = project.app_instance('admin')
+ user = M.User.by_username('root')
+
+ task_args = dict(
+ args=['foo'],
+ kwargs=dict(bar='baz'))
+
+ r = self.app.post('/nf/admin/task_manager/create', params=dict(
+ task='allura.tests.functional.test_site_admin.test_task',
+ task_args=json.dumps(task_args),
+ user='root',
+ path='/p/test/admin',
+ ), status=302)
+ task = M.MonQTask.query.find({}).sort('_id', -1).next()
+ assert str(task._id) in r.location
+ assert task.context['project_id'] == project._id
+ assert task.context['app_config_id'] == app.config._id
+ assert task.context['user_id'] == user._id
+ assert task.args == task_args['args']
+ assert task.kwargs == task_args['kwargs']
+
+ def test_task_doc(self):
+ r = self.app.get('/nf/admin/task_manager/task_doc', params=dict(
+ task_name='allura.tests.functional.test_site_admin.test_task'))
+ assert json.loads(r.body)['doc'] == 'test_task doc string'
+
+
+@task
+def test_task(*args, **kw):
+ """test_task doc string"""
+ pass
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/550d4f9c/Allura/allura/tests/test_validators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_validators.py b/Allura/allura/tests/test_validators.py
new file mode 100644
index 0000000..5472e2d
--- /dev/null
+++ b/Allura/allura/tests/test_validators.py
@@ -0,0 +1,104 @@
+import unittest
+import formencode as fe
+
+from allura import model as M
+from allura.lib import validators as v
+from allura.lib.decorators import task
+from alluratest.controller import setup_basic_test
+
+
+def setUp():
+ setup_basic_test()
+
+
+@task
+def dummy_task(*args, **kw):
+ pass
+
+
+class TestJsonConverter(unittest.TestCase):
+ val = v.JsonConverter
+
+ def test_valid(self):
+ self.assertEqual({}, self.val.to_python('{}'))
+
+ def test_invalid(self):
+ with self.assertRaises(fe.Invalid):
+ self.val.to_python('{')
+
+
+class TestUserValidator(unittest.TestCase):
+ val = v.UserValidator
+
+ def test_valid(self):
+ self.assertEqual(M.User.by_username('root'), self.val.to_python('root'))
+
+ def test_invalid(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('fakeuser')
+ self.assertEqual(str(cm.exception), "Invalid username")
+
+
+class TestTaskValidator(unittest.TestCase):
+ val = v.TaskValidator
+
+ def test_valid(self):
+ self.assertEqual(dummy_task, self.val.to_python('allura.tests.test_validators.dummy_task'))
+
+ def test_invalid_name(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('badname')
+ self.assertTrue(str(cm.exception).startswith('Invalid task name'))
+
+ def test_import_failure(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('allura.does.not.exist')
+ self.assertEqual(str(cm.exception), 'Could not import "allura.does.not.exist"')
+
+ def test_attr_lookup_failure(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('allura.tests.test_validators.typo')
+ self.assertEqual(str(cm.exception), 'Module has no attribute "typo"')
+
+ def test_not_a_task(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('allura.tests.test_validators.setUp')
+ self.assertEqual(str(cm.exception), '"allura.tests.test_validators.setUp" is not a task.')
+
+
+class TestPathValidator(unittest.TestCase):
+ val = v.PathValidator
+
+ def test_valid_project(self):
+ project = M.Project.query.get(shortname='test')
+ d = self.val.to_python('/p/test')
+ self.assertEqual(d['project'], project)
+ self.assertTrue('app' not in d)
+
+ def test_valid_app(self):
+ project = M.Project.query.get(shortname='test')
+ app = project.app_instance('admin')
+ d = self.val.to_python('/p/test/admin/')
+ self.assertEqual(d['project'], project)
+ self.assertEqual(d['app'].config._id, app.config._id)
+
+ def test_invalid_format(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('test')
+ self.assertTrue(str(cm.exception).startswith(
+ 'You must specify at least a neighborhood and project'))
+
+ def test_invalid_neighborhood(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('/q/test')
+ self.assertEqual(str(cm.exception), 'Invalid neighborhood: /q/')
+
+ def test_invalid_project(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('/p/badproject')
+ self.assertEqual(str(cm.exception), 'Invalid project: badproject')
+
+ def test_invalid_app_mount_point(self):
+ with self.assertRaises(fe.Invalid) as cm:
+ self.val.to_python('/p/test/badapp')
+ self.assertEqual(str(cm.exception), 'Invalid app mount point: badapp')