← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/validate-milestone-tags into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/validate-milestone-tags into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1159619 in Launchpad itself: "milestonetag validation is only performed at the DB-level"
  https://bugs.launchpad.net/launchpad/+bug/1159619

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/validate-milestone-tags/+merge/155171

Validate tags during {Product,Distribution}Series:+addmilestone, Milestone:+edit, and IMilestone.setTags(), rather than relying on the database constraint firing which will result in an OOPS.

I also moved validate_tags() from milestone's browser code, and put it with milestonetag's model code, since the places that were pulling it in import from there anyway.

Deal with some whitespace.
-- 
https://code.launchpad.net/~stevenk/launchpad/validate-milestone-tags/+merge/155171
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/validate-milestone-tags into lp:launchpad.
=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py	2012-12-20 14:55:13 +0000
+++ lib/lp/registry/browser/milestone.py	2013-03-25 06:17:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Milestone views."""
@@ -21,7 +21,6 @@
     'MilestoneView',
     'MilestoneViewMixin',
     'ObjectMilestonesView',
-    'validate_tags',
     ]
 
 
@@ -46,7 +45,6 @@
     LaunchpadFormView,
     safe_action,
     )
-from lp.app.validators.name import valid_name
 from lp.app.widgets.date import DateWidget
 from lp.bugs.browser.bugtask import BugTaskListingItem
 from lp.bugs.browser.structuralsubscription import (
@@ -72,7 +70,10 @@
 from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import IProduct
-from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
+from lp.registry.model.milestonetag import (
+    ProjectGroupMilestoneTag,
+    validate_tags,
+    )
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
     canonical_url,
@@ -259,12 +260,9 @@
         tags = self._bug_task_tags.get(bugtask.id, ())
         people = self._bug_task_people
         return BugTaskListingItem(
-            bugtask,
-            badge_property['has_branch'],
-            badge_property['has_specification'],
-            badge_property['has_patch'],
-            tags,
-            people)
+            bugtask, badge_property['has_branch'],
+            badge_property['has_specification'], badge_property['has_patch'],
+            tags, people)
 
     @cachedproperty
     def bugtasks(self):
@@ -346,8 +344,7 @@
         a project milestone tag, else return False."""
         return (
             IProjectGroupMilestone.providedBy(self.context) or
-            self.is_project_milestone_tag
-            )
+            self.is_project_milestone_tag)
 
     @property
     def has_bugs_or_specs(self):
@@ -428,7 +425,27 @@
     should_show_bugs_and_blueprints = False
 
 
-class MilestoneAddView(LaunchpadFormView):
+class MilestoneTagBase:
+
+    def extendFields(self):
+        """See `LaunchpadFormView`.
+
+        Add a text-entry widget for milestone tags since there is not property
+        on the interface.
+        """
+        tag_entry = TextLine(
+            __name__='tags', title=u'Tags', required=False,
+            constraint=lambda value: validate_tags(value.split()))
+        self.form_fields += form.Fields(
+            tag_entry, render_context=self.render_context)
+        # Make an instance attribute to avoid mutating the class attribute.
+        self.field_names = getattr(self, '_field_names', self.field_names)[:]
+        # Insert the tags field before the summary.
+        summary_index = self.field_names.index('summary')
+        self.field_names.insert(summary_index, tag_entry.__name__)
+
+
+class MilestoneAddView(MilestoneTagBase, LaunchpadFormView):
     """A view for creating a new Milestone."""
 
     schema = IMilestone
@@ -438,22 +455,7 @@
     custom_widget('dateexpected', DateWidget)
 
     def extendFields(self):
-        """See `LaunchpadFormView`.
-
-        Add a text-entry widget for milestone tags since there is not property
-        on the interface.
-        """
-        tag_entry = TextLine(
-            __name__='tags',
-            title=u'Tags',
-            required=False)
-        self.form_fields += form.Fields(
-            tag_entry, render_context=self.render_context)
-        # Make an instance attribute to avoid mutating the class attribute.
-        self.field_names = self.field_names[:]
-        # Insert the tags field before the summary.
-        summary_index = self.field_names.index('summary')
-        self.field_names.insert(summary_index, tag_entry.__name__)
+        super(MilestoneAddView, self).extendFields()
 
     @action(_('Register Milestone'), name='register')
     def register_action(self, action, data):
@@ -479,7 +481,7 @@
         return canonical_url(self.context)
 
 
