launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02392
[Merge] lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad
Tim Penhey has proposed merging lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad with lp:~thumper/launchpad/recipe-inline-edit-owner as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#423149 Wacky error when using the inline edit for bug task project in Chromium
https://bugs.launchpad.net/bugs/423149
For more details, see:
https://code.launchpad.net/~thumper/launchpad/inline-ppa-chooser/+merge/46983
The primary purpose of this branch is to add an inline picker
for the daily build PPA. This brought around much refactoring
and simplification.
lib/canonical/widgets/lazrjs.py
- cleaned up the docstring for the InlineEditPickerWidget
- renamed some parameters and removed the jsonification of
some where it was wrong
lib/canonical/widgets/templates/inline-picker.pt
- changed the registration code to run on load
lib/canonical/widgets/tests/test_inlineeditpickerwidget.py
- added tests for the InlineEditPickerWidget
lib/lp/registry/browser/webservice.py => lib/lp/app/browser/webservice.py
lib/lp/registry/browser/webservice.py
lib/lp/app/browser/webservice.py
- moved the person_xhtml_representation to from lp.registry
to lp.app and made more generic, and renamed to
reference_xhtml_representation
- moved the text_xhtml_representation to lp.app
lib/lp/app/browser/tales.py
- tweaked format_link to raise if link isn't supported
lib/lp/app/javascript/picker.js
- renamed non_searchable_vocabulary to show_search_box
- fixed up the config value existance checks
- fixed the code where it was checking to see of the content
uri had changed to use the full_resource_uri
lib/lp/bugs/javascript/bugtask_index.js
- Remove the webkit special casing for product editing on
bugtask rows
lib/lp/code/browser/sourcepackagerecipe.py
- show that daily builds that haven't specified a PPA will
not get run.
- add InlineEditPickerWidgets for person and archive
lib/lp/code/model/sourcepackagerecipebuild.py
- have the daily build code handle the case where the recipe
doesn't have an archive specified
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
- and test it here
lib/lp/code/templates/sourcepackagerecipe-index.pt
- clean up the page template now that we are using the widget
--
https://code.launchpad.net/~thumper/launchpad/inline-ppa-chooser/+merge/46983
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad.
=== modified file 'lib/canonical/widgets/lazrjs.py'
--- lib/canonical/widgets/lazrjs.py 2011-01-20 22:32:56 +0000
+++ lib/canonical/widgets/lazrjs.py 2011-01-20 22:33:30 +0000
@@ -18,6 +18,7 @@
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.component import getUtility
from zope.security.checker import canAccess, canWrite
+from zope.schema.interfaces import IVocabulary
from zope.schema.vocabulary import getVocabularyRegistry
from lazr.restful.interfaces import IWebServiceClientRequest
@@ -26,6 +27,7 @@
from canonical.launchpad.webapp.interfaces import ILaunchBag
from canonical.launchpad.webapp.publisher import canonical_url
from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
+from lp.services.propertycache import cachedproperty
class TextLineEditorWidget:
@@ -260,21 +262,22 @@
__call__ = ViewPageTemplateFile('templates/inline-picker.pt')
def __init__(self, context, request, interface_attribute, default_html,
- id=None, header='Select an item', step_title='Search',
- remove_button_text='Remove', null_display_value='None'):
+ content_box_id=None, header='Select an item',
+ step_title='Search', remove_button_text='Remove',
+ null_display_value='None'):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param request: The request object.
- :param interface_attribute: The attribute being edited.
+ :param interface_attribute: The attribute being edited. This should be
+ a field from an interface of the form ISomeInterface['fieldname']
:param default_html: Default display of attribute.
- :param id: The HTML id to use for this widget. Automatically
+ :param content_box_id: The HTML id to use for this widget. Automatically
generated if this is not provided.
:param header: The large text at the top of the picker.
:param step_title: Smaller line of text below the header.
- :param show_remove_button: Show remove button below search box.
- :param show_assign_me_button: Show assign-me button below search box.
:param remove_button_text: Override default button text: "Remove"
+ :param null_display_value: This will be shown for a missing value
"""
self.context = context
self.request = request
@@ -282,10 +285,10 @@
self.interface_attribute = interface_attribute
self.attribute_name = interface_attribute.__name__
- if id is None:
- self.id = self._generate_id()
+ if content_box_id is None:
+ self.content_box_id = self._generate_id()
else:
- self.id = id
+ self.content_box_id = content_box_id
self.header = header
self.step_title = step_title
@@ -293,12 +296,11 @@
self.null_display_value = null_display_value
# JSON encoded attributes.
- self.json_id = simplejson.dumps(self.id)
+ self.json_content_box_id = simplejson.dumps(self.content_box_id)
self.json_attribute = simplejson.dumps(self.attribute_name + '_link')
- self.vocabulary_name = simplejson.dumps(
+ self.json_vocabulary_name = simplejson.dumps(
self.interface_attribute.vocabularyName)
- self.show_remove_button = simplejson.dumps(
- not self.interface_attribute.required)
+ self.show_remove_button = not self.interface_attribute.required
@property
def config(self):
@@ -307,16 +309,25 @@
remove_button_text=self.remove_button_text,
null_display_value=self.null_display_value,
show_remove_button=self.show_remove_button,
- show_assign_me_button=self.show_assign_me_button))
+ show_assign_me_button=self.show_assign_me_button,
+ show_search_box=self.show_search_box))
+
+ @cachedproperty
+ def vocabulary(self):
+ registry = getVocabularyRegistry()
+ return registry.get(
+ IVocabulary, self.interface_attribute.vocabularyName)
+
+ @property
+ def show_search_box(self):
+ return IHugeVocabulary.providedBy(self.vocabulary)
@property
def show_assign_me_button(self):
# show_assign_me_button is true if user is in the vocabulary.
- registry = getVocabularyRegistry()
- vocabulary = registry.get(
- IHugeVocabulary, self.interface_attribute.vocabularyName)
+ vocabulary = self.vocabulary
user = getUtility(ILaunchBag).user
- return simplejson.dumps(user and user in vocabulary)
+ return user and user in vocabulary
@classmethod
def _generate_id(cls):
@@ -325,7 +336,7 @@
return 'inline-picker-activator-id-%d' % cls.last_id
@property
- def resource_uri(self):
+ def json_resource_uri(self):
return simplejson.dumps(
canonical_url(
self.context, request=IWebServiceClientRequest(self.request),
=== modified file 'lib/canonical/widgets/templates/inline-picker.pt'
--- lib/canonical/widgets/templates/inline-picker.pt 2011-01-20 22:32:56 +0000
+++ lib/canonical/widgets/templates/inline-picker.pt 2011-01-20 22:33:30 +0000
@@ -1,4 +1,4 @@
-<span tal:attributes="id view/id">
+<span tal:attributes="id view/content_box_id">
<span class="yui3-activator-data-box">
<tal:attribute replace="structure view/default_html"/>
</span>
@@ -14,11 +14,13 @@
return;
}
- Y.lp.app.picker.addPickerPatcher(
- ${view/vocabulary_name},
- ${view/resource_uri},
+ Y.on('load', function(e) {
+ Y.lp.app.picker.addPickerPatcher(
+ ${view/json_vocabulary_name},
+ ${view/json_resource_uri},
${view/json_attribute},
- ${view/json_id},
+ ${view/json_content_box_id},
${view/config});
+ }, window);
});
"/>
=== renamed directory 'lib/canonical/widgets/ftests' => 'lib/canonical/widgets/tests'
=== added file 'lib/canonical/widgets/tests/test_inlineeditpickerwidget.py'
--- lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 2011-01-20 22:33:30 +0000
@@ -0,0 +1,57 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the InlineEditPickerWidget."""
+
+__metaclass__ = type
+
+from zope.interface import Interface
+from zope.schema import Choice
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.widgets.lazrjs import InlineEditPickerWidget
+from lp.testing import (
+ login_person,
+ TestCaseWithFactory,
+ )
+
+
+class TestInlineEditPickerWidget(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def getWidget(self, **kwargs):
+ class ITest(Interface):
+ test_field = Choice(**kwargs)
+ return InlineEditPickerWidget(None, None, ITest['test_field'], None)
+
+ def test_huge_vocabulary_is_searchable(self):
+ # Make sure that when given a field for a huge vocabulary, the picker
+ # is set to show the search box.
+ widget = self.getWidget(vocabulary='ValidPersonOrTeam')
+ self.assertTrue(widget.show_search_box)
+
+ def test_normal_vocabulary_is_not_searchable(self):
+ # Make sure that when given a field for a normal vocabulary, the picker
+ # is set to show the search box.
+ widget = self.getWidget(vocabulary='UserTeamsParticipation')
+ self.assertFalse(widget.show_search_box)
+
+ def test_required_fields_dont_have_a_remove_link(self):
+ widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
+ self.assertFalse(widget.show_remove_button)
+
+ def test_optional_fields_do_have_a_remove_link(self):
+ widget = self.getWidget(
+ vocabulary='ValidPersonOrTeam', required=False)
+ self.assertTrue(widget.show_remove_button)
+
+ def test_assign_me_exists_if_user_in_vocabulary(self):
+ widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
+ login_person(self.factory.makePerson())
+ self.assertTrue(widget.show_assign_me_button)
+
+ def test_assign_me_not_shown_if_user_not_in_vocabulary(self):
+ widget = self.getWidget(vocabulary='TargetPPAs', required=True)
+ login_person(self.factory.makePerson())
+ self.assertFalse(widget.show_assign_me_button)
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2011-01-06 01:36:25 +0000
+++ lib/lp/app/browser/configure.zcml 2011-01-20 22:33:30 +0000
@@ -556,4 +556,10 @@
<adapter
factory="lp.app.browser.tales.LaunchpadLayerToMainTemplateAdapter"
/>
+
+ <adapter
+ factory="lp.app.browser.webservice.reference_xhtml_representation"/>
+ <adapter
+ factory="lp.app.browser.webservice.text_xhtml_representation"/>
+
</configure>
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2011-01-14 10:06:25 +0000
+++ lib/lp/app/browser/tales.py 2011-01-20 22:33:30 +0000
@@ -78,7 +78,10 @@
def format_link(obj, view_name=None):
"""Return the equivalent of obj/fmt:link as a string."""
adapter = queryAdapter(obj, IPathAdapter, 'fmt')
- return adapter.link(view_name)
+ link = getattr(adapter, 'link', None)
+ if link is None:
+ raise NotImplementedError("Missing link function on adapter.")
+ return link(view_name)
class MenuLinksDict(dict):
=== renamed file 'lib/lp/registry/browser/webservice.py' => 'lib/lp/app/browser/webservice.py'
--- lib/lp/registry/browser/webservice.py 2011-01-20 22:32:56 +0000
+++ lib/lp/app/browser/webservice.py 2011-01-20 22:33:30 +0000
@@ -8,6 +8,7 @@
from lazr.restful.interfaces import (
IFieldHTMLRenderer,
+ IReference,
IWebServiceClientRequest,
)
from zope import component
@@ -18,22 +19,24 @@
from zope.schema.interfaces import IText
from lp.app.browser.stringformatter import FormattersAPI
-from lp.app.browser.tales import PersonFormatterAPI
-from lp.services.fields import IPersonChoice
-
-
-@component.adapter(Interface, IPersonChoice, IWebServiceClientRequest)
+from lp.app.browser.tales import format_link
+
+
+@component.adapter(Interface, IReference, IWebServiceClientRequest)
@implementer(IFieldHTMLRenderer)
-def person_xhtml_representation(context, field, request):
- """Render a person as a link to the person."""
+def reference_xhtml_representation(context, field, request):
+ """Render an object as a link to the object."""
def render(value):
- # The value is a webservice link to a person.
- person = getattr(context, field.__name__, None)
- if person is None:
+ # The value is a webservice link to the object, we want field value.
+ obj = getattr(context, field.__name__, None)
+ if obj is None:
return ''
else:
- return PersonFormatterAPI(person).link(None)
+ try:
+ return format_link(obj)
+ except NotImplementedError:
+ return value
return render
=== modified file 'lib/lp/app/javascript/picker.js'
--- lib/lp/app/javascript/picker.js 2011-01-20 22:32:56 +0000
+++ lib/lp/app/javascript/picker.js 2011-01-20 22:33:30 +0000
@@ -23,10 +23,8 @@
* Defaults to false, should be a boolean.
* config.show_assign_me_botton: Should the 'assign me' button be shown?
* Defaults to false, should be a boolean.
- * config.non_searchable_vocabulary: No search bar is shown, and the
- * vocabulary is required to not implement IHugeVocabulary. The
- * vocabularies values are then offered in a batched way for
- * selection. Defaults to false.
+ * config.show_search_box: Should the search box be shown.
+ * Vocabularies that are not huge should not have a search box.
*/
namespace.addPickerPatcher = function (
vocabulary, resource_uri, attribute_name,
@@ -40,30 +38,30 @@
var show_assign_me_button = false;
var remove_button_text = 'Remove';
var null_display_value = 'None';
- var non_searchable_vocabulary = false;
+ var show_search_box = true;
var full_resource_uri = LP.client.get_absolute_uri(resource_uri);
var current_context_uri = LP.client.cache['context']['self_link'];
var editing_main_context = (full_resource_uri == current_context_uri);
if (config !== undefined) {
- if (config.remove_button_text) {
+ if (config.remove_button_text !== undefined) {
remove_button_text = config.remove_button_text;
}
- if (config.null_display_value) {
+ if (config.null_display_value !== undefined) {
null_display_value = config.null_display_value;
}
- if (config.show_remove_button) {
+ if (config.show_remove_button !== undefined) {
show_remove_button = config.show_remove_button;
}
- if (config.show_assign_me_button) {
+ if (config.show_assign_me_button !== undefined) {
show_assign_me_button = config.show_assign_me_button;
}
- if (config.non_searchable_vocabulary) {
- non_searchable_vocabulary = config.non_searchable_vocabulary;
+ if (config.show_search_box !== undefined) {
+ show_search_box = config.show_search_box;
}
}
@@ -125,7 +123,7 @@
} else if (current_field == 'self_link') {
picker._resource_uri = element.get('innerHTML');
content_uri_has_changed = (
- resource_uri != picker._resource_uri);
+ full_resource_uri != picker._resource_uri);
}
}
});
@@ -139,7 +137,7 @@
// fix this.
var new_url = picker._resource_uri.replace('/api/devel', '');
window.location = new_url;
- }
+ }
};
var patch_payload = {};
@@ -215,7 +213,7 @@
picker.set('footer_slot', extra_buttons);
activator.subscribe('act', function (e) {
- if (non_searchable_vocabulary) {
+ if (!show_search_box) {
picker.set('min_search_chars', 0);
picker.fire('search', '');
picker.get('contentBox').one('.yui3-picker-search-box').addClass('unseen');
=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js 2011-01-20 22:32:56 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js 2011-01-20 22:33:30 +0000
@@ -1352,21 +1352,13 @@
if ((LP.client.links.me !== undefined) && (LP.client.links.me !== null)) {
if (Y.Lang.isValue(bugtarget_content)) {
if (conf.target_is_product) {
- if (Y.UA.webkit) {
- bugtarget_content.replaceChild(
- Y.DOM.create(
- '<a href="+editstatus" ' +
- ' class="sprite edit yui3-activator-act" />'),
- bugtarget_content.one('.yui3-activator-act'));
- } else {
- var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
+ var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
'Product',
conf.bugtask_path,
"target_link",
bugtarget_content.get('id'),
{"step_title": "Search products",
"header": "Change product"});
- }
}
}
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-01-18 18:44:11 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-01-20 22:33:30 +0000
@@ -58,11 +58,12 @@
)
from canonical.launchpad.webapp.authorization import check_permission
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
-from canonical.widgets.suggestion import RecipeOwnerWidget
from canonical.widgets.itemswidgets import (
LabeledMultiCheckBoxWidget,
LaunchpadRadioWidget,
)
+from canonical.widgets.lazrjs import InlineEditPickerWidget
+from canonical.widgets.suggestion import RecipeOwnerWidget
from lp.app.browser.launchpadform import (
action,
custom_widget,
@@ -182,7 +183,12 @@
super(SourcePackageRecipeView, self).initialize()
self.request.response.addWarningNotification(RECIPE_BETA_MESSAGE)
recipe = self.context
- if self.dailyBuildWithoutUploadPermission():
+ if recipe.build_daily and recipe.daily_build_archive is None:
+ self.request.response.addWarningNotification(
+ structured(
+ "Daily builds for this recipe will <strong>not</strong> "
+ "occur.<br/><br/>There is no PPA."))
+ elif self.dailyBuildWithoutUploadPermission():
self.request.response.addWarningNotification(
structured(
"Daily builds for this recipe will <strong>not</strong> "
@@ -226,6 +232,30 @@
return not has_upload
return False
+ @property
+ def person_picker(self):
+ return InlineEditPickerWidget(
+ self.context, self.request, ISourcePackageRecipe['owner'],
+ format_link(self.context.owner),
+ content_box_id='recipe-owner',
+ header='Change owner',
+ step_title='Select a new owner')
+
+ @property
+ def archive_picker(self):
+ ppa = self.context.daily_build_archive
+ if ppa is None:
+ initial_html = 'None'
+ else:
+ initial_html = format_link(ppa)
+ return InlineEditPickerWidget(
+ self.context, self.request,
+ ISourcePackageAddSchema['daily_build_archive'],
+ initial_html,
+ content_box_id='recipe-ppa',
+ header='Change daily build archive',
+ step_title='Select a PPA')
+
class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
"""A view for requesting builds of a SourcePackageRecipe."""
=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-15 06:32:40 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-20 22:33:30 +0000
@@ -194,8 +194,12 @@
logger = logging.getLogger()
builds = []
for recipe in recipes:
+ recipe.is_stale = False
logger.debug(
'Recipe %s/%s is stale', recipe.owner.name, recipe.name)
+ if recipe.daily_build_archive is None:
+ logger.debug(' - No daily build archive specified.')
+ continue
for distroseries in recipe.distroseries:
series_name = distroseries.named_version
try:
@@ -215,7 +219,6 @@
else:
logger.debug(' - build requested for %s', series_name)
builds.append(build)
- recipe.is_stale = False
return builds
def _unqueueBuild(self):
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-10 03:22:42 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-20 22:33:30 +0000
@@ -235,13 +235,21 @@
self.assertIs(None, build.manifest)
def test_makeDailyBuilds(self):
- self.assertEqual([],
- SourcePackageRecipeBuild.makeDailyBuilds())
+ self.assertEqual([], SourcePackageRecipeBuild.makeDailyBuilds())
recipe = self.factory.makeSourcePackageRecipe(build_daily=True)
- build = SourcePackageRecipeBuild.makeDailyBuilds()[0]
+ [build] = SourcePackageRecipeBuild.makeDailyBuilds()
self.assertEqual(recipe, build.recipe)
self.assertEqual(list(recipe.distroseries), [build.distroseries])
+ def test_makeDailyBuilds_skips_missing_archive(self):
+ """When creating daily builds, skip ones that are already pending."""
+ recipe = self.factory.makeSourcePackageRecipe(
+ build_daily=True, is_stale=True)
+ with person_logged_in(recipe.owner):
+ recipe.daily_build_archive = None
+ builds = SourcePackageRecipeBuild.makeDailyBuilds()
+ self.assertEqual([], builds)
+
def test_makeDailyBuilds_logs_builds(self):
# If a logger is passed into the makeDailyBuilds method, each recipe
# that a build is requested for gets logged.
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-01-20 22:32:56 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-01-20 22:33:30 +0000
@@ -13,27 +13,6 @@
padding-left: 2em;
}
</style>
- <script type="text/javascript"
- tal:content="string:
- LPS.use('lp.app.picker', function(Y) {
- Y.on('load', function(e) {
-
- var config = {
- header: 'Change owner',
- step_title: 'Select a new owner',
- non_searchable_vocabulary: true
- };
-
- Y.lp.app.picker.addPickerPatcher(
- 'UserTeamsParticipationPlusSelf',
- LP.client.cache['context']['self_link'],
- 'owner_link',
- 'recipe-owner',
- config);
-
- }, window);
- });
- "/>
</metal:block>
<body>
@@ -76,17 +55,7 @@
<dl id="owner">
<dt>Owner:</dt>
- <dd>
- <span id="recipe-owner">
- <span class="yui3-activator-data-box">
- <tal:owner replace="structure context/owner/fmt:link" />
- </span>
- <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
- tal:condition="context/required:launchpad.Edit">
- Edit
- </button>
- <div class="yui3-activator-message-box yui3-activator-hidden" />
- </span>
+ <dd tal:content="structure view/person_picker"/>
</dl>
<dl id="base-branch">
<dt>Base branch:</dt>
@@ -98,10 +67,7 @@
</dl>
<dl id="daily_build_archive">
<dt>Daily build archive:</dt>
- <dd tal:content="structure context/daily_build_archive/fmt:link"
- tal:condition="context/daily_build_archive">
- </dd>
- <dd tal:condition="not: context/daily_build_archive">None</dd>
+ <dd tal:content="structure view/archive_picker"/>
</dl>
<dl id="distros">
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2011-01-19 22:53:32 +0000
+++ lib/lp/registry/browser/configure.zcml 2011-01-20 22:33:30 +0000
@@ -2328,9 +2328,4 @@
rootsite="api"
attribute_to_parent="owner" />
- <adapter
- factory="lp.registry.browser.webservice.person_xhtml_representation"/>
- <adapter
- factory="lp.registry.browser.webservice.text_xhtml_representation"/>
-
</configure>
=== modified file 'versions.cfg'
--- versions.cfg 2011-01-20 22:32:56 +0000
+++ versions.cfg 2011-01-20 22:33:30 +0000
@@ -33,7 +33,7 @@
lazr.delegates = 1.2.0
lazr.enum = 1.1.2
lazr.lifecycle = 1.1
-lazr.restful = 0.15.1
+lazr.restful = 0.15.2
lazr.restfulclient = 0.11.1
lazr.smtptest = 1.1
lazr.testing = 0.1.1