Skip to content

Commit

Permalink
Merge pull request #1683 from yetanotherion/nested
Browse files Browse the repository at this point in the history
forcedialog: integrate children of nested fields
  • Loading branch information
Mikhail Sobolev committed May 24, 2015
2 parents de2d9fa + ae41f90 commit 5b53269
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -30,7 +30,7 @@ pep8:
frontend:
$(PIP) install -e pkg
$(PIP) install mock
for i in base codeparameter console_view waterfall_view; do $(PIP) install -e www/$$i ; done
for i in base codeparameter console_view waterfall_view nestedexample; do $(PIP) install -e www/$$i ; done

# do installation tests. Test front-end can build and install for all install methods
frontend_install_tests:
Expand Down
Expand Up @@ -3,7 +3,7 @@ class forceDialog extends Controller
# prepare default values
prepareFields = (fields) ->
for field in fields
if field.type == 'nested'
if field.fields?
prepareFields(field.fields)
else
field.value = field.default
Expand All @@ -22,7 +22,7 @@ class forceDialog extends Controller
gatherFields = (fields) ->
for field in fields
field.errors = ''
if field.type == 'nested'
if field.fields?
gatherFields(field.fields)
else
params[field.fullName] = field.value
Expand All @@ -39,4 +39,4 @@ class forceDialog extends Controller
$rootScope.$apply()

cancel: ->
modal.modal.dismiss()
modal.modal.dismiss()
65 changes: 65 additions & 0 deletions www/nestedexample/README
@@ -0,0 +1,65 @@
This plugin permits to create two linked UI inputs that can be integrated
in the force dialog:
- pizza: a text field where the name of the pizza (lower or uppercase)
can be written,
- ingredients: a select input where the ingredients to make the pizza
described in the input above can be selected. This input is automatically
populated via a custom webservice provided by the plugin.

The force dialog is the UI element that is displayed
when one clicks on the FORCE button associated to a ForceScheduler.

More precisely the code is composed of two parts:
- python:
* buildbot_nestedexample/__init__.py: definition of NestedExample,
child of buildbot.schedulers.forcesched.NestedParameter.
The two UI elements are embedded in the fields attribute,
and linked to the coffee/jade code by means of its type="nestedexample".
* buildbot_nestedexample/api.py: define the "getIngredients" endpoint
that returns the ingredients necessary to make one pizza.
- coffee:
* plugin boilerplate:
guanlecoja/config.coffee
gulpfile.js
package.json
setup.py
* ui logic:
src/module/nestedexamplefield.directive.coffee
src/module/nestedexamplefield.tpl.jade

Note that the name of the files match, as they must, the following naming
convention:
<type>field.directive.coffee
<type>field.tpl.jade

Regarding the coffee code, the only non standard angular code, is the one
that permits:
* ...directive.coffee: to extract the embedded elements from the scope, and
* ...tpl.jade: to communicate to the two basefield-s where the embedded elements are.
This permits to benefit from the error displaying features provided
by basefield.
Please have a look at the commentaries in the code for more details.

To activate that plugin in one buildbot instance,
one should:
- add that UI element in the ForceScheduler like,

...
from buildbot_nestedexample import NestedExample
from buildbot.schedulers.forcesched import ForceScheduler

ForceScheduler(codebases=[CodebaseParameter(codebase="",
branch=FixedParameter(name="branch", default=""),
revision=FixedParameter(name="revision", default=""),
repository=FixedParameter(name="repository", default=""),
project=FixedParameter(name="project", default=""))],
reason=StringParameter(name="reason",
default=""),
properties=[NestedExample(required=True,
default="",
size=80)])

- and activate the plugin in the buildbot configuration,

c['www'] = dict(...
plugins=dict(nestedexample={}))
69 changes: 69 additions & 0 deletions www/nestedexample/buildbot_nestedexample/__init__.py
@@ -0,0 +1,69 @@
from twisted.internet import defer

from buildbot.www.plugin import Application
from buildbot.schedulers.forcesched import NestedParameter
from buildbot.schedulers.forcesched import StringParameter
from buildbot.schedulers.forcesched import ChoiceStringParameter
from buildbot.schedulers.forcesched import ValidationError

from .api import Api


class NestedExample(NestedParameter):