-class MilestoneEditView(LaunchpadEditFormView):
+class MilestoneEditView(MilestoneTagBase, LaunchpadEditFormView):
     """A view for editing milestone properties.
 
     This view supports editing of properties such as the name, the date it is
@@ -516,27 +518,10 @@
 
     @property
     def initial_values(self):
-        tags = self.context.getTags()
-        tagstring = ' '.join(tags)
-        return dict(tags=tagstring)
+        return {'tags': u' '.join(self.context.getTags())}
 
     def extendFields(self):
-        """See `LaunchpadFormView`.
-
-        Add a text-entry widget for milestone tags since there is not property
-        on the interface.
-        """
-        tag_entry = TextLine(
-            __name__='tags',
-            title=u'Tags',
-            required=False)
-        self.form_fields += form.Fields(
-            tag_entry, render_context=self.render_context)
-        # Make an instance attribute to avoid mutating the class attribute.
-        self.field_names = self._field_names[:]
-        # Insert the tags field before the summary.
-        summary_index = self.field_names.index('summary')
-        self.field_names.insert(summary_index, tag_entry.__name__)
+        super(MilestoneEditView, self).extendFields()
 
     def setUpFields(self):
         """See `LaunchpadFormView`.
@@ -614,14 +599,6 @@
         self.next_url = canonical_url(series)
 
 
-def validate_tags(tags):
-    """Check that `separator` separated `tags` are valid tag names."""
-    return (
-        all(valid_name(tag) for tag in tags) and
-        len(set(tags)) == len(tags)
-        )
-
-
 class ISearchMilestoneTagsForm(Interface):
     """Schema for the search milestone tags form."""
 

=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py	2012-10-12 01:56:34 +0000
+++ lib/lp/registry/browser/project.py	2013-03-25 06:17:46 +0000
@@ -76,7 +76,6 @@
     IRegistryCollectionNavigationMenu,
     RegistryCollectionActionMenuBase,
     )
-from lp.registry.browser.milestone import validate_tags
 from lp.registry.browser.objectreassignment import ObjectReassignmentView
 from lp.registry.browser.pillar import PillarViewMixin
 from lp.registry.browser.product import (
@@ -90,7 +89,10 @@
     IProjectGroupSeries,
     IProjectGroupSet,
     )
