Merge lp:~jcsackett/launchpad/bad-state-transition-641266 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 11660
Proposed branch: lp:~jcsackett/launchpad/bad-state-transition-641266
Merge into: lp:launchpad
Diff against target: 151 lines (+100/-6)
4 files modified
lib/canonical/launchpad/interfaces/__init__.py (+1/-0)
lib/lp/registry/errors.py (+20/-0)
lib/lp/registry/model/teammembership.py (+11/-6)
lib/lp/registry/tests/test_teammembership_webservice.py (+68/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/bad-state-transition-641266
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+36726@code.launchpad.net

Commit message

Restructure stacked assertions into exceptions based on conditionals to provide appropriate errors to API users instead of OOPSing.

Description of the change

Summary
=======

Exposes an API error and refactors some assertions to more cleanly deal with an error condition across the API instead of OOPSing.

Proposed Fix
============

Rather than the stacked asserts in the setStatus method, implement a conditional tree to check each condition and raise a new Error class related to bad transitions that has been registered with the webservice.

Pre-Implementation Talk
=======================

Spoke with Curtis Hovey about the error conditions and API usage.

Spoke further with Aaron Bentley about appropriate exposure of Exceptions across the API.

Implementation details
======================

As in proposed.

Tests
=====

bin/test -t test_teammembership_webservice

Demo and Q/A
============

Use launchpadlib to connect launchpad.dev. Either create or find a team and retrieve one of its teammembership entries. Attempt to setStatus to one that isn't valid for the current (e.g. switch to Proposed from Approved).

Lint
====
make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/interfaces/__init__.py
  lib/lp/registry/errors.py
  lib/lp/registry/model/teammembership.py
  lib/lp/registry/tests/test_teammembership_webservice.py

./lib/lp/registry/model/teammembership.py
     569: Line exceeds 78 characters.

I can see no clear way to shorten the line on 569; as it predates this branch, I've elected to leave it alone, but welcome suggestions to clean it up.

There are a horde of errors in canonical.launchpad.interface.__init__; it was touched as part of getting errors registered with webservice. Another branch is in the works to remove registry items (or at least errors) from this file and into lp.registry.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

As discussed, expose should not be necessary.

Revision history for this message
j.c.sackett (jcsackett) wrote :

> As discussed, expose should not be necessary.

Expose has been removed. It did require adding a line to the mess that is canonical.launchpad.interfaces.__init__; I have another branch in the works to move all registry error related matters out of that module, but it's out of scope for this bug fix.

Revision history for this message
Graham Binns (gmb) wrote :

> === modified file 'lib/canonical/launchpad/interfaces/__init__.py'
> --- lib/canonical/launchpad/interfaces/__init__.py 2010-09-11 19:25:13 +0000
> +++ lib/canonical/launchpad/interfaces/__init__.py 2010-09-28 15:23:43 +0000
> @@ -96,6 +96,7 @@
> from canonical.launchpad.interfaces.packagerelationship import *
> from canonical.launchpad.interfaces.pathlookup import *
> from lp.registry.interfaces.poll import *
> +from lp.registry.errors import TeamMembershipTransitionError
> from lp.soyuz.interfaces.processor import *
> from lp.registry.interfaces.product import *
> from lp.registry.interfaces.productlicense import *
> === added file 'lib/lp/registry/errors.py'
> --- lib/lp/registry/errors.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/registry/errors.py 2010-09-28 15:23:43 +0000
> @@ -0,0 +1,20 @@
> +# Copyright 2009 Canonical Ltd. This software is licensed under the

It's 2010. If you want to update standard_template and
standard_test_template to be correct (for the next three months anyway)
then that's fine with me.

Other than that, r=me.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/interfaces/__init__.py'
--- lib/canonical/launchpad/interfaces/__init__.py 2010-09-11 19:25:13 +0000
+++ lib/canonical/launchpad/interfaces/__init__.py 2010-09-29 21:54:47 +0000
@@ -96,6 +96,7 @@
96from canonical.launchpad.interfaces.packagerelationship import *96from canonical.launchpad.interfaces.packagerelationship import *
97from canonical.launchpad.interfaces.pathlookup import *97from canonical.launchpad.interfaces.pathlookup import *
98from lp.registry.interfaces.poll import *98from lp.registry.interfaces.poll import *
99from lp.registry.errors import TeamMembershipTransitionError
99from lp.soyuz.interfaces.processor import *100from lp.soyuz.interfaces.processor import *
100from lp.registry.interfaces.product import *101from lp.registry.interfaces.product import *
101from lp.registry.interfaces.productlicense import *102from lp.registry.interfaces.productlicense import *
102103
=== added file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/errors.py 2010-09-29 21:54:47 +0000
@@ -0,0 +1,20 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5__all__ = [
6 'TeamMembershipTransitionError',
7 ]
8
9import httplib
10
11from lazr.restful.declarations import webservice_error
12
13
14class TeamMembershipTransitionError(ValueError):
15 """Indicates something has gone wrong with the transtiion.
16
17 Generally, this indicates a bad transition (e.g. approved to proposed)
18 or an invalid transition (e.g. unicorn).
19 """
20 webservice_error(httplib.BAD_REQUEST)
021
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2010-09-27 20:47:58 +0000
+++ lib/lp/registry/model/teammembership.py 2010-09-29 21:54:47 +0000
@@ -46,6 +46,7 @@
46from canonical.launchpad.mailnotification import MailWrapper46from canonical.launchpad.mailnotification import MailWrapper
47from canonical.launchpad.webapp import canonical_url47from canonical.launchpad.webapp import canonical_url
48from lp.app.browser.tales import DurationFormatterAPI48from lp.app.browser.tales import DurationFormatterAPI
49from lp.registry.errors import TeamMembershipTransitionError
49from lp.registry.interfaces.person import (50from lp.registry.interfaces.person import (
50 IPersonSet,51 IPersonSet,
51 TeamMembershipRenewalPolicy,52 TeamMembershipRenewalPolicy,
@@ -331,11 +332,14 @@
331 declined: [proposed, approved, admin],332 declined: [proposed, approved, admin],
332 invited: [approved, admin, invitation_declined],333 invited: [approved, admin, invitation_declined],
333 invitation_declined: [invited, approved, admin]}334 invitation_declined: [invited, approved, admin]}
334 assert self.status in state_transition, (335
335 "Unknown status: %s" % self.status.name)336 if self.status not in state_transition:
336 assert status in state_transition[self.status], (337 raise TeamMembershipTransitionError(
337 "Bad state transition from %s to %s"338 "Unknown status: %s" % self.status.name)
338 % (self.status.name, status.name))339 if status not in state_transition[self.status]:
340 raise TeamMembershipTransitionError(
341 "Bad state transition from %s to %s"
342 % (self.status.name, status.name))
339343
340 if status in ACTIVE_STATES and self.team in self.person.allmembers:344 if status in ACTIVE_STATES and self.team in self.person.allmembers:
341 raise CyclicalTeamMembershipError(345 raise CyclicalTeamMembershipError(
@@ -589,7 +593,8 @@
589 SELECT 1 FROM TeamParticipation593 SELECT 1 FROM TeamParticipation
590 WHERE person = %(person_id)s AND team IN (594 WHERE person = %(person_id)s AND team IN (
591 SELECT person595 SELECT person
592 FROM TeamParticipation JOIN Person ON (person = Person.id)596 FROM TeamParticipation JOIN Person ON
597 (person = Person.id)
593 WHERE team = %(team_id)s598 WHERE team = %(team_id)s
594 AND person NOT IN (%(team_id)s, %(person_id)s)599 AND person NOT IN (%(team_id)s, %(person_id)s)
595 AND teamowner IS NOT NULL600 AND teamowner IS NOT NULL
596601
=== added file 'lib/lp/registry/tests/test_teammembership_webservice.py'
--- lib/lp/registry/tests/test_teammembership_webservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_teammembership_webservice.py 2010-09-29 21:54:47 +0000
@@ -0,0 +1,68 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import unittest
7
8from zope.component import getUtility
9
10from canonical.testing import DatabaseFunctionalLayer
11from lazr.restfulclient.errors import HTTPError
12from lp.registry.interfaces.teammembership import (
13 ITeamMembershipSet,
14 TeamMembershipStatus,
15 )
16from lp.testing import (
17 TestCaseWithFactory,
18 launchpadlib_for,
19 )
20
21
22class TestTeamMembershipTransitions(TestCaseWithFactory):
23
24 layer = DatabaseFunctionalLayer
25
26 def setUp(self):
27 super(TestTeamMembershipTransitions, self).setUp()
28 self.person = self.factory.makePerson(name='some-person')
29 owner = self.factory.makePerson()
30 self.team = self.factory.makeTeam(
31 name='some-team',
32 owner=owner)
33 membership_set = getUtility(ITeamMembershipSet)
34 membership = membership_set.new(
35 self.person,
36 self.team,
37 TeamMembershipStatus.APPROVED,
38 self.person)
39 self.launchpad = launchpadlib_for("test", owner.name)
40
41 def test_no_such_status(self):
42 # An error should be thrown when transitioning to a status that
43 # doesn't exist.
44 team = self.launchpad.people['some-team']
45 team_membership = team.members_details[1]
46 # The error in this instance should be a valueerror, b/c the
47 # WADL used by launchpadlib will enforce the method args.
48 api_exception = self.assertRaises(
49 ValueError,
50 team_membership.setStatus,
51 status='NOTVALIDSTATUS')
52
53 def test_invalid_transition(self):
54 # An error should be thrown when transitioning to a status that
55 # isn't a valid move.
56 team = self.launchpad.people['some-team']
57 team_membership = team.members_details[1]
58 # The error used here should be an HTTPError, since it is being
59 # passed back by the server across the API.
60 api_exception = self.assertRaises(
61 HTTPError,
62 team_membership.setStatus,
63 status='Proposed')
64 self.assertEqual(400, api_exception.response.status)
65
66
67def test_suite():
68 return unittest.TestLoader().loadTestsFromName(__name__)