"""UI zone"""
type = "nestedexample"
PIZZA = "pizza"
INGREDIENTS = "ingredients"

def __init__(self, **kw):
pizzaInput = StringParameter(label="type the name of your pizza",
name=self.PIZZA,
required=True)
ingredientsInput = ChoiceStringParameter(name=self.INGREDIENTS,
label="ingredients necessary to make the pizza",
multiple=True,
strict=False,
default="",
choices=[])
self.params = {self.PIZZA: pizzaInput,
self.INGREDIENTS: ingredientsInput}
self.allIngredients = set(sum([ingr for ingr in Api.pizzaIngredients.values()],
[]))
fields = self.params.values()
super(NestedExample, self).__init__(self.type, label='', fields=fields, **kw)

def createNestedPropertyName(self, propertyName):
return "{}_{}".format(self.type, propertyName)

@defer.inlineCallbacks
def validateProperties(self, collector, properties):
# we implement the check between the input and
# the ingredients
if properties[self.INGREDIENTS] not in self.allIngredients or\
not properties[self.PIZZA]:
# we trigger a specific error message in PIZZA only
def f():
return defer.fail(ValidationError('Invalid pizza'))
nestedProp = self.createNestedPropertyName(self.PIZZA)
yield collector.collectValidationErrors(nestedProp, f)

@defer.inlineCallbacks
def updateFromKwargs(self, kwargs, properties, collector, **kw):
yield super(NestedExample, self).updateFromKwargs(kwargs, properties, collector, **kw)
# the properties we have are in the form
# {nestedexample: {input: <url>,
# ingredients: <ingredients>}}
# we just flatten the dict to have
# - input, and
# - ingredients
# in properties
for prop, val in properties.pop(self.type).iteritems():
properties[prop] = val
yield self.validateProperties(collector, properties)


# create the interface for the setuptools entry point
ep = Application(__name__, "Buildbot nested parameter example")
api = Api(ep)
ep.resource.putChild("api", api.app.resource())
24 changes: 24 additions & 0 deletions www/nestedexample/buildbot_nestedexample/api.py
@@ -0,0 +1,24 @@
import json

from klein import Klein
from twisted.internet import defer


class Api(object):
app = Klein()
pizzaIngredients = {'margherita': ['tomato', 'ham', 'cheese'],
'regina': ['tomato', 'ham', 'cheese', 'mushrooms']}

def __init__(self, ep):
self.ep = ep

@app.route("/getIngredients", methods=['GET'])
def getIngredients(self, request):
pizzaArgument = request.args.get('pizza')
if pizzaArgument is None:
return defer.succeed(json.dumps("invalid request"))
pizza = pizzaArgument[0].lower()
res = self.pizzaIngredients.get(pizza,
["only {} are supported "
"for now".format(self.pizzaIngredients.keys())])
return defer.succeed(json.dumps(res))
47 changes: 47 additions & 0 deletions www/nestedexample/guanlecoja/config.coffee
@@ -0,0 +1,47 @@
### ###############################################################################################
#
# This module contains all configuration for the build process
#
### ###############################################################################################
ANGULAR_TAG = "~1.3.0"

config =

### ###########################################################################################
# Name of the plugin
### ###########################################################################################
name: 'nestedexample'


### ###########################################################################################
# Directories
### ###########################################################################################
dir:
# The build folder is where the app resides once it's completely built
build: 'buildbot_nestedexample/static'

### ###########################################################################################
# Bower dependancies configuration
### ###########################################################################################
bower:
testdeps:
jquery:
version: '2.1.1'
files: 'dist/jquery.js'
angular:
version: ANGULAR_TAG
files: 'angular.js'
lodash:
version: "~2.4.1"
files: 'dist/lodash.js'
"angular-mocks":
version: ANGULAR_TAG
files: "angular-mocks.js"

buildtasks: ['scripts', 'styles', 'fonts', 'imgs',
'index', 'tests', 'generatedfixtures', 'fixtures']

karma:
# we put tests first, so that we have angular, and fake app defined
files: ["tests.js", "scripts.js", 'fixtures.js', "mode-python.js"]
module.exports = config
1 change: 1 addition & 0 deletions www/nestedexample/gulpfile.js
@@ -0,0 +1 @@
require("guanlecoja")(require("gulp"))
10 changes: 10 additions & 0 deletions www/nestedexample/package.json
@@ -0,0 +1,10 @@
{
"name": "buildbot-nestedexample",
"engines": {
"node": ">=0.10.0",
"npm": ">=1.4.0"
},
"devDependencies": {
"guanlecoja": "~0.3.5"
}
}
31 changes: 31 additions & 0 deletions www/nestedexample/setup.py
@@ -0,0 +1,31 @@
#!/usr/bin/env python
try:
from buildbot_pkg import setup_www_plugin
except ImportError:
import sys
print >> sys.stderr, "Please install buildbot_pkg module in order to install that package, or use the pre-build .whl modules available on pypi"
sys.exit(1)

setup_www_plugin(
name='buildbot-nestedexample',
description='"An example of a custom nested parameter"',
author=u'Ion Alberdi',
author_email=u'ialberdi@intel.com',
url='http://buildbot.net/',
license='GNU GPL',
version='0.0.1',
packages=['buildbot_nestedexample'],
install_requires=[
'klein'
],
package_data={
'': [
'VERSION',
'static/*'
]
},
entry_points="""
[buildbot.www]
nestedexample = buildbot_nestedexample:ep
""",
)
70 changes: 70 additions & 0 deletions www/nestedexample/src/module/nestedexamplefield.directive.coffee
@@ -0,0 +1,70 @@
# Register new module
class Nestedexample extends App
constructor: -> return [
'common'
]

class Nestedexamplefield extends Directive
constructor: ->
return {
replace: false
restrict: 'E'
scope: false
templateUrl: "nestedexample/views/nestedexamplefield.html"
controller: '_nestedexamplefieldController'
}

class _nestedexamplefield extends Controller
constructor: ($scope, $http) ->
# boilerplate to extract our two embedded
# UI elements

# the name of the embedded UI elements
# are prefixed by the type of the root
# element, "nestedexample" in our case.
# This method permits to compute that
# prefixed name.
createNestedName = (name) ->
return "nestedexample_#{name}"

# utility method to find the embedded
# field from the scope
findNestedElement = (name) ->
nameInNestedField = createNestedName(name)
res = undefined
$scope.field.fields.forEach (v, i) ->
if v.fullName == nameInNestedField
res = v
return
return res

# we put our two embedded fields in the scope
$scope.pizza = findNestedElement('pizza')
$scope.ingredients = findNestedElement('ingredients')

# function that will be called each time a change
# event happens in the pizza input.
ingredientsUrl = (pizza) ->
return "nestedexample/api/getIngredients?pizza=#{pizza}"

updateValues = (pizza) ->
if pizza == ""
$scope.ingredients.choices = []
$scope.ingredients.value = ""
return
$http.get(ingredientsUrl(pizza)).then (r) ->
if r.status == 200
if r.data.error?
$scope.ingredients.choices = [r.data.error]
else
$scope.ingredients.choices = r.data
else
error = "unexpected error got #{r.status}"
$scope.ingredients.choices = [error]
if $scope.ingredients.choices.length > 0
$scope.ingredients.value = $scope.ingredients.choices[0]
else
$scope.ingredients.value = ""

$scope.getIngredients = () ->
updateValues($scope.pizza.value)
23 changes: 23 additions & 0 deletions www/nestedexample/src/module/nestedexamplefield.tpl.jade
@@ -0,0 +1,23 @@
div
//- basefield is an angular-js directive provided by buildbot-nine.
//- (ng-init="field=pizza") (ng-init="field=ingredients")
//- are necessary to benefit
//- from the error reporting mechanism provided by basefield
//- in our embedded UI elements. They just say that the field
//- element to process is not the root element (here nestedexample) but
//- each of the embedded elements.
basefield(ng-init="field=pizza")
label.control-label.col-sm-2(for="{{pizza.name}}")
| {{pizza.label}}
.col-sm-10
input.form-control(type='text',
name="{{pizza.name}}",
ng-model="pizza.value",
ng-change="getIngredients()")
basefield(ng-init="field=ingredients")
label.control-label.col-sm-2(for="{{ingredients.name}}")
| {{ingredients.label}}
.col-sm-10
select.form-control(name="{{ingredients.name}}",
ng-model="ingredients.value",
ng-options="v for v in ingredients.choices")
Empty file.

0 comments on commit 5b53269

Please sign in to comment.