-from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
+from lp.registry.model.milestonetag import (
+    ProjectGroupMilestoneTag,
+    validate_tags,
+    )
 from lp.services.feeds.browser import FeedsMixin
 from lp.services.fields import (
     PillarAliases,

=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py	2013-01-25 05:59:00 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py	2013-03-25 06:17:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test milestone views."""
@@ -156,10 +156,9 @@
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        TestCaseWithFactory.setUp(self)
+        super(TestAddMilestoneViews, self).setUp()
         self.product = self.factory.makeProduct()
-        self.series = (
-            self.factory.makeProductSeries(product=self.product))
+        self.series = self.factory.makeProductSeries(product=self.product)
         self.owner = self.product.owner
         login_person(self.owner)
 
@@ -168,8 +167,7 @@
             'field.name': '1.1',
             'field.actions.register': 'Register Milestone',
             }
-        view = create_initialized_view(
-            self.series, '+addmilestone', form=form)
+        view = create_initialized_view(self.series, '+addmilestone', form=form)
         self.assertEqual([], view.errors)
 
     def test_add_milestone_with_good_date(self):
@@ -178,8 +176,7 @@
             'field.dateexpected': '2010-10-10',
             'field.actions.register': 'Register Milestone',
             }
-        view = create_initialized_view(
-            self.series, '+addmilestone', form=form)
+        view = create_initialized_view(self.series, '+addmilestone', form=form)
         # It's important to make sure no errors occured,
         # but also confirm that the milestone was created.
         self.assertEqual([], view.errors)
@@ -191,8 +188,7 @@
             'field.dateexpected': '1010-10-10',
             'field.actions.register': 'Register Milestone',
             }
-        view = create_initialized_view(
-            self.series, '+addmilestone', form=form)
+        view = create_initialized_view(self.series, '+addmilestone', form=form)
         error_msg = view.errors[0].errors[0]
         expected_msg = (
             "Date could not be formatted. Provide a date formatted "
@@ -206,19 +202,27 @@
             'field.tags': tags,
             'field.actions.register': 'Register Milestone',
             }
-        view = create_initialized_view(
-            self.series, '+addmilestone', form=form)
+        view = create_initialized_view(self.series, '+addmilestone', form=form)
         self.assertEqual([], view.errors)
         expected = sorted(tags.split())
         self.assertEqual(expected, self.product.milestones[0].getTags())
 
+    def test_add_milestone_with_invalid_tags(self):
+        form = {
+            'field.name': '1.1',
+            'field.tags': '&%&%*',
+            'field.actions.register': 'Register Milestone',
+            }
+        view = create_initialized_view(self.series, '+addmilestone', form=form)
+        self.assertEqual(1, len(view.errors))
+
 
 class TestMilestoneEditView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        TestCaseWithFactory.setUp(self)
+        super(TestMilestoneEditView, self).setUp()
         self.product = self.factory.makeProduct()
         self.milestone = self.factory.makeMilestone(
             name='orig-name', product=self.product)
@@ -226,35 +230,38 @@
         login_person(self.owner)
 
     def test_edit_milestone_with_tags(self):
-        orig_tags = u'b a c'
+        orig_tags = u'ba ac'
         self.milestone.setTags(orig_tags.split(), self.owner)
-        new_tags = u'z a B'
+        new_tags = u'za ab'
         form = {
             'field.name': 'new-name',
             'field.tags': new_tags,
             'field.actions.update': 'Update',
             }
-        view = create_initialized_view(
-            self.milestone, '+edit', form=form)
+        view = create_initialized_view(self.milestone, '+edit', form=form)
         self.assertEqual([], view.errors)
         self.assertEqual('new-name', self.milestone.name)
         expected = sorted(new_tags.lower().split())
         self.assertEqual(expected, self.milestone.getTags())
 
     def test_edit_milestone_clear_tags(self):
-        orig_tags = u'b a c'
+        orig_tags = u'ba ac'
         self.milestone.setTags(orig_tags.split(), self.owner)
         form = {
             'field.name': 'new-name',
             'field.tags': '',
             'field.actions.update': 'Update',
             }
+        view = create_initialized_view(self.milestone, '+edit', form=form)
+        self.assertEqual([], view.errors)
+        self.assertEqual('new-name', self.milestone.name)
+        self.assertEqual([], self.milestone.getTags())
+
+    def test_edit_milestone_with_invalid_tags(self):
+        form = {'field.tags': '&%$^', 'field.actions.update': 'Update'}
         view = create_initialized_view(
             self.milestone, '+edit', form=form)
-        self.assertEqual([], view.errors)
-        self.assertEqual('new-name', self.milestone.name)
-        expected = []
-        self.assertEqual(expected, self.milestone.getTags())
+        self.assertEqual(1, len(view.errors))
 
 
 class TestMilestoneDeleteView(TestCaseWithFactory):

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2013-01-25 04:28:40 +0000
+++ lib/lp/registry/model/milestone.py	2013-03-25 06:17:46 +0000
@@ -139,6 +139,14 @@
         super(MultipleProductReleases, self).__init__(msg)
 
 
+@error_status(httplib.BAD_REQUEST)
+class InvalidTags(Exception):
+    """Raised when tags are invalid."""
+    
+    def __init__(self, msg='Tags are invalid.'):
+        super(InvalidTags, self).__init__(msg)
+
+
 class MilestoneData:
     implements(IMilestoneData)
 
@@ -312,7 +320,7 @@
         assert self.product_release is None, (
             "You cannot delete a milestone which has a product release "
             "associated with it.")
-        SQLBase.destroySelf(self)
+        super(Milestone, self).destroySelf()
 
     def getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
@@ -323,9 +331,11 @@
     def setTags(self, tags, user):
         """See IMilestone."""
         # Circular reference prevention.
-        from lp.registry.model.milestonetag import MilestoneTag
+        from lp.registry.model.milestonetag import MilestoneTag, validate_tags
         store = Store.of(self)
         if tags:
+            if not validate_tags(tags):
+                raise InvalidTags()
             current_tags = set(self.getTags())
             new_tags = set(tags)
             if new_tags == current_tags:

=== modified file 'lib/lp/registry/model/milestonetag.py'
--- lib/lp/registry/model/milestonetag.py	2012-10-18 16:49:57 +0000
+++ lib/lp/registry/model/milestonetag.py	2013-03-25 06:17:46 +0000
@@ -7,6 +7,7 @@
 __all__ = [
     'MilestoneTag',
     'ProjectGroupMilestoneTag',
+    'validate_tags',
     ]
 
 
@@ -23,6 +24,7 @@
 from storm.references import Reference
 from zope.interface import implements
 
+from lp.app.validators.name import valid_name
 from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
 from lp.registry.model.milestone import (
     Milestone,
@@ -93,3 +95,10 @@
                 Milestone.productID == Product.id,
                 Product.project == self.target,
                 tag_constraints))
+
+
+def validate_tags(tags):
+    """Check that `separator` separated `tags` are valid tag names."""
+    return (
+        all(valid_name(tag) for tag in tags) and
+        len(set(tags)) == len(tags))

=== modified file 'lib/lp/registry/tests/test_milestonetag.py'
--- lib/lp/registry/tests/test_milestonetag.py	2012-10-18 16:49:57 +0000
+++ lib/lp/registry/tests/test_milestonetag.py	2013-03-25 06:17:46 +0000
@@ -7,6 +7,7 @@
 
 import datetime
 
+from lazr.restfulclient.errors import BadRequest
 import transaction
 
 from lp.registry.model.milestonetag import (
@@ -243,3 +244,7 @@
         self.ws_milestone.lp_save()
         transaction.begin()
         self.assertEqual(sorted(tags2), self.milestone.getTags())
+
+    def test_set_tags_invalid(self):
+        self.assertRaises(
+            BadRequest, self.ws_milestone.setTags, tags=[u'&%&%^&'])


Follow